@@ -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,27 @@ 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+ for _ , analyses := range c .selectAnalyses {
149+ if strings .ToLower (analyses ) == "circular" || strings .ToLower (analyses ) == "deps" {
150+ skipCircular = false
151+ break
152+ }
153+ }
122154
123155 // Count issues found
124156 var issueCount int
@@ -168,14 +200,33 @@ func (c *CheckCommand) runCheck(cmd *cobra.Command, args []string) error {
168200 }
169201 }
170202
203+ // Pass prefix when calling loop detection
204+ if ! skipCircular {
205+ circularIssues , err := c .checkCircularDependencies (cmd , args , c .prefix )
206+ if err != nil {
207+ // Print error message
208+ fmt .Fprintf (cmd .ErrOrStderr (), "❌ Circular dependency check failed: %v\n " , err )
209+ // Return an error, terminate the process, and the CLI will exit with an error code of 1.
210+ return err
211+ }
212+ c .circularIssueCount = circularIssues
213+ issueCount += circularIssues
214+ }
215+
171216 // Handle results
172217 if hasErrors {
173218 return fmt .Errorf ("analysis failed with errors" )
174219 }
175220
176221 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
222+ if c .allowCircularDeps && issueCount == c .circularIssueCount {
223+ if ! c .quiet {
224+ fmt .Fprintf (cmd .ErrOrStderr (), "! Found %d circular dependency warning(s) (allowed by flag)\n " , issueCount )
225+ }
226+ } else {
227+ fmt .Fprintf (cmd .ErrOrStderr (), "❌ Found %d quality issue(s)\n " , issueCount )
228+ os .Exit (1 ) // Exit with code 1 to indicate issues found
229+ }
179230 }
180231
181232 if ! c .quiet {
@@ -204,7 +255,10 @@ func (c *CheckCommand) determineEnabledAnalyses() (skipComplexity bool, skipDead
204255// containsAnalysis checks if the specified analysis is in the select list
205256func (c * CheckCommand ) containsAnalysis (analysis string ) bool {
206257 for _ , a := range c .selectAnalyses {
207- if strings .ToLower (a ) == analysis {
258+ lowered := strings .ToLower (a )
259+ if lowered == analysis ||
260+ (analysis == "circular" && lowered == "deps" ) ||
261+ (analysis == "deps" && lowered == "circular" ) {
208262 return true
209263 }
210264 }
@@ -232,6 +286,8 @@ func (c *CheckCommand) validateSelectedAnalyses() error {
232286 "complexity" : true ,
233287 "deadcode" : true ,
234288 "clones" : true ,
289+ "circular" : true ,
290+ "deps" : true ,
235291 }
236292 for _ , analysis := range c .selectAnalyses {
237293 if ! validAnalyses [strings .ToLower (analysis )] {
@@ -456,3 +512,40 @@ func NewCheckCmd() *cobra.Command {
456512 checkCommand := NewCheckCommand ()
457513 return checkCommand .CreateCobraCommand ()
458514}
515+
516+ // checkCircularDependencies performs circular dependency detection on the provided paths,
517+ // using the CircularDependencyService to build a dependency graph and detect cycles.
518+ func (c * CheckCommand ) checkCircularDependencies (cmd * cobra.Command , args []string , prefix string ) (int , error ) {
519+ // Establish and utilize services for dependency graph construction and loop detection
520+ service := service .NewCircularDependencyService ()
521+
522+ // Detect the loop cycles from given paths, with start node and prefix filtering
523+ cycles , err := service .DetectCycles (args , c .desiredStart , prefix )
524+ if err != nil {
525+ return 0 , err
526+ }
527+
528+ importPositions := service .GetImportPositions ()
529+
530+ issueCount := len (cycles )
531+
532+ // Iterate detected cycles and output formatted circular dependency messages
533+ for _ , cycle := range cycles {
534+ pos := importPositions [cycle [0 ]][cycle [1 ]]
535+ if pos .Line == 0 {
536+ pos .Line = 1
537+ }
538+ if pos .Column == 0 {
539+ pos .Column = 1
540+ }
541+ fmt .Fprintf (cmd .ErrOrStderr (), "%s:%d:%d: circular dependency detected: %s\n " ,
542+ cycle [0 ], pos .Line , pos .Column , strings .Join (cycle , " -> " ))
543+ }
544+
545+ // Enforce maxCycles limit and allowCircularDeps flag, return error if limit exceeded
546+ if issueCount > c .maxCycles && ! c .allowCircularDeps {
547+ return issueCount , fmt .Errorf ("too many circular dependencies" )
548+ }
549+
550+ return issueCount , nil
551+ }
0 commit comments