WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit c03bfc7

Browse files
authored
Merge pull request #106 from mongodb/audit-cli-bugfixes
Audit CLI Bugfixes and Enhancements
2 parents 9453fcd + 4a1a27e commit c03bfc7

34 files changed

+2486
-315
lines changed

audit-cli/README.md

Lines changed: 114 additions & 44 deletions
Large diffs are not rendered by default.

audit-cli/commands/analyze/includes/analyzer.go

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

audit-cli/commands/analyze/includes/output.go

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package includes
33
import (
44
"fmt"
55
"path/filepath"
6+
"strings"
7+
8+
"github.com/mongodb/code-example-tooling/audit-cli/internal/projectinfo"
69
)
710

811
// PrintTree prints the include tree structure.
@@ -17,7 +20,8 @@ func PrintTree(analysis *IncludeAnalysis) {
1720
fmt.Println("INCLUDE TREE")
1821
fmt.Println("============================================================")
1922
fmt.Printf("Root File: %s\n", analysis.RootFile)
20-
fmt.Printf("Total Files: %d\n", analysis.TotalFiles)
23+
fmt.Printf("Unique Files: %d\n", analysis.TotalFiles)
24+
fmt.Printf("Include Directives: %d\n", analysis.TotalIncludeDirectives)
2125
fmt.Printf("Max Depth: %d\n", analysis.MaxDepth)
2226
fmt.Println("============================================================")
2327
fmt.Println()
@@ -45,13 +49,13 @@ func printTreeNode(node *IncludeNode, prefix string, isLast bool, isRoot bool) {
4549

4650
// Print the current node
4751
if isRoot {
48-
fmt.Printf("%s\n", filepath.Base(node.FilePath))
52+
fmt.Printf("%s\n", formatDisplayPath(node.FilePath))
4953
} else {
5054
connector := "├── "
5155
if isLast {
5256
connector = "└── "
5357
}
54-
fmt.Printf("%s%s%s\n", prefix, connector, filepath.Base(node.FilePath))
58+
fmt.Printf("%s%s%s\n", prefix, connector, formatDisplayPath(node.FilePath))
5559
}
5660

5761
// Print children
@@ -82,7 +86,8 @@ func PrintList(analysis *IncludeAnalysis) {
8286
fmt.Println("INCLUDE FILE LIST")
8387
fmt.Println("============================================================")
8488
fmt.Printf("Root File: %s\n", analysis.RootFile)
85-
fmt.Printf("Total Files: %d\n", analysis.TotalFiles)
89+
fmt.Printf("Unique Files: %d\n", analysis.TotalFiles)
90+
fmt.Printf("Include Directives: %d\n", analysis.TotalIncludeDirectives)
8691
fmt.Println("============================================================")
8792
fmt.Println()
8893

@@ -105,7 +110,8 @@ func PrintSummary(analysis *IncludeAnalysis) {
105110
fmt.Println("INCLUDE ANALYSIS SUMMARY")
106111
fmt.Println("============================================================")
107112
fmt.Printf("Root File: %s\n", analysis.RootFile)
108-
fmt.Printf("Total Files: %d\n", analysis.TotalFiles)
113+
fmt.Printf("Unique Files: %d\n", analysis.TotalFiles)
114+
fmt.Printf("Include Directives: %d\n", analysis.TotalIncludeDirectives)
109115
fmt.Printf("Max Depth: %d\n", analysis.MaxDepth)
110116
fmt.Println("============================================================")
111117
fmt.Println()
@@ -114,3 +120,69 @@ func PrintSummary(analysis *IncludeAnalysis) {
114120
fmt.Println()
115121
}
116122

123+
// formatDisplayPath formats a file path for display in the tree or verbose output.
124+
//
125+
// This function returns:
126+
// - If the file is in an "includes" directory: the path starting from "includes"
127+
// (e.g., "includes/load-sample-data.rst" or "includes/php/connection.rst")
128+
// - If the file is NOT in an "includes" directory: the path from the source directory
129+
// (e.g., "get-started/node/language-connection-steps.rst")
130+
//
131+
// This helps writers understand the directory structure and disambiguate files
132+
// with the same name in different directories.
133+
//
134+
// Parameters:
135+
// - filePath: Absolute path to the file
136+
//
137+
// Returns:
138+
// - string: Formatted path for display
139+
func formatDisplayPath(filePath string) string {
140+
// Try to find the source directory
141+
sourceDir, err := projectinfo.FindSourceDirectory(filePath)
142+
if err != nil {
143+
// If we can't find source directory, just return the base name
144+
return filepath.Base(filePath)
145+
}
146+
147+
// Check if the file is in an includes directory
148+
// Walk up from the file to find if there's an "includes" directory
149+
dir := filepath.Dir(filePath)
150+
var includesDir string
151+
152+
for {
153+
// Check if the current directory is named "includes"
154+
if filepath.Base(dir) == "includes" {
155+
includesDir = dir
156+
break
157+
}
158+
159+
// Move up one directory
160+
parent := filepath.Dir(dir)
161+
162+
// If we've reached the source directory or root, stop
163+
if parent == dir || dir == sourceDir {
164+
break
165+
}
166+
167+
dir = parent
168+
}
169+
170+
// If we found an includes directory, get the relative path from it
171+
if includesDir != "" {
172+
relPath, err := filepath.Rel(includesDir, filePath)
173+
if err == nil && !strings.HasPrefix(relPath, "..") {
174+
// Prepend "includes/" to show it's in the includes directory
175+
return filepath.Join("includes", relPath)
176+
}
177+
}
178+
179+
// Otherwise, get the relative path from the source directory
180+
relPath, err := filepath.Rel(sourceDir, filePath)
181+
if err != nil {
182+
// If we can't get relative path, just return the base name
183+
return filepath.Base(filePath)
184+
}
185+
186+
return relPath
187+
}
188+

audit-cli/commands/analyze/includes/types.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ type IncludeNode struct {
1414
// This type holds both the tree structure and the flat list of all files
1515
// discovered through include directives.
1616
type IncludeAnalysis struct {
17-
RootFile string // The original file that was analyzed
18-
Tree *IncludeNode // Tree structure of include relationships
19-
AllFiles []string // Flat list of all files (in order discovered)
20-
TotalFiles int // Total number of unique files
21-
MaxDepth int // Maximum depth of include nesting
17+
RootFile string // The original file that was analyzed
18+
Tree *IncludeNode // Tree structure of include relationships
19+
AllFiles []string // Flat list of all files (in order discovered)
20+
TotalFiles int // Total number of unique files
21+
TotalIncludeDirectives int // Total number of include directive instances across all files
22+
MaxDepth int // Maximum depth of include nesting
2223
}
2324

0 commit comments

Comments
 (0)