@@ -26,17 +26,26 @@ type CheckCommand struct {
2626
2727 // Select specific analyses to run
2828 selectAnalyses []string
29+
30+ // Check circular dependency related fields
31+ allowCircularDeps bool
32+ circularIssueCount int // Number of circular dependency issues found in the current run
33+ maxCycles int
34+ desiredStart string // Optional node name used to rotate detected cycles to start with this node
35+ prefix string // Path prefix used to filter modules during circular dependency detection
2936}
3037
3138// NewCheckCommand creates a new check command
3239func NewCheckCommand () * CheckCommand {
3340 return & CheckCommand {
34- configFile : "" ,
35- quiet : false ,
36- maxComplexity : 10 , // Fail if complexity > 10
37- allowDeadCode : false , // Fail on any dead code
38- skipClones : false ,
39- selectAnalyses : []string {},
41+ configFile : "" ,
42+ quiet : false ,
43+ maxComplexity : 10 , // Fail if complexity > 10
44+ allowDeadCode : false , // Fail on any dead code
45+ skipClones : false ,
46+ selectAnalyses : []string {},
47+ allowCircularDeps : false ,
48+ maxCycles : 0 ,
4049 }
4150}
4251
@@ -100,6 +109,10 @@ Examples:
100109 cmd .Flags ().StringSliceVarP (& c .selectAnalyses , "select" , "s" , []string {},
101110 "Comma-separated list of analyses to run: complexity, deadcode, clones" )
102111
112+ // Check circular dependency
113+ cmd .Flags ().BoolVar (& c .allowCircularDeps , "allow-circular-deps" , false , "Allow circular dependencies (warnings only)" )
114+ cmd .Flags ().IntVar (& c .maxCycles , "max-cycles" , 0 , "Maximum allowed circular dependency cycles before failing" )
115+
103116 return cmd
104117}
105118
@@ -117,8 +130,26 @@ func (c *CheckCommand) runCheck(cmd *cobra.Command, args []string) error {
117130 }
118131 }
119132
133+ // Dynamically generate the prefix using the first path as the base, ensuring it ends with a slash
134+ if len (args ) > 0 {
135+ if args [0 ] == "." {
136+ c .prefix = "" // Avoid prefixes starting with "./"; set to an empty string to facilitate matching absolute paths
137+ } else {
138+ c .prefix = strings .TrimSuffix (args [0 ], "/" ) + "/"
139+ }
140+ } else {
141+ c .prefix = ""
142+ }
143+
120144 // Create use case configuration
121145 skipComplexity , skipDeadCode , skipClones := c .determineEnabledAnalyses ()
146+ // Check circular dependencies
147+ skipCircular := true
148+ if len (c .selectAnalyses ) > 0 {
149+ skipCircular = ! c .containsAnalysis ("deps" )
150+ } else {
151+ skipCircular = false
152+ }
122153
123154 // Count issues found
124155 var issueCount int
@@ -168,14 +199,33 @@ func (c *CheckCommand) runCheck(cmd *cobra.Command, args []string) error {
168199 }
169200 }
170201
202+ // Pass prefix when calling loop detection
203+ if ! skipCircular {
204+ circularIssues , err := c .checkCircularDependencies (cmd , args , c .prefix )
205+ if err != nil {
206+ // Print error message
207+ fmt .Fprintf (cmd .ErrOrStderr (), "❌ Circular dependency check failed: %v\n " , err )
208+ // Return an error, terminate the process, and the CLI will exit with an error code of 1.
209+ return err
210+ }
211+ c .circularIssueCount = circularIssues
212+ issueCount += circularIssues
213+ }
214+
171215 // Handle results
172216 if hasErrors {
173217 return fmt .Errorf ("analysis failed with errors" )
174218 }
175219
176220 if issueCount > 0 {
177- fmt .Fprintf (cmd .ErrOrStderr (), "❌ Found %d quality issue(s)\n " , issueCount )
178- os .Exit (1 ) // Exit with code 1 to indicate issues found
221+ if c .allowCircularDeps && issueCount == c .circularIssueCount {
222+ if ! c .quiet {
223+ fmt .Fprintf (cmd .ErrOrStderr (), "! Found %d circular dependency warning(s) (allowed by flag)\n " , issueCount )
224+ }
225+ } else {
226+ fmt .Fprintf (cmd .ErrOrStderr (), "❌ Found %d quality issue(s)\n " , issueCount )
227+ os .Exit (1 ) // Exit with code 1 to indicate issues found
228+ }
179229 }
180230
181231 if ! c .quiet {
@@ -232,6 +282,8 @@ func (c *CheckCommand) validateSelectedAnalyses() error {
232282 "complexity" : true ,
233283 "deadcode" : true ,
234284 "clones" : true ,
285+ "circular" : true ,
286+ "deps" : true ,
235287 }
236288 for _ , analysis := range c .selectAnalyses {
237289 if ! validAnalyses [strings .ToLower (analysis )] {
@@ -456,3 +508,40 @@ func NewCheckCmd() *cobra.Command {
456508 checkCommand := NewCheckCommand ()
457509 return checkCommand .CreateCobraCommand ()
458510}
511+
512+ // checkCircularDependencies performs circular dependency detection on the provided paths,
513+ // using the CircularDependencyService to build a dependency graph and detect cycles.
514+ func (c * CheckCommand ) checkCircularDependencies (cmd * cobra.Command , args []string , prefix string ) (int , error ) {
515+ // Establish and utilize services for dependency graph construction and loop detection
516+ service := service .NewCircularDependencyService ()
517+
518+ // Detect the loop cycles from given paths, with start node and prefix filtering
519+ cycles , err := service .DetectCycles (args , c .desiredStart , prefix )
520+ if err != nil {
521+ return 0 , err
522+ }
523+
524+ importPositions := service .GetImportPositions ()
525+
526+ issueCount := len (cycles )
527+
528+ // Iterate detected cycles and output formatted circular dependency messages
529+ for _ , cycle := range cycles {
530+ pos := importPositions [cycle [0 ]][cycle [1 ]]
531+ if pos .Line == 0 {
532+ pos .Line = 1
533+ }
534+ if pos .Column == 0 {
535+ pos .Column = 1
536+ }
537+ fmt .Fprintf (cmd .ErrOrStderr (), "%s:%d:%d: circular dependency detected: %s\n " ,
538+ cycle [0 ], pos .Line , pos .Column , strings .Join (cycle , " -> " ))
539+ }
540+
541+ // Enforce maxCycles limit and allowCircularDeps flag, return error if limit exceeded
542+ if issueCount > c .maxCycles && ! c .allowCircularDeps {
543+ return issueCount , fmt .Errorf ("too many circular dependencies" )
544+ }
545+
546+ return issueCount , nil
547+ }
0 commit comments