@@ -36,28 +36,31 @@ func AnalyzeIncludes(filePath string, verbose bool) (*IncludeAnalysis, error) {
3636 }
3737
3838 // Build the tree structure
39- visited := make (map [string ]bool )
40- tree , err := buildIncludeTree (absPath , visited , verbose , 0 )
39+ // Use a recursion path to detect true circular includes
40+ recursionPath := make (map [string ]bool )
41+ // Track which files we've seen for verbose output (to show duplicates with different bullet)
42+ seenFiles := make (map [string ]bool )
43+ tree , err := buildIncludeTree (absPath , recursionPath , seenFiles , verbose , 0 )
4144 if err != nil {
4245 return nil , err
4346 }
4447
45- // Collect all unique files from the visited map
46- // The visited map contains all unique files that were processed
47- allFiles := make ([]string , 0 , len (visited ))
48- for file := range visited {
49- allFiles = append (allFiles , file )
50- }
48+ // Collect all unique files from the tree
49+ allFiles := collectUniqueFiles (tree )
5150
5251 // Calculate max depth
5352 maxDepth := calculateMaxDepth (tree , 0 )
5453
54+ // Count total include directives
55+ totalDirectives := countIncludeDirectives (tree )
56+
5557 analysis := & IncludeAnalysis {
56- RootFile : absPath ,
57- Tree : tree ,
58- AllFiles : allFiles ,
59- TotalFiles : len (allFiles ),
60- MaxDepth : maxDepth ,
58+ RootFile : absPath ,
59+ Tree : tree ,
60+ AllFiles : allFiles ,
61+ TotalFiles : len (allFiles ),
62+ TotalIncludeDirectives : totalDirectives ,
63+ MaxDepth : maxDepth ,
6164 }
6265
6366 return analysis , nil
@@ -66,18 +69,19 @@ func AnalyzeIncludes(filePath string, verbose bool) (*IncludeAnalysis, error) {
6669// buildIncludeTree recursively builds a tree of include relationships.
6770//
6871// This function creates an IncludeNode for the given file and recursively
69- // processes all files it includes, preventing circular includes.
72+ // processes all files it includes, preventing true circular includes.
7073//
7174// Parameters:
7275// - filePath: Path to the file to process
73- // - visited: Map tracking already-processed files (prevents circular includes)
76+ // - recursionPath: Map tracking files in the current recursion path (prevents circular includes)
77+ // - seenFiles: Map tracking files we've already printed (for duplicate indicators in verbose mode)
7478// - verbose: If true, print detailed processing information
7579// - depth: Current depth in the tree (for verbose output)
7680//
7781// Returns:
7882// - *IncludeNode: Tree node representing this file and its includes
7983// - error: Any error encountered during processing
80- func buildIncludeTree (filePath string , visited map [string ]bool , verbose bool , depth int ) (* IncludeNode , error ) {
84+ func buildIncludeTree (filePath string , recursionPath map [ string ] bool , seenFiles map [string ]bool , verbose bool , depth int ) (* IncludeNode , error ) {
8185 absPath , err := filepath .Abs (filePath )
8286 if err != nil {
8387 return nil , err
@@ -89,15 +93,19 @@ func buildIncludeTree(filePath string, visited map[string]bool, verbose bool, de
8993 Children : []* IncludeNode {},
9094 }
9195
92- // Check if we've already visited this file ( circular include)
93- if visited [absPath ] {
96+ // Check if this file is already in the current recursion path (true circular include)
97+ if recursionPath [absPath ] {
9498 if verbose {
9599 indent := getIndent (depth )
96- fmt .Printf ("%s⚠ Circular include detected: %s\n " , indent , filepath . Base (absPath ))
100+ fmt .Printf ("%s⚠ Circular include detected: %s\n " , indent , formatDisplayPath (absPath ))
97101 }
98102 return node , nil
99103 }
100- visited [absPath ] = true
104+
105+ // Add this file to the recursion path
106+ recursionPath [absPath ] = true
107+ // Ensure we remove it when we're done processing this branch
108+ defer delete (recursionPath , absPath )
101109
102110 // Find include directives in this file
103111 includeFiles , err := rst .FindIncludeDirectives (absPath )
@@ -106,14 +114,31 @@ func buildIncludeTree(filePath string, visited map[string]bool, verbose bool, de
106114 includeFiles = []string {}
107115 }
108116
109- if verbose && len (includeFiles ) > 0 {
117+ // Print verbose output for this file
118+ if verbose {
110119 indent := getIndent (depth )
111- fmt .Printf ("%s📄 %s (%d includes)\n " , indent , filepath .Base (absPath ), len (includeFiles ))
120+ // Use hollow bullet (◦) for files we've seen before, filled bullet (•) for first occurrence
121+ bullet := "•"
122+ if seenFiles [absPath ] {
123+ bullet = "◦"
124+ } else {
125+ seenFiles [absPath ] = true
126+ }
127+
128+ if len (includeFiles ) > 0 {
129+ directiveWord := "include directives"
130+ if len (includeFiles ) == 1 {
131+ directiveWord = "include directive"
132+ }
133+ fmt .Printf ("%s%s %s (%d %s)\n " , indent , bullet , formatDisplayPath (absPath ), len (includeFiles ), directiveWord )
134+ } else {
135+ fmt .Printf ("%s%s %s\n " , indent , bullet , formatDisplayPath (absPath ))
136+ }
112137 }
113138
114139 // Recursively process each included file
115140 for _ , includeFile := range includeFiles {
116- childNode , err := buildIncludeTree (includeFile , visited , verbose , depth + 1 )
141+ childNode , err := buildIncludeTree (includeFile , recursionPath , seenFiles , verbose , depth + 1 )
117142 if err != nil {
118143 fmt .Fprintf (os .Stderr , "Warning: failed to process file %s: %v\n " , includeFile , err )
119144 continue
@@ -167,3 +192,70 @@ func getIndent(depth int) string {
167192 return indent
168193}
169194
195+ // collectUniqueFiles traverses the tree and collects all unique file paths.
196+ //
197+ // This function recursively walks the tree and builds a list of all unique
198+ // files that appear in the tree, even if they appear multiple times.
199+ //
200+ // Parameters:
201+ // - node: The root node of the tree to traverse
202+ //
203+ // Returns:
204+ // - []string: List of unique file paths
205+ func collectUniqueFiles (node * IncludeNode ) []string {
206+ if node == nil {
207+ return []string {}
208+ }
209+
210+ visited := make (map [string ]bool )
211+ var files []string
212+
213+ var traverse func (* IncludeNode )
214+ traverse = func (n * IncludeNode ) {
215+ if n == nil {
216+ return
217+ }
218+
219+ // Add this file if we haven't seen it before
220+ if ! visited [n .FilePath ] {
221+ visited [n .FilePath ] = true
222+ files = append (files , n .FilePath )
223+ }
224+
225+ // Traverse children
226+ for _ , child := range n .Children {
227+ traverse (child )
228+ }
229+ }
230+
231+ traverse (node )
232+ return files
233+ }
234+
235+ // countIncludeDirectives counts the total number of include directive instances in the tree.
236+ //
237+ // This function counts every include directive in every file, including duplicates.
238+ // For example, if file A includes file B, and file C also includes file B,
239+ // that counts as 2 include directives (even though B is only one unique file).
240+ //
241+ // Parameters:
242+ // - node: The root node of the tree to traverse
243+ //
244+ // Returns:
245+ // - int: Total number of include directive instances
246+ func countIncludeDirectives (node * IncludeNode ) int {
247+ if node == nil {
248+ return 0
249+ }
250+
251+ // Count the children of this node (these are the include directives in this file)
252+ count := len (node .Children )
253+
254+ // Recursively count include directives in all children
255+ for _ , child := range node .Children {
256+ count += countIncludeDirectives (child )
257+ }
258+
259+ return count
260+ }
261+
0 commit comments