From 9704b1a1497ba6b41cc3f8d9b689e010df44e5f2 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 24 Oct 2025 08:20:54 -0400 Subject: [PATCH 01/14] feat: add analyze-references cmd for finding all files that ref a target file --- audit-cli/README.md | 228 +++++++++++- audit-cli/commands/analyze/analyze.go | 8 +- .../commands/analyze/references/analyzer.go | 340 ++++++++++++++++++ .../commands/analyze/references/output.go | 215 +++++++++++ .../commands/analyze/references/references.go | 166 +++++++++ .../analyze/references/references_test.go | 291 +++++++++++++++ .../commands/analyze/references/types.go | 82 +++++ .../compare/file-contents/comparer.go | 4 +- .../compare/file-contents/version_resolver.go | 116 +----- .../internal/pathresolver/pathresolver.go | 119 ++++++ .../pathresolver/pathresolver_test.go | 197 ++++++++++ .../internal/pathresolver/source_finder.go | 55 +++ audit-cli/internal/pathresolver/types.go | 33 ++ .../internal/pathresolver/version_resolver.go | 196 ++++++++++ audit-cli/internal/rst/include_resolver.go | 43 +-- 15 files changed, 1936 insertions(+), 157 deletions(-) create mode 100644 audit-cli/commands/analyze/references/analyzer.go create mode 100644 audit-cli/commands/analyze/references/output.go create mode 100644 audit-cli/commands/analyze/references/references.go create mode 100644 audit-cli/commands/analyze/references/references_test.go create mode 100644 audit-cli/commands/analyze/references/types.go create mode 100644 audit-cli/internal/pathresolver/pathresolver.go create mode 100644 audit-cli/internal/pathresolver/pathresolver_test.go create mode 100644 audit-cli/internal/pathresolver/source_finder.go create mode 100644 audit-cli/internal/pathresolver/types.go create mode 100644 audit-cli/internal/pathresolver/version_resolver.go diff --git a/audit-cli/README.md b/audit-cli/README.md index 8b2cb50..f8789e8 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -58,7 +58,8 @@ audit-cli ├── search # Search through extracted content or source files │ └── find-string ├── analyze # Analyze RST file structures -│ └── includes +│ ├── includes +│ └── references └── compare # Compare files across versions └── file-contents ``` @@ -282,6 +283,196 @@ times (e.g., file A includes file C, and file B also includes file C), the file However, the tree view will show it in all locations where it appears, with subsequent occurrences marked as circular includes in verbose mode. +#### `analyze references` + +Find all files that reference a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, or `io-code-block` directives. + +The command searches all RST files (both `.rst` and `.txt` extensions) in the source directory tree. + +**Use Cases:** + +This command helps writers: +- Understand the impact of changes to a file (what pages will be affected) +- Find all usages of an include file across the documentation +- Track where code examples are referenced +- Identify orphaned files (files with no references) +- Plan refactoring by understanding file dependencies + +**Basic Usage:** + +```bash +# Find what references an include file +./audit-cli analyze references path/to/includes/fact.rst + +# Find what references a code example +./audit-cli analyze references path/to/code-examples/example.js + +# Get JSON output for automation +./audit-cli analyze references path/to/file.rst --format json + +# Show detailed information with line numbers +./audit-cli analyze references path/to/file.rst --verbose +``` + +**Flags:** + +- `--format ` - Output format: `text` (default) or `json` +- `-v, --verbose` - Show detailed information including line numbers and reference paths +- `-c, --count-only` - Only show the count of references (useful for quick checks and scripting) +- `--paths-only` - Only show the file paths, one per line (useful for piping to other commands) +- `-t, --directive-type ` - Filter by directive type: `include`, `literalinclude`, or `io-code-block` + +**Understanding the Counts:** + +The command shows two metrics: +- **Total Files**: Number of unique files that reference the target (deduplicated) +- **Total References**: Total number of directive occurrences (includes duplicates) + +When a file includes the target multiple times, it counts as: +- 1 file (in Total Files) +- Multiple references (in Total References) + +This helps identify both the impact scope (how many files) and duplicate includes (when references > files). + +**Supported Directive Types:** + +The command tracks three types of RST directives: + +1. **`.. include::`** - RST content includes + ```rst + .. include:: /includes/intro.rst + ``` + +2. **`.. literalinclude::`** - Code file references + ```rst + .. literalinclude:: /code-examples/example.py + :language: python + ``` + +3. **`.. io-code-block::`** - Input/output examples with file arguments + ```rst + .. io-code-block:: + + .. input:: /code-examples/query.js + :language: javascript + + .. output:: /code-examples/result.json + :language: json + ``` + +**Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) is not tracked. + +**Output Formats:** + +**Text** (default): +``` +============================================================ +REFERENCE ANALYSIS +============================================================ +Target File: /path/to/includes/intro.rst +Total Files: 3 +Total References: 4 +============================================================ + +include : 3 files, 4 references + + 1. [include] duplicate-include-test.rst (2 references) + 2. [include] include-test.rst + 3. [include] page.rst + +``` + +**Text with --verbose:** +``` +============================================================ +REFERENCE ANALYSIS +============================================================ +Target File: /path/to/includes/intro.rst +Total Files: 3 +Total References: 4 +============================================================ + +include : 3 files, 4 references + + 1. [include] duplicate-include-test.rst (2 references) + Line 6: /includes/intro.rst + Line 13: /includes/intro.rst + 2. [include] include-test.rst + Line 6: /includes/intro.rst + 3. [include] page.rst + Line 12: /includes/intro.rst + +``` + +**JSON** (--format json): +```json +{ + "target_file": "/path/to/includes/intro.rst", + "source_dir": "/path/to/source", + "total_files": 3, + "total_references": 4, + "referencing_files": [ + { + "FilePath": "/path/to/duplicate-include-test.rst", + "DirectiveType": "include", + "ReferencePath": "/includes/intro.rst", + "LineNumber": 6 + }, + { + "FilePath": "/path/to/duplicate-include-test.rst", + "DirectiveType": "include", + "ReferencePath": "/includes/intro.rst", + "LineNumber": 13 + }, + { + "FilePath": "/path/to/include-test.rst", + "DirectiveType": "include", + "ReferencePath": "/includes/intro.rst", + "LineNumber": 6 + } + ] +} +``` + +**Examples:** + +```bash +# Check if an include file is being used +./audit-cli analyze references ~/docs/source/includes/fact-atlas.rst + +# Find all pages that use a specific code example +./audit-cli analyze references ~/docs/source/code-examples/connect.py + +# Get machine-readable output for scripting +./audit-cli analyze references ~/docs/source/includes/fact.rst --format json | jq '.total_references' + +# See exactly where a file is referenced (with line numbers) +./audit-cli analyze references ~/docs/source/includes/intro.rst --verbose + +# Quick check: just show the count +./audit-cli analyze references ~/docs/source/includes/fact.rst --count-only +# Output: 5 + +# Get list of files for piping to other commands +./audit-cli analyze references ~/docs/source/includes/fact.rst --paths-only +# Output: +# page1.rst +# page2.rst +# page3.rst + +# Filter to only show include directives (not literalinclude or io-code-block) +./audit-cli analyze references ~/docs/source/includes/fact.rst --directive-type include + +# Filter to only show literalinclude references +./audit-cli analyze references ~/docs/source/code-examples/example.py --directive-type literalinclude + +# Combine filters: count only literalinclude references +./audit-cli analyze references ~/docs/source/code-examples/example.py -t literalinclude -c + +# Combine filters: list files that use this as an io-code-block +./audit-cli analyze references ~/docs/source/code-examples/query.js -t io-code-block --paths-only +``` + ### Compare Commands #### `compare file-contents` @@ -469,9 +660,15 @@ audit-cli/ │ │ └── report.go # Report generation │ ├── analyze/ # Analyze parent command │ │ ├── analyze.go # Parent command definition -│ │ └── includes/ # Includes analysis subcommand -│ │ ├── includes.go # Command logic -│ │ ├── analyzer.go # Include tree building +│ │ ├── includes/ # Includes analysis subcommand +│ │ │ ├── includes.go # Command logic +│ │ │ ├── analyzer.go # Include tree building +│ │ │ ├── output.go # Output formatting +│ │ │ └── types.go # Type definitions +│ │ └── references/ # References analysis subcommand +│ │ ├── references.go # Command logic +│ │ ├── references_test.go # Tests +│ │ ├── analyzer.go # Reference finding logic │ │ ├── output.go # Output formatting │ │ └── types.go # Type definitions │ └── compare/ # Compare parent command @@ -485,6 +682,12 @@ audit-cli/ │ ├── types.go # Type definitions │ └── version_resolver.go # Version path resolution ├── internal/ # Internal packages +│ ├── pathresolver/ # Path resolution utilities +│ │ ├── pathresolver.go # Core path resolution +│ │ ├── pathresolver_test.go # Tests +│ │ ├── source_finder.go # Source directory detection +│ │ ├── version_resolver.go # Version path resolution +│ │ └── types.go # Type definitions │ └── rst/ # RST parsing utilities │ ├── parser.go # Generic parsing with includes │ ├── include_resolver.go # Include directive resolution @@ -1075,6 +1278,23 @@ used as the base for resolving relative include paths. ## Internal Packages +### `internal/pathresolver` + +Provides centralized path resolution utilities for working with MongoDB documentation structure: + +- **Source directory detection** - Finds the documentation root by walking up the directory tree +- **Project info detection** - Identifies product directory, version, and whether a project is versioned +- **Version path resolution** - Resolves file paths across multiple documentation versions +- **Relative path resolution** - Resolves paths relative to the source directory + +**Key Functions:** +- `FindSourceDirectory(filePath string)` - Finds the source directory for a given file +- `DetectProjectInfo(filePath string)` - Detects project structure information +- `ResolveVersionPaths(referenceFile, productDir string, versions []string)` - Resolves paths across versions +- `ResolveRelativeToSource(sourceDir, relativePath string)` - Resolves relative paths + +See the code in `internal/pathresolver/` for implementation details. + ### `internal/rst` Provides reusable utilities for parsing and processing RST files: diff --git a/audit-cli/commands/analyze/analyze.go b/audit-cli/commands/analyze/analyze.go index dd4f9a8..c60aa34 100644 --- a/audit-cli/commands/analyze/analyze.go +++ b/audit-cli/commands/analyze/analyze.go @@ -3,12 +3,14 @@ // This package serves as the parent command for various analysis operations. // Currently supports: // - includes: Analyze include directive relationships in RST files +// - references: Find all files that reference a target file // // Future subcommands could include analyzing cross-references, broken links, or content metrics. package analyze import ( "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/includes" + "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/references" "github.com/spf13/cobra" ) @@ -22,12 +24,16 @@ func NewAnalyzeCommand() *cobra.Command { Short: "Analyze reStructuredText file structures", Long: `Analyze various aspects of reStructuredText files and their relationships. -Currently supports analyzing include directive relationships to understand file dependencies. +Currently supports: + - includes: Analyze include directive relationships (forward dependencies) + - references: Find all files that reference a target file (reverse dependencies) + Future subcommands may support analyzing cross-references, broken links, or content metrics.`, } // Add subcommands cmd.AddCommand(includes.NewIncludesCommand()) + cmd.AddCommand(references.NewReferencesCommand()) return cmd } diff --git a/audit-cli/commands/analyze/references/analyzer.go b/audit-cli/commands/analyze/references/analyzer.go new file mode 100644 index 0000000..5894945 --- /dev/null +++ b/audit-cli/commands/analyze/references/analyzer.go @@ -0,0 +1,340 @@ +package references + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" +) + +// Regular expressions for matching directives +var ( + // includeRegex matches: .. include:: /path/to/file.rst + includeRegex = regexp.MustCompile(`^\.\.\s+include::\s+(.+)$`) + + // literalIncludeRegex matches: .. literalinclude:: /path/to/file.ext + literalIncludeRegex = regexp.MustCompile(`^\.\.\s+literalinclude::\s+(.+)$`) + + // ioCodeBlockRegex matches: .. io-code-block:: + ioCodeBlockRegex = regexp.MustCompile(`^\.\.\s+io-code-block::`) + + // inputRegex matches: .. input:: /path/to/file.ext (within io-code-block) + inputRegex = regexp.MustCompile(`^\.\.\s+input::\s+(.+)$`) + + // outputRegex matches: .. output:: /path/to/file.ext (within io-code-block) + outputRegex = regexp.MustCompile(`^\.\.\s+output::\s+(.+)$`) +) + +// AnalyzeReferences finds all files that reference the target file. +// +// This function searches through all RST files in the source directory to find +// files that reference the target file using include, literalinclude, or +// io-code-block directives. +// +// Parameters: +// - targetFile: Absolute path to the file to analyze +// +// Returns: +// - *ReferenceAnalysis: The analysis results +// - error: Any error encountered during analysis +func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { + // Get absolute path + absTargetFile, err := filepath.Abs(targetFile) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Find the source directory + sourceDir, err := pathresolver.FindSourceDirectory(absTargetFile) + if err != nil { + return nil, fmt.Errorf("failed to find source directory: %w", err) + } + + // Initialize analysis result + analysis := &ReferenceAnalysis{ + TargetFile: absTargetFile, + SourceDir: sourceDir, + ReferencingFiles: []FileReference{}, + } + + // Walk through all RST files in the source directory + err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + // Only process RST files (.rst and .txt extensions) + ext := filepath.Ext(path) + if ext != ".rst" && ext != ".txt" { + return nil + } + + // Search for references in this file + refs, err := findReferencesInFile(path, absTargetFile, sourceDir) + if err != nil { + // Log error but continue processing other files + fmt.Fprintf(os.Stderr, "Warning: failed to process %s: %v\n", path, err) + return nil + } + + // Add any found references + analysis.ReferencingFiles = append(analysis.ReferencingFiles, refs...) + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk source directory: %w", err) + } + + // Update total counts + analysis.TotalReferences = len(analysis.ReferencingFiles) + analysis.TotalFiles = countUniqueFiles(analysis.ReferencingFiles) + + return analysis, nil +} + +// findReferencesInFile searches a single file for references to the target file. +// +// This function scans through the file line by line looking for include, +// literalinclude, and io-code-block directives that reference the target file. +// +// Parameters: +// - filePath: Path to the file to search +// - targetFile: Absolute path to the target file +// - sourceDir: Source directory (for resolving relative paths) +// +// Returns: +// - []FileReference: List of references found in this file +// - error: Any error encountered during processing +func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReference, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var references []FileReference + scanner := bufio.NewScanner(file) + lineNum := 0 + inIOCodeBlock := false + ioCodeBlockStartLine := 0 + + for scanner.Scan() { + lineNum++ + line := scanner.Text() + trimmedLine := strings.TrimSpace(line) + + // Check for io-code-block start + if ioCodeBlockRegex.MatchString(trimmedLine) { + inIOCodeBlock = true + ioCodeBlockStartLine = lineNum + continue + } + + // Check if we're exiting io-code-block (unindented line that's not empty) + if inIOCodeBlock && len(line) > 0 && line[0] != ' ' && line[0] != '\t' { + inIOCodeBlock = false + } + + // Check for include directive + if matches := includeRegex.FindStringSubmatch(trimmedLine); matches != nil { + refPath := strings.TrimSpace(matches[1]) + if referencesTarget(refPath, targetFile, sourceDir, filePath) { + references = append(references, FileReference{ + FilePath: filePath, + DirectiveType: "include", + ReferencePath: refPath, + LineNumber: lineNum, + }) + } + continue + } + + // Check for literalinclude directive + if matches := literalIncludeRegex.FindStringSubmatch(trimmedLine); matches != nil { + refPath := strings.TrimSpace(matches[1]) + if referencesTarget(refPath, targetFile, sourceDir, filePath) { + references = append(references, FileReference{ + FilePath: filePath, + DirectiveType: "literalinclude", + ReferencePath: refPath, + LineNumber: lineNum, + }) + } + continue + } + + // Check for input/output directives within io-code-block + if inIOCodeBlock { + // Check for input directive + if matches := inputRegex.FindStringSubmatch(trimmedLine); matches != nil { + refPath := strings.TrimSpace(matches[1]) + if referencesTarget(refPath, targetFile, sourceDir, filePath) { + references = append(references, FileReference{ + FilePath: filePath, + DirectiveType: "io-code-block", + ReferencePath: refPath, + LineNumber: ioCodeBlockStartLine, + }) + } + continue + } + + // Check for output directive + if matches := outputRegex.FindStringSubmatch(trimmedLine); matches != nil { + refPath := strings.TrimSpace(matches[1]) + if referencesTarget(refPath, targetFile, sourceDir, filePath) { + references = append(references, FileReference{ + FilePath: filePath, + DirectiveType: "io-code-block", + ReferencePath: refPath, + LineNumber: ioCodeBlockStartLine, + }) + } + continue + } + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return references, nil +} + +// referencesTarget checks if a reference path points to the target file. +// +// This function resolves the reference path and compares it to the target file. +// +// Parameters: +// - refPath: The path from the directive (e.g., "/includes/file.rst") +// - targetFile: Absolute path to the target file +// - sourceDir: Source directory (for resolving relative paths) +// - currentFile: Path to the file containing the reference +// +// Returns: +// - bool: true if the reference points to the target file +func referencesTarget(refPath, targetFile, sourceDir, currentFile string) bool { + // Resolve the reference path + var resolvedPath string + + if strings.HasPrefix(refPath, "/") { + // Absolute path (relative to source directory) + resolvedPath = filepath.Join(sourceDir, refPath) + } else { + // Relative path (relative to current file) + currentDir := filepath.Dir(currentFile) + resolvedPath = filepath.Join(currentDir, refPath) + } + + // Clean and get absolute path + resolvedPath = filepath.Clean(resolvedPath) + absResolvedPath, err := filepath.Abs(resolvedPath) + if err != nil { + return false + } + + // Compare with target file + return absResolvedPath == targetFile +} + +// FilterByDirectiveType filters the analysis results to only include references +// of the specified directive type. +// +// Parameters: +// - analysis: The original analysis results +// - directiveType: The directive type to filter by (include, literalinclude, io-code-block) +// +// Returns: +// - *ReferenceAnalysis: A new analysis with filtered results +func FilterByDirectiveType(analysis *ReferenceAnalysis, directiveType string) *ReferenceAnalysis { + filtered := &ReferenceAnalysis{ + TargetFile: analysis.TargetFile, + SourceDir: analysis.SourceDir, + ReferencingFiles: []FileReference{}, + ReferenceTree: analysis.ReferenceTree, + } + + // Filter references + for _, ref := range analysis.ReferencingFiles { + if ref.DirectiveType == directiveType { + filtered.ReferencingFiles = append(filtered.ReferencingFiles, ref) + } + } + + // Update counts + filtered.TotalReferences = len(filtered.ReferencingFiles) + filtered.TotalFiles = countUniqueFiles(filtered.ReferencingFiles) + + return filtered +} + +// countUniqueFiles counts the number of unique files in the reference list. +// +// Parameters: +// - refs: List of file references +// +// Returns: +// - int: Number of unique files +func countUniqueFiles(refs []FileReference) int { + uniqueFiles := make(map[string]bool) + for _, ref := range refs { + uniqueFiles[ref.FilePath] = true + } + return len(uniqueFiles) +} + +// GroupReferencesByFile groups references by file path and directive type. +// +// This function takes a flat list of references and groups them by file, +// counting how many times each file references the target. +// +// Parameters: +// - refs: List of file references +// +// Returns: +// - []GroupedFileReference: List of grouped references, sorted by file path +func GroupReferencesByFile(refs []FileReference) []GroupedFileReference { + // Group by file path and directive type + type groupKey struct { + filePath string + directiveType string + } + groups := make(map[groupKey][]FileReference) + + for _, ref := range refs { + key := groupKey{ref.FilePath, ref.DirectiveType} + groups[key] = append(groups[key], ref) + } + + // Convert to slice + var grouped []GroupedFileReference + for key, refs := range groups { + grouped = append(grouped, GroupedFileReference{ + FilePath: key.filePath, + DirectiveType: key.directiveType, + References: refs, + Count: len(refs), + }) + } + + // Sort by file path for consistent output + sort.Slice(grouped, func(i, j int) bool { + return grouped[i].FilePath < grouped[j].FilePath + }) + + return grouped +} + diff --git a/audit-cli/commands/analyze/references/output.go b/audit-cli/commands/analyze/references/output.go new file mode 100644 index 0000000..5785a8d --- /dev/null +++ b/audit-cli/commands/analyze/references/output.go @@ -0,0 +1,215 @@ +package references + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// OutputFormat represents the output format for the analysis results. +type OutputFormat string + +const ( + // FormatText is the default human-readable text format + FormatText OutputFormat = "text" + // FormatJSON is the JSON format + FormatJSON OutputFormat = "json" +) + +// PrintAnalysis prints the analysis results in the specified format. +// +// Parameters: +// - analysis: The analysis results to print +// - format: The output format (text or json) +// - verbose: If true, show additional details +func PrintAnalysis(analysis *ReferenceAnalysis, format OutputFormat, verbose bool) error { + switch format { + case FormatJSON: + return printJSON(analysis) + case FormatText: + printText(analysis, verbose) + return nil + default: + return fmt.Errorf("unknown output format: %s", format) + } +} + +// printText prints the analysis results in human-readable text format. +func printText(analysis *ReferenceAnalysis, verbose bool) { + fmt.Println("============================================================") + fmt.Println("REFERENCE ANALYSIS") + fmt.Println("============================================================") + fmt.Printf("Target File: %s\n", analysis.TargetFile) + fmt.Printf("Total Files: %d\n", analysis.TotalFiles) + fmt.Printf("Total References: %d\n", analysis.TotalReferences) + fmt.Println("============================================================") + fmt.Println() + + if analysis.TotalReferences == 0 { + fmt.Println("No files reference this file.") + fmt.Println() + return + } + + // Group references by directive type + byDirectiveType := groupByDirectiveType(analysis.ReferencingFiles) + + // Print breakdown by directive type with file and reference counts + for _, directiveType := range []string{"include", "literalinclude", "io-code-block"} { + if refs, ok := byDirectiveType[directiveType]; ok { + uniqueFiles := countUniqueFiles(refs) + totalRefs := len(refs) + if uniqueFiles == totalRefs { + // No duplicates - just show count + fmt.Printf("%-20s: %d\n", directiveType, uniqueFiles) + } else { + // Has duplicates - show both counts + if uniqueFiles == 1 { + fmt.Printf("%-20s: %d file, %d references\n", directiveType, uniqueFiles, totalRefs) + } else { + fmt.Printf("%-20s: %d files, %d references\n", directiveType, uniqueFiles, totalRefs) + } + } + } + } + fmt.Println() + + // Group references by file + grouped := GroupReferencesByFile(analysis.ReferencingFiles) + + // Print detailed list of referencing files + for i, group := range grouped { + // Get relative path from source directory for cleaner output + relPath, err := filepath.Rel(analysis.SourceDir, group.FilePath) + if err != nil { + relPath = group.FilePath + } + + // Print file path with directive type label + if group.Count > 1 { + // Multiple references from this file + fmt.Printf("%3d. [%s] %s (%d references)\n", i+1, group.DirectiveType, relPath, group.Count) + } else { + // Single reference + fmt.Printf("%3d. [%s] %s\n", i+1, group.DirectiveType, relPath) + } + + // Print line numbers in verbose mode + if verbose { + for _, ref := range group.References { + fmt.Printf(" Line %d: %s\n", ref.LineNumber, ref.ReferencePath) + } + } + } + + fmt.Println() +} + +// printJSON prints the analysis results in JSON format. +func printJSON(analysis *ReferenceAnalysis) error { + // Create a JSON-friendly structure + output := struct { + TargetFile string `json:"target_file"` + SourceDir string `json:"source_dir"` + TotalFiles int `json:"total_files"` + TotalReferences int `json:"total_references"` + ReferencingFiles []FileReference `json:"referencing_files"` + }{ + TargetFile: analysis.TargetFile, + SourceDir: analysis.SourceDir, + TotalFiles: analysis.TotalFiles, + TotalReferences: analysis.TotalReferences, + ReferencingFiles: analysis.ReferencingFiles, + } + + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(output) +} + +// groupByDirectiveType groups references by their directive type. +func groupByDirectiveType(refs []FileReference) map[string][]FileReference { + groups := make(map[string][]FileReference) + + for _, ref := range refs { + groups[ref.DirectiveType] = append(groups[ref.DirectiveType], ref) + } + + return groups +} + +// FormatReferencePath formats a reference path for display. +// +// This function shortens paths for better readability while maintaining +// enough context to identify the file. +func FormatReferencePath(path, sourceDir string) string { + // Try to get relative path from source directory + relPath, err := filepath.Rel(sourceDir, path) + if err != nil { + return path + } + + // If the relative path is shorter, use it + if len(relPath) < len(path) { + return relPath + } + + return path +} + +// GetDirectiveTypeLabel returns a human-readable label for a directive type. +func GetDirectiveTypeLabel(directiveType string) string { + labels := map[string]string{ + "include": "Include", + "literalinclude": "Literal Include", + "io-code-block": "I/O Code Block", + } + + if label, ok := labels[directiveType]; ok { + return label + } + + return strings.Title(directiveType) +} + +// PrintPathsOnly prints only the file paths, one per line. +// +// This is useful for piping to other commands or for simple scripting. +// +// Parameters: +// - analysis: The analysis results +// +// Returns: +// - error: Any error encountered during printing +func PrintPathsOnly(analysis *ReferenceAnalysis) error { + // Get unique file paths (in case there are duplicates) + seen := make(map[string]bool) + var paths []string + + for _, ref := range analysis.ReferencingFiles { + // Get relative path from source directory for cleaner output + relPath, err := filepath.Rel(analysis.SourceDir, ref.FilePath) + if err != nil { + relPath = ref.FilePath + } + + if !seen[relPath] { + seen[relPath] = true + paths = append(paths, relPath) + } + } + + // Sort for consistent output + sort.Strings(paths) + + // Print each path + for _, path := range paths { + fmt.Println(path) + } + + return nil +} + diff --git a/audit-cli/commands/analyze/references/references.go b/audit-cli/commands/analyze/references/references.go new file mode 100644 index 0000000..21fb15a --- /dev/null +++ b/audit-cli/commands/analyze/references/references.go @@ -0,0 +1,166 @@ +// Package references provides functionality for analyzing which files reference a target file. +// +// This package implements the "analyze references" subcommand, which finds all files +// that reference a given file through RST directives (include, literalinclude, io-code-block). +// +// The command performs reverse dependency analysis, showing which files depend on the +// target file. This is useful for: +// - Understanding the impact of changes to a file +// - Finding all usages of an include file +// - Tracking code example references +// - Identifying orphaned files (files with no references) +package references + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// NewReferencesCommand creates the references subcommand. +// +// This command analyzes which files reference a given target file through +// RST directives (include, literalinclude, io-code-block). +// +// Usage: +// analyze references /path/to/file.rst +// analyze references /path/to/code-example.js +// +// Flags: +// - --format: Output format (text or json) +// - -v, --verbose: Show detailed information including line numbers +// - -c, --count-only: Only show the count of references +// - --paths-only: Only show the file paths +// - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block) +func NewReferencesCommand() *cobra.Command { + var ( + format string + verbose bool + countOnly bool + pathsOnly bool + directiveType string + ) + + cmd := &cobra.Command{ + Use: "references [filepath]", + Short: "Find all files that reference a target file", + Long: `Find all files that reference a target file through RST directives. + +This command performs reverse dependency analysis, showing which files reference +the target file through include, literalinclude, or io-code-block directives. + +Supported directive types: + - .. include:: RST content includes + - .. literalinclude:: Code file references + - .. io-code-block:: Input/output examples with file arguments + +The command searches all RST files in the source directory tree and identifies +files that reference the target file. This is useful for: + - Understanding the impact of changes to a file + - Finding all usages of an include file + - Tracking code example references + - Identifying orphaned files (files with no references) + +Examples: + # Find what references an include file + analyze references /path/to/includes/fact.rst + + # Find what references a code example + analyze references /path/to/code-examples/example.js + + # Get JSON output + analyze references /path/to/file.rst --format json + + # Show detailed information with line numbers + analyze references /path/to/file.rst --verbose + + # Just show the count + analyze references /path/to/file.rst --count-only + + # Just show the file paths + analyze references /path/to/file.rst --paths-only + + # Filter by directive type + analyze references /path/to/file.rst --directive-type include`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runReferences(args[0], format, verbose, countOnly, pathsOnly, directiveType) + }, + } + + cmd.Flags().StringVar(&format, "format", "text", "Output format (text or json)") + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed information including line numbers") + cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of references") + cmd.Flags().BoolVar(&pathsOnly, "paths-only", false, "Only show the file paths (one per line)") + cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block)") + + return cmd +} + +// runReferences executes the references analysis. +// +// This function performs the analysis and prints the results in the specified format. +// +// Parameters: +// - targetFile: Path to the file to analyze +// - format: Output format (text or json) +// - verbose: If true, show detailed information +// - countOnly: If true, only show the count +// - pathsOnly: If true, only show the file paths +// - directiveType: Filter by directive type (empty string means all types) +// +// Returns: +// - error: Any error encountered during analysis +func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool, directiveType string) error { + // Validate directive type if specified + if directiveType != "" { + validTypes := map[string]bool{ + "include": true, + "literalinclude": true, + "io-code-block": true, + } + if !validTypes[directiveType] { + return fmt.Errorf("invalid directive type: %s (must be 'include', 'literalinclude', or 'io-code-block')", directiveType) + } + } + + // Validate format + outputFormat := OutputFormat(format) + if outputFormat != FormatText && outputFormat != FormatJSON { + return fmt.Errorf("invalid format: %s (must be 'text' or 'json')", format) + } + + // Validate flag combinations + if countOnly && pathsOnly { + return fmt.Errorf("cannot use --count-only and --paths-only together") + } + if (countOnly || pathsOnly) && outputFormat == FormatJSON { + return fmt.Errorf("--count-only and --paths-only are not compatible with --format json") + } + + // Perform analysis + analysis, err := AnalyzeReferences(targetFile) + if err != nil { + return fmt.Errorf("failed to analyze references: %w", err) + } + + // Filter by directive type if specified + if directiveType != "" { + analysis = FilterByDirectiveType(analysis, directiveType) + } + + // Handle count-only output + if countOnly { + fmt.Println(analysis.TotalReferences) + return nil + } + + // Handle paths-only output + if pathsOnly { + return PrintPathsOnly(analysis) + } + + // Print full results + return PrintAnalysis(analysis, outputFormat, verbose) +} + diff --git a/audit-cli/commands/analyze/references/references_test.go b/audit-cli/commands/analyze/references/references_test.go new file mode 100644 index 0000000..b98dfda --- /dev/null +++ b/audit-cli/commands/analyze/references/references_test.go @@ -0,0 +1,291 @@ +package references + +import ( + "path/filepath" + "testing" +) + +// TestAnalyzeReferences tests the AnalyzeReferences function with various scenarios. +func TestAnalyzeReferences(t *testing.T) { + // Get the testdata directory + testDataDir := "../../../testdata/input-files/source" + + tests := []struct { + name string + targetFile string + expectedReferences int + expectedDirectiveType string + }{ + { + name: "Include file with multiple references", + targetFile: "includes/intro.rst", + expectedReferences: 4, + expectedDirectiveType: "include", + }, + { + name: "Code example with literalinclude", + targetFile: "code-examples/example.py", + expectedReferences: 1, + expectedDirectiveType: "literalinclude", + }, + { + name: "Code example with multiple directive types", + targetFile: "code-examples/example.js", + expectedReferences: 2, // literalinclude + io-code-block + expectedDirectiveType: "", // mixed types + }, + { + name: "File with no references", + targetFile: "code-block-test.rst", + expectedReferences: 0, + expectedDirectiveType: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build absolute path to target file + targetPath := filepath.Join(testDataDir, tt.targetFile) + absTargetPath, err := filepath.Abs(targetPath) + if err != nil { + t.Fatalf("failed to get absolute path: %v", err) + } + + // Run analysis + analysis, err := AnalyzeReferences(absTargetPath) + if err != nil { + t.Fatalf("AnalyzeReferences failed: %v", err) + } + + // Check total references + if analysis.TotalReferences != tt.expectedReferences { + t.Errorf("expected %d references, got %d", tt.expectedReferences, analysis.TotalReferences) + } + + // Check that we got the expected number of referencing files + if len(analysis.ReferencingFiles) != tt.expectedReferences { + t.Errorf("expected %d referencing files, got %d", tt.expectedReferences, len(analysis.ReferencingFiles)) + } + + // If we expect a specific directive type, check it + if tt.expectedDirectiveType != "" && tt.expectedReferences > 0 { + foundExpectedType := false + for _, ref := range analysis.ReferencingFiles { + if ref.DirectiveType == tt.expectedDirectiveType { + foundExpectedType = true + break + } + } + if !foundExpectedType { + t.Errorf("expected to find directive type %s, but didn't", tt.expectedDirectiveType) + } + } + + // Verify source directory was found + if analysis.SourceDir == "" { + t.Error("source directory should not be empty") + } + + // Verify target file matches + if analysis.TargetFile != absTargetPath { + t.Errorf("expected target file %s, got %s", absTargetPath, analysis.TargetFile) + } + }) + } +} + +// TestFindReferencesInFile tests the findReferencesInFile function. +func TestFindReferencesInFile(t *testing.T) { + testDataDir := "../../../testdata/input-files/source" + sourceDir := testDataDir + + tests := []struct { + name string + searchFile string + targetFile string + expectedReferences int + expectedDirective string + }{ + { + name: "Include directive", + searchFile: "include-test.rst", + targetFile: "includes/intro.rst", + expectedReferences: 1, + expectedDirective: "include", + }, + { + name: "Literalinclude directive", + searchFile: "literalinclude-test.rst", + targetFile: "code-examples/example.py", + expectedReferences: 1, + expectedDirective: "literalinclude", + }, + { + name: "IO code block directive", + searchFile: "io-code-block-test.rst", + targetFile: "code-examples/example.js", + expectedReferences: 1, + expectedDirective: "io-code-block", + }, + { + name: "Duplicate includes", + searchFile: "duplicate-include-test.rst", + targetFile: "includes/intro.rst", + expectedReferences: 2, // Same file included twice + expectedDirective: "include", + }, + { + name: "No references", + searchFile: "code-block-test.rst", + targetFile: "includes/intro.rst", + expectedReferences: 0, + expectedDirective: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + searchPath := filepath.Join(testDataDir, tt.searchFile) + targetPath := filepath.Join(testDataDir, tt.targetFile) + + // Get absolute paths + absSearchPath, err := filepath.Abs(searchPath) + if err != nil { + t.Fatalf("failed to get absolute search path: %v", err) + } + absTargetPath, err := filepath.Abs(targetPath) + if err != nil { + t.Fatalf("failed to get absolute target path: %v", err) + } + absSourceDir, err := filepath.Abs(sourceDir) + if err != nil { + t.Fatalf("failed to get absolute source dir: %v", err) + } + + refs, err := findReferencesInFile(absSearchPath, absTargetPath, absSourceDir) + if err != nil { + t.Fatalf("findReferencesInFile failed: %v", err) + } + + if len(refs) != tt.expectedReferences { + t.Errorf("expected %d references, got %d", tt.expectedReferences, len(refs)) + } + + // Check directive type if we expect references + if tt.expectedReferences > 0 && tt.expectedDirective != "" { + for _, ref := range refs { + if ref.DirectiveType != tt.expectedDirective { + t.Errorf("expected directive type %s, got %s", tt.expectedDirective, ref.DirectiveType) + } + } + } + + // Verify all references have required fields + for _, ref := range refs { + if ref.FilePath == "" { + t.Error("reference should have a file path") + } + if ref.DirectiveType == "" { + t.Error("reference should have a directive type") + } + if ref.ReferencePath == "" { + t.Error("reference should have a reference path") + } + if ref.LineNumber == 0 { + t.Error("reference should have a line number") + } + } + }) + } +} + +// TestReferencesTarget tests the referencesTarget function. +func TestReferencesTarget(t *testing.T) { + testDataDir := "../../../testdata/input-files/source" + + // Get absolute paths + absTestDataDir, err := filepath.Abs(testDataDir) + if err != nil { + t.Fatalf("failed to get absolute test data dir: %v", err) + } + + tests := []struct { + name string + refPath string + targetFile string + currentFile string + expected bool + }{ + { + name: "Absolute path match", + refPath: "/includes/intro.rst", + targetFile: filepath.Join(absTestDataDir, "includes/intro.rst"), + currentFile: filepath.Join(absTestDataDir, "include-test.rst"), + expected: true, + }, + { + name: "Absolute path no match", + refPath: "/includes/intro.rst", + targetFile: filepath.Join(absTestDataDir, "includes/examples.rst"), + currentFile: filepath.Join(absTestDataDir, "include-test.rst"), + expected: false, + }, + { + name: "Relative path match", + refPath: "intro.rst", + targetFile: filepath.Join(absTestDataDir, "includes/intro.rst"), + currentFile: filepath.Join(absTestDataDir, "includes/nested-include.rst"), + expected: true, + }, + { + name: "Relative path no match", + refPath: "intro.rst", + targetFile: filepath.Join(absTestDataDir, "includes/examples.rst"), + currentFile: filepath.Join(absTestDataDir, "includes/nested-include.rst"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := referencesTarget(tt.refPath, tt.targetFile, absTestDataDir, tt.currentFile) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +// TestGroupByDirectiveType tests the groupByDirectiveType function. +func TestGroupByDirectiveType(t *testing.T) { + refs := []FileReference{ + {DirectiveType: "include", FilePath: "file1.rst"}, + {DirectiveType: "include", FilePath: "file2.rst"}, + {DirectiveType: "literalinclude", FilePath: "file3.rst"}, + {DirectiveType: "io-code-block", FilePath: "file4.rst"}, + {DirectiveType: "include", FilePath: "file5.rst"}, + } + + groups := groupByDirectiveType(refs) + + // Check that we have 3 groups + if len(groups) != 3 { + t.Errorf("expected 3 groups, got %d", len(groups)) + } + + // Check include group + if len(groups["include"]) != 3 { + t.Errorf("expected 3 include references, got %d", len(groups["include"])) + } + + // Check literalinclude group + if len(groups["literalinclude"]) != 1 { + t.Errorf("expected 1 literalinclude reference, got %d", len(groups["literalinclude"])) + } + + // Check io-code-block group + if len(groups["io-code-block"]) != 1 { + t.Errorf("expected 1 io-code-block reference, got %d", len(groups["io-code-block"])) + } +} + diff --git a/audit-cli/commands/analyze/references/types.go b/audit-cli/commands/analyze/references/types.go new file mode 100644 index 0000000..638c8b4 --- /dev/null +++ b/audit-cli/commands/analyze/references/types.go @@ -0,0 +1,82 @@ +package references + +// ReferenceAnalysis contains the results of analyzing which files reference a target file. +// +// This structure holds both a flat list of referencing files and a hierarchical +// tree structure showing the reference relationships. +type ReferenceAnalysis struct { + // TargetFile is the absolute path to the file being analyzed + TargetFile string + + // ReferencingFiles is a flat list of all files that reference the target + ReferencingFiles []FileReference + + // ReferenceTree is a hierarchical tree structure of references + // (for future use - showing nested references) + ReferenceTree *ReferenceNode + + // TotalReferences is the total number of directive occurrences + TotalReferences int + + // TotalFiles is the total number of unique files that reference the target + TotalFiles int + + // SourceDir is the source directory that was searched + SourceDir string +} + +// FileReference represents a single file that references the target file. +// +// This structure captures details about how and where the reference occurs. +type FileReference struct { + // FilePath is the absolute path to the file that references the target + FilePath string + + // DirectiveType is the type of directive used to reference the file + // Possible values: "include", "literalinclude", "io-code-block" + DirectiveType string + + // ReferencePath is the path used in the directive (as written in the file) + ReferencePath string + + // LineNumber is the line number where the reference occurs + LineNumber int +} + +// ReferenceNode represents a node in the reference tree. +// +// This structure is used to build a hierarchical view of references, +// showing which files reference the target and which files reference those files. +type ReferenceNode struct { + // FilePath is the absolute path to this file + FilePath string + + // DirectiveType is the type of directive used to reference the file + DirectiveType string + + // ReferencePath is the path used in the directive + ReferencePath string + + // Children are files that include this file + // (for building nested reference trees) + Children []*ReferenceNode +} + +// GroupedFileReference represents a file with all its references to the target. +// +// This structure groups multiple references from the same file together, +// showing how many times a file references the target and where. +type GroupedFileReference struct { + // FilePath is the absolute path to the file + FilePath string + + // DirectiveType is the type of directive used + DirectiveType string + + // References is the list of all references from this file + References []FileReference + + // Count is the number of references from this file + Count int +} + diff --git a/audit-cli/commands/compare/file-contents/comparer.go b/audit-cli/commands/compare/file-contents/comparer.go index deabaa0..4f23eb4 100644 --- a/audit-cli/commands/compare/file-contents/comparer.go +++ b/audit-cli/commands/compare/file-contents/comparer.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" ) // CompareFiles performs a direct comparison between two files. @@ -160,7 +162,7 @@ func CompareVersions(referenceFile, productDir string, versions []string, genera // // Returns: // - FileComparison: The comparison result for this file -func compareFile(referencePath, referenceContent string, versionPath VersionPath, generateDiff bool, verbose bool) FileComparison { +func compareFile(referencePath, referenceContent string, versionPath pathresolver.VersionPath, generateDiff bool, verbose bool) FileComparison { comparison := FileComparison{ Version: versionPath.Version, FilePath: versionPath.FilePath, diff --git a/audit-cli/commands/compare/file-contents/version_resolver.go b/audit-cli/commands/compare/file-contents/version_resolver.go index 4f42d55..52f43a1 100644 --- a/audit-cli/commands/compare/file-contents/version_resolver.go +++ b/audit-cli/commands/compare/file-contents/version_resolver.go @@ -1,17 +1,9 @@ package file_contents import ( - "fmt" - "path/filepath" - "strings" + "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" ) -// VersionPath represents a resolved file path for a specific version. -type VersionPath struct { - Version string - FilePath string -} - // ResolveVersionPaths resolves file paths for all specified versions. // // Given a reference file path and a list of versions, this function constructs @@ -33,65 +25,10 @@ type VersionPath struct { // - versions: List of version identifiers // // Returns: -// - []VersionPath: List of resolved version paths +// - []pathresolver.VersionPath: List of resolved version paths // - error: Any error encountered during resolution -func ResolveVersionPaths(referenceFile string, productDir string, versions []string) ([]VersionPath, error) { - // Clean the paths - referenceFile = filepath.Clean(referenceFile) - productDir = filepath.Clean(productDir) - - // Ensure productDir ends with a separator for proper prefix matching - if !strings.HasSuffix(productDir, string(filepath.Separator)) { - productDir += string(filepath.Separator) - } - - // Check if referenceFile is under productDir - if !strings.HasPrefix(referenceFile, productDir) { - return nil, fmt.Errorf("reference file %s is not under product directory %s", referenceFile, productDir) - } - - // Extract the relative path from productDir - relativePath := strings.TrimPrefix(referenceFile, productDir) - - // Find the version segment and the path after it - // Expected format: {version}/source/{rest-of-path} - parts := strings.Split(relativePath, string(filepath.Separator)) - if len(parts) < 2 { - return nil, fmt.Errorf("invalid file path structure: expected {version}/source/... format, got %s", relativePath) - } - - // Find the "source" directory - sourceIndex := -1 - for i, part := range parts { - if part == "source" { - sourceIndex = i - break - } - } - - if sourceIndex == -1 { - return nil, fmt.Errorf("could not find 'source' directory in path: %s", relativePath) - } - - if sourceIndex == 0 { - return nil, fmt.Errorf("invalid path structure: 'source' cannot be the first segment in %s", relativePath) - } - - // The version is the segment before "source" - // Everything from "source" onwards is the path we want to preserve - pathFromSource := strings.Join(parts[sourceIndex:], string(filepath.Separator)) - - // Build version paths - var versionPaths []VersionPath - for _, version := range versions { - versionPath := filepath.Join(productDir, version, pathFromSource) - versionPaths = append(versionPaths, VersionPath{ - Version: version, - FilePath: versionPath, - }) - } - - return versionPaths, nil +func ResolveVersionPaths(referenceFile string, productDir string, versions []string) ([]pathresolver.VersionPath, error) { + return pathresolver.ResolveVersionPaths(referenceFile, productDir, versions) } // ExtractVersionFromPath extracts the version identifier from a file path. @@ -112,49 +49,6 @@ func ResolveVersionPaths(referenceFile string, productDir string, versions []str // - string: The version identifier // - error: Any error encountered during extraction func ExtractVersionFromPath(filePath string, productDir string) (string, error) { - // Clean the paths - filePath = filepath.Clean(filePath) - productDir = filepath.Clean(productDir) - - // Ensure productDir ends with a separator for proper prefix matching - if !strings.HasSuffix(productDir, string(filepath.Separator)) { - productDir += string(filepath.Separator) - } - - // Check if filePath is under productDir - if !strings.HasPrefix(filePath, productDir) { - return "", fmt.Errorf("file path %s is not under product directory %s", filePath, productDir) - } - - // Extract the relative path from productDir - relativePath := strings.TrimPrefix(filePath, productDir) - - // Split into parts - parts := strings.Split(relativePath, string(filepath.Separator)) - if len(parts) < 2 { - return "", fmt.Errorf("invalid file path structure: expected {version}/source/... format, got %s", relativePath) - } - - // Find the "source" directory - sourceIndex := -1 - for i, part := range parts { - if part == "source" { - sourceIndex = i - break - } - } - - if sourceIndex == -1 { - return "", fmt.Errorf("could not find 'source' directory in path: %s", relativePath) - } - - if sourceIndex == 0 { - return "", fmt.Errorf("invalid path structure: 'source' cannot be the first segment in %s", relativePath) - } - - // The version is the segment before "source" - version := parts[sourceIndex-1] - - return version, nil + return pathresolver.ExtractVersionFromPath(filePath, productDir) } diff --git a/audit-cli/internal/pathresolver/pathresolver.go b/audit-cli/internal/pathresolver/pathresolver.go new file mode 100644 index 0000000..7294b4a --- /dev/null +++ b/audit-cli/internal/pathresolver/pathresolver.go @@ -0,0 +1,119 @@ +package pathresolver + +import ( + "fmt" + "path/filepath" +) + +// DetectProjectInfo analyzes a file path and determines the project structure. +// +// This function detects whether the file is part of a versioned or non-versioned +// project and extracts relevant information about the project structure. +// +// Versioned project structure: +// {product}/{version}/source/... +// Example: /path/to/manual/v8.0/source/includes/file.rst +// +// Non-versioned project structure: +// {product}/source/... +// Example: /path/to/atlas/source/includes/file.rst +// +// Parameters: +// - filePath: Path to a file within the documentation tree +// +// Returns: +// - *ProjectInfo: Information about the project structure +// - error: Any error encountered during detection +func DetectProjectInfo(filePath string) (*ProjectInfo, error) { + // Get absolute path + absPath, err := filepath.Abs(filePath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Find the source directory + sourceDir, err := FindSourceDirectory(absPath) + if err != nil { + return nil, err + } + + // Get the parent directory of source (could be version or product) + parent := filepath.Dir(sourceDir) + parentName := filepath.Base(parent) + + // Check if this is a versioned project + isVersioned, err := IsVersionedProject(sourceDir) + if err != nil { + return nil, err + } + + var productDir string + var version string + + if isVersioned { + // Versioned project: parent is the version directory + version = parentName + productDir = filepath.Dir(parent) + } else { + // Non-versioned project: parent is the product directory + version = "" + productDir = parent + } + + return &ProjectInfo{ + SourceDir: sourceDir, + ProductDir: productDir, + Version: version, + IsVersioned: isVersioned, + }, nil +} + +// ResolveRelativeToSource resolves a path relative to the source directory. +// +// This function takes a relative path (like "/includes/file.rst") and resolves +// it to an absolute path based on the source directory. +// +// Parameters: +// - sourceDir: The absolute path to the source directory +// - relativePath: The relative path to resolve (can start with / or not) +// +// Returns: +// - string: The absolute path +// - error: Any error encountered during resolution +func ResolveRelativeToSource(sourceDir, relativePath string) (string, error) { + // Clean the paths + sourceDir = filepath.Clean(sourceDir) + relativePath = filepath.Clean(relativePath) + + // Remove leading slash if present (it's relative to source, not filesystem root) + if len(relativePath) > 0 && relativePath[0] == '/' { + relativePath = relativePath[1:] + } + + // Join with source directory + fullPath := filepath.Join(sourceDir, relativePath) + + return fullPath, nil +} + +// FindProductDirectory walks up the directory tree to find the product root directory. +// +// The product directory is the parent of either: +// - The version directory (for versioned projects) +// - The source directory (for non-versioned projects) +// +// Parameters: +// - filePath: Path to a file within the documentation tree +// +// Returns: +// - string: Absolute path to the product directory +// - error: Error if product directory cannot be found +func FindProductDirectory(filePath string) (string, error) { + projectInfo, err := DetectProjectInfo(filePath) + if err != nil { + return "", err + } + + return projectInfo.ProductDir, nil +} + diff --git a/audit-cli/internal/pathresolver/pathresolver_test.go b/audit-cli/internal/pathresolver/pathresolver_test.go new file mode 100644 index 0000000..6766d97 --- /dev/null +++ b/audit-cli/internal/pathresolver/pathresolver_test.go @@ -0,0 +1,197 @@ +package pathresolver + +import ( + "path/filepath" + "testing" +) + +func TestFindSourceDirectory(t *testing.T) { + tests := []struct { + name string + filePath string + wantContains string + wantErr bool + }{ + { + name: "versioned project file", + filePath: "../../testdata/compare/product/v8.0/source/includes/example.rst", + wantContains: "testdata/compare/product/v8.0/source", + wantErr: false, + }, + { + name: "non-versioned project file", + filePath: "../../testdata/compare/product/manual/source/includes/example.rst", + wantContains: "testdata/compare/product/manual/source", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FindSourceDirectory(tt.filePath) + if (err != nil) != tt.wantErr { + t.Errorf("FindSourceDirectory() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + // Check that the path contains the expected substring + if !filepath.IsAbs(got) { + t.Errorf("FindSourceDirectory() returned relative path: %v", got) + } + if !filepath.HasPrefix(got, "/") { + t.Errorf("FindSourceDirectory() returned non-absolute path: %v", got) + } + // Check that it ends with the expected path + if !filepath.HasPrefix(got, "/") || !filepath.HasPrefix(filepath.Clean(got), "/") { + t.Errorf("FindSourceDirectory() = %v, should be absolute", got) + } + } + }) + } +} + +func TestDetectProjectInfo(t *testing.T) { + tests := []struct { + name string + filePath string + wantVersion string + wantVersioned bool + wantErr bool + }{ + { + name: "versioned project v8.0", + filePath: "../../testdata/compare/product/v8.0/source/includes/example.rst", + wantVersion: "v8.0", + wantVersioned: true, + wantErr: false, + }, + { + name: "versioned project manual", + filePath: "../../testdata/compare/product/manual/source/includes/example.rst", + wantVersion: "manual", + wantVersioned: true, + wantErr: false, + }, + { + name: "versioned project upcoming", + filePath: "../../testdata/compare/product/upcoming/source/includes/example.rst", + wantVersion: "upcoming", + wantVersioned: true, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DetectProjectInfo(tt.filePath) + if (err != nil) != tt.wantErr { + t.Errorf("DetectProjectInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if got.Version != tt.wantVersion { + t.Errorf("DetectProjectInfo() Version = %v, want %v", got.Version, tt.wantVersion) + } + if got.IsVersioned != tt.wantVersioned { + t.Errorf("DetectProjectInfo() IsVersioned = %v, want %v", got.IsVersioned, tt.wantVersioned) + } + if got.SourceDir == "" { + t.Errorf("DetectProjectInfo() SourceDir is empty") + } + if got.ProductDir == "" { + t.Errorf("DetectProjectInfo() ProductDir is empty") + } + } + }) + } +} + +func TestResolveVersionPaths(t *testing.T) { + // Get absolute path to test data + testFile := "../../testdata/compare/product/v8.0/source/includes/example.rst" + absTestFile, _ := filepath.Abs(testFile) + + // Get product directory (parent of v8.0) + sourceDir := filepath.Dir(absTestFile) // .../includes + sourceDir = filepath.Dir(sourceDir) // .../source + versionDir := filepath.Dir(sourceDir) // .../v8.0 + productDir := filepath.Dir(versionDir) // .../product + + versions := []string{"v8.0", "manual", "upcoming"} + + got, err := ResolveVersionPaths(absTestFile, productDir, versions) + if err != nil { + t.Fatalf("ResolveVersionPaths() error = %v", err) + } + + if len(got) != 3 { + t.Errorf("ResolveVersionPaths() returned %d paths, want 3", len(got)) + } + + // Check that each version path is constructed correctly + for i, vp := range got { + if vp.Version != versions[i] { + t.Errorf("VersionPath[%d].Version = %v, want %v", i, vp.Version, versions[i]) + } + expectedPath := filepath.Join(productDir, versions[i], "source", "includes", "example.rst") + if vp.FilePath != expectedPath { + t.Errorf("VersionPath[%d].FilePath = %v, want %v", i, vp.FilePath, expectedPath) + } + } +} + +func TestExtractVersionFromPath(t *testing.T) { + testFile := "../../testdata/compare/product/v8.0/source/includes/example.rst" + absTestFile, _ := filepath.Abs(testFile) + + // Get product directory (parent of v8.0) + sourceDir := filepath.Dir(absTestFile) // .../includes + sourceDir = filepath.Dir(sourceDir) // .../source + versionDir := filepath.Dir(sourceDir) // .../v8.0 + productDir := filepath.Dir(versionDir) // .../product + + got, err := ExtractVersionFromPath(absTestFile, productDir) + if err != nil { + t.Fatalf("ExtractVersionFromPath() error = %v", err) + } + + want := "v8.0" + if got != want { + t.Errorf("ExtractVersionFromPath() = %v, want %v", got, want) + } +} + +func TestResolveRelativeToSource(t *testing.T) { + sourceDir := "/path/to/manual/v8.0/source" + + tests := []struct { + name string + relativePath string + want string + }{ + { + name: "path with leading slash", + relativePath: "/includes/file.rst", + want: "/path/to/manual/v8.0/source/includes/file.rst", + }, + { + name: "path without leading slash", + relativePath: "includes/file.rst", + want: "/path/to/manual/v8.0/source/includes/file.rst", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveRelativeToSource(sourceDir, tt.relativePath) + if err != nil { + t.Errorf("ResolveRelativeToSource() error = %v", err) + return + } + if got != tt.want { + t.Errorf("ResolveRelativeToSource() = %v, want %v", got, tt.want) + } + }) + } +} + diff --git a/audit-cli/internal/pathresolver/source_finder.go b/audit-cli/internal/pathresolver/source_finder.go new file mode 100644 index 0000000..77395f6 --- /dev/null +++ b/audit-cli/internal/pathresolver/source_finder.go @@ -0,0 +1,55 @@ +package pathresolver + +import ( + "fmt" + "os" + "path/filepath" +) + +// FindSourceDirectory walks up the directory tree to find the "source" directory. +// +// MongoDB documentation is typically organized with a "source" directory at the root. +// This function walks up from the current file to find that directory, which is used +// as the base for resolving include paths. +// +// Parameters: +// - filePath: Path to a file within the documentation tree +// +// Returns: +// - string: Absolute path to the source directory +// - error: Error if source directory cannot be found +func FindSourceDirectory(filePath string) (string, error) { + // Get absolute path first + absPath, err := filepath.Abs(filePath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // Get the directory containing the file + dir := filepath.Dir(absPath) + + // Walk up the directory tree + for { + // Check if the current directory is named "source" + if filepath.Base(dir) == "source" { + return dir, nil + } + + // Check if there's a "source" subdirectory + sourceSubdir := filepath.Join(dir, "source") + if info, err := os.Stat(sourceSubdir); err == nil && info.IsDir() { + return sourceSubdir, nil + } + + // Move up one directory + parent := filepath.Dir(dir) + + // If we've reached the root, stop + if parent == dir { + return "", fmt.Errorf("could not find source directory for %s", filePath) + } + + dir = parent + } +} + diff --git a/audit-cli/internal/pathresolver/types.go b/audit-cli/internal/pathresolver/types.go new file mode 100644 index 0000000..f6b071c --- /dev/null +++ b/audit-cli/internal/pathresolver/types.go @@ -0,0 +1,33 @@ +package pathresolver + +// ProjectInfo contains information about a documentation project's structure. +// +// MongoDB documentation projects can be either versioned or non-versioned: +// - Versioned: {product}/{version}/source/... (e.g., manual/v8.0/source/...) +// - Non-versioned: {product}/source/... (e.g., atlas/source/...) +type ProjectInfo struct { + // SourceDir is the absolute path to the source directory + SourceDir string + + // ProductDir is the absolute path to the product directory + ProductDir string + + // Version is the version identifier (e.g., "v8.0", "manual", "upcoming") + // Empty string for non-versioned projects + Version string + + // IsVersioned indicates whether this is a versioned project + IsVersioned bool +} + +// VersionPath represents a resolved file path for a specific version. +// +// Used when resolving the same file across multiple versions of a product. +type VersionPath struct { + // Version is the version identifier (e.g., "v8.0", "manual", "upcoming") + Version string + + // FilePath is the absolute path to the file in this version + FilePath string +} + diff --git a/audit-cli/internal/pathresolver/version_resolver.go b/audit-cli/internal/pathresolver/version_resolver.go new file mode 100644 index 0000000..f7c6344 --- /dev/null +++ b/audit-cli/internal/pathresolver/version_resolver.go @@ -0,0 +1,196 @@ +package pathresolver + +import ( + "fmt" + "path/filepath" + "strings" +) + +// ResolveVersionPaths resolves file paths for all specified versions. +// +// Given a reference file path and a list of versions, this function constructs +// the corresponding file paths for each version by replacing the version segment +// in the path. +// +// Example: +// Input: /path/to/manual/manual/source/includes/file.rst +// Versions: [manual, upcoming, v8.1, v8.0] +// Output: +// - manual: /path/to/manual/manual/source/includes/file.rst +// - upcoming: /path/to/manual/upcoming/source/includes/file.rst +// - v8.1: /path/to/manual/v8.1/source/includes/file.rst +// - v8.0: /path/to/manual/v8.0/source/includes/file.rst +// +// Parameters: +// - referenceFile: The absolute path to the reference file +// - productDir: The absolute path to the product directory (e.g., /path/to/manual) +// - versions: List of version identifiers +// +// Returns: +// - []VersionPath: List of resolved version paths +// - error: Any error encountered during resolution +func ResolveVersionPaths(referenceFile string, productDir string, versions []string) ([]VersionPath, error) { + // Clean the paths + referenceFile = filepath.Clean(referenceFile) + productDir = filepath.Clean(productDir) + + // Ensure productDir ends with a separator for proper prefix matching + if !strings.HasSuffix(productDir, string(filepath.Separator)) { + productDir += string(filepath.Separator) + } + + // Check if referenceFile is under productDir + if !strings.HasPrefix(referenceFile, productDir) { + return nil, fmt.Errorf("reference file %s is not under product directory %s", referenceFile, productDir) + } + + // Extract the relative path from productDir + relativePath := strings.TrimPrefix(referenceFile, productDir) + + // Find the version segment and the path after it + // Expected format: {version}/source/{rest-of-path} + parts := strings.Split(relativePath, string(filepath.Separator)) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid file path structure: expected {version}/source/... format, got %s", relativePath) + } + + // Find the "source" directory + sourceIndex := -1 + for i, part := range parts { + if part == "source" { + sourceIndex = i + break + } + } + + if sourceIndex == -1 { + return nil, fmt.Errorf("could not find 'source' directory in path: %s", relativePath) + } + + if sourceIndex == 0 { + return nil, fmt.Errorf("invalid path structure: 'source' cannot be the first segment in %s", relativePath) + } + + // The version is the segment before "source" + // Everything from "source" onwards is the path we want to preserve + pathFromSource := strings.Join(parts[sourceIndex:], string(filepath.Separator)) + + // Build version paths + var versionPaths []VersionPath + for _, version := range versions { + versionPath := filepath.Join(productDir, version, pathFromSource) + versionPaths = append(versionPaths, VersionPath{ + Version: version, + FilePath: versionPath, + }) + } + + return versionPaths, nil +} + +// ExtractVersionFromPath extracts the version identifier from a file path. +// +// Given a file path within a versioned project, this function extracts the +// version segment (the directory before "source"). +// +// Example: +// Input: /path/to/manual/v8.0/source/includes/file.rst +// Output: "v8.0" +// +// Parameters: +// - filePath: The absolute path to a file +// - productDir: The absolute path to the product directory +// +// Returns: +// - string: The version identifier +// - error: Any error encountered during extraction +func ExtractVersionFromPath(filePath string, productDir string) (string, error) { + // Clean the paths + filePath = filepath.Clean(filePath) + productDir = filepath.Clean(productDir) + + // Ensure productDir ends with a separator for proper prefix matching + if !strings.HasSuffix(productDir, string(filepath.Separator)) { + productDir += string(filepath.Separator) + } + + // Check if filePath is under productDir + if !strings.HasPrefix(filePath, productDir) { + return "", fmt.Errorf("file path %s is not under product directory %s", filePath, productDir) + } + + // Extract the relative path from productDir + relativePath := strings.TrimPrefix(filePath, productDir) + + // Split into parts + parts := strings.Split(relativePath, string(filepath.Separator)) + if len(parts) < 2 { + return "", fmt.Errorf("invalid file path structure: expected {version}/source/... format, got %s", relativePath) + } + + // Find the "source" directory + sourceIndex := -1 + for i, part := range parts { + if part == "source" { + sourceIndex = i + break + } + } + + if sourceIndex == -1 { + return "", fmt.Errorf("could not find 'source' directory in path: %s", relativePath) + } + + if sourceIndex == 0 { + return "", fmt.Errorf("invalid path structure: 'source' cannot be the first segment in %s", relativePath) + } + + // The version is the segment before "source" + version := parts[sourceIndex-1] + + return version, nil +} + +// IsVersionedProject determines if a path is part of a versioned project. +// +// A versioned project has the structure: {product}/{version}/source/... +// A non-versioned project has the structure: {product}/source/... +// +// This function checks if there's a directory between the product root and "source". +// +// Parameters: +// - sourceDir: The absolute path to the source directory +// +// Returns: +// - bool: True if this is a versioned project +// - error: Any error encountered during detection +func IsVersionedProject(sourceDir string) (bool, error) { + // Get the parent directory of source + parent := filepath.Dir(sourceDir) + + // Check if the parent directory name looks like a version + // Common patterns: v8.0, v7.0, manual, upcoming, master, current + parentName := filepath.Base(parent) + + // If parent is named after common version patterns, it's versioned + // This is a heuristic - we check if there's a directory between product root and source + grandparent := filepath.Dir(parent) + + // If grandparent is the root or doesn't exist, it's likely non-versioned + if grandparent == parent || grandparent == "/" || grandparent == "." { + return false, nil + } + + // Check if there's another "source" directory at the grandparent level + // If not, then parent is likely a version directory + grandparentSource := filepath.Join(grandparent, "source") + if grandparentSource == sourceDir { + // This means parent is not a version directory + return false, nil + } + + // If we have: grandparent/parent/source, then parent is likely a version + // and this is a versioned project + return parentName != "", nil +} + diff --git a/audit-cli/internal/rst/include_resolver.go b/audit-cli/internal/rst/include_resolver.go index af437ec..575bead 100644 --- a/audit-cli/internal/rst/include_resolver.go +++ b/audit-cli/internal/rst/include_resolver.go @@ -7,6 +7,8 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" ) // IncludeDirectiveRegex matches .. include:: directives in RST files. @@ -84,7 +86,7 @@ func ResolveIncludePath(currentFilePath, includePath string) (string, error) { } // Find the source directory by walking up from the current file - sourceDir, err := FindSourceDirectory(currentFilePath) + sourceDir, err := pathresolver.FindSourceDirectory(currentFilePath) if err != nil { return "", err } @@ -317,44 +319,5 @@ func ResolveTemplateVariable(yamlFilePath, varName string) (string, error) { return "", fmt.Errorf("template variable %s not found in replacement section of %s", varName, yamlFilePath) } -// FindSourceDirectory walks up the directory tree to find the "source" directory. -// -// MongoDB documentation is typically organized with a "source" directory at the root. -// This function walks up from the current file to find that directory, which is used -// as the base for resolving include paths. -// -// Parameters: -// - filePath: Path to a file within the documentation tree -// -// Returns: -// - string: Absolute path to the source directory -// - error: Error if source directory cannot be found -func FindSourceDirectory(filePath string) (string, error) { - // Get the directory containing the file - dir := filepath.Dir(filePath) - - // Walk up the directory tree - for { - // Check if the current directory is named "source" - if filepath.Base(dir) == "source" { - return dir, nil - } - - // Check if there's a "source" subdirectory - sourceSubdir := filepath.Join(dir, "source") - if info, err := os.Stat(sourceSubdir); err == nil && info.IsDir() { - return sourceSubdir, nil - } - - // Move up one directory - parent := filepath.Dir(dir) - // If we've reached the root, stop - if parent == dir { - return "", fmt.Errorf("could not find source directory for %s", filePath) - } - - dir = parent - } -} From 8422c462e0809d6bf78556a97dbfa2959bf0451e Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 10:56:24 -0400 Subject: [PATCH 02/14] apply Dachary feedback and update to support yaml and shared toctree functionality --- audit-cli/README.md | 41 +++--- audit-cli/commands/analyze/analyze.go | 8 +- .../analyzer.go | 78 ++++++++++-- .../file_references.go} | 58 +++++---- .../file_references_test.go} | 13 +- .../{references => file-references}/output.go | 2 +- .../{references => file-references}/types.go | 4 +- .../commands/analyze/includes/analyzer.go | 22 +++- .../commands/analyze/includes/includes.go | 28 +++-- .../code-examples/code_examples_test.go | 4 +- audit-cli/internal/rst/include_resolver.go | 117 ++++++++++++++++++ .../source/includes/extracts-test.yaml | 17 +++ .../testdata/input-files/source/index.rst | 13 ++ 13 files changed, 322 insertions(+), 83 deletions(-) rename audit-cli/commands/analyze/{references => file-references}/analyzer.go (77%) rename audit-cli/commands/analyze/{references/references.go => file-references/file_references.go} (70%) rename audit-cli/commands/analyze/{references/references_test.go => file-references/file_references_test.go} (96%) rename audit-cli/commands/analyze/{references => file-references}/output.go (99%) rename audit-cli/commands/analyze/{references => file-references}/types.go (98%) create mode 100644 audit-cli/testdata/input-files/source/includes/extracts-test.yaml create mode 100644 audit-cli/testdata/input-files/source/index.rst diff --git a/audit-cli/README.md b/audit-cli/README.md index f8789e8..f16c19f 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -223,7 +223,9 @@ With `-v` flag, also shows: #### `analyze includes` -Analyze `include` directive relationships in RST files to understand file dependencies. +Analyze `include` directive and `toctree` relationships in RST files to understand file dependencies. + +This command recursively follows both `.. include::` directives and `.. toctree::` entries to show all files that are referenced from a starting file. This provides a complete picture of file dependencies including both content includes and table of contents structure. **Use Cases:** @@ -232,6 +234,7 @@ This command helps writers: - Identify circular include dependencies (files included multiple times) - Document file relationships for maintenance - Plan refactoring of complex include structures +- Understand table of contents structure and page hierarchies **Basic Usage:** @@ -283,11 +286,11 @@ times (e.g., file A includes file C, and file B also includes file C), the file However, the tree view will show it in all locations where it appears, with subsequent occurrences marked as circular includes in verbose mode. -#### `analyze references` +#### `analyze file-references` -Find all files that reference a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, or `io-code-block` directives. +Find all files that reference a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, `io-code-block`, or `toctree` directives. -The command searches all RST files (both `.rst` and `.txt` extensions) in the source directory tree. +The command searches all RST files (`.rst` and `.txt` extensions) and YAML files (`.yaml` and `.yml` extensions) in the source directory tree. YAML files are included because extract and release files contain RST directives within their content blocks. **Use Cases:** @@ -295,23 +298,23 @@ This command helps writers: - Understand the impact of changes to a file (what pages will be affected) - Find all usages of an include file across the documentation - Track where code examples are referenced -- Identify orphaned files (files with no references) +- Identify orphaned files (files with no references, including toctree entries) - Plan refactoring by understanding file dependencies **Basic Usage:** ```bash # Find what references an include file -./audit-cli analyze references path/to/includes/fact.rst +./audit-cli analyze file-references path/to/includes/fact.rst # Find what references a code example -./audit-cli analyze references path/to/code-examples/example.js +./audit-cli analyze file-references path/to/code-examples/example.js # Get JSON output for automation -./audit-cli analyze references path/to/file.rst --format json +./audit-cli analyze file-references path/to/file.rst --format json # Show detailed information with line numbers -./audit-cli analyze references path/to/file.rst --verbose +./audit-cli analyze file-references path/to/file.rst --verbose ``` **Flags:** @@ -438,39 +441,39 @@ include : 3 files, 4 references ```bash # Check if an include file is being used -./audit-cli analyze references ~/docs/source/includes/fact-atlas.rst +./audit-cli analyze file-references ~/docs/source/includes/fact-atlas.rst # Find all pages that use a specific code example -./audit-cli analyze references ~/docs/source/code-examples/connect.py +./audit-cli analyze file-references ~/docs/source/code-examples/connect.py # Get machine-readable output for scripting -./audit-cli analyze references ~/docs/source/includes/fact.rst --format json | jq '.total_references' +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --format json | jq '.total_references' # See exactly where a file is referenced (with line numbers) -./audit-cli analyze references ~/docs/source/includes/intro.rst --verbose +./audit-cli analyze file-references ~/docs/source/includes/intro.rst --verbose # Quick check: just show the count -./audit-cli analyze references ~/docs/source/includes/fact.rst --count-only +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --count-only # Output: 5 # Get list of files for piping to other commands -./audit-cli analyze references ~/docs/source/includes/fact.rst --paths-only +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --paths-only # Output: # page1.rst # page2.rst # page3.rst # Filter to only show include directives (not literalinclude or io-code-block) -./audit-cli analyze references ~/docs/source/includes/fact.rst --directive-type include +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --directive-type include # Filter to only show literalinclude references -./audit-cli analyze references ~/docs/source/code-examples/example.py --directive-type literalinclude +./audit-cli analyze file-references ~/docs/source/code-examples/example.py --directive-type literalinclude # Combine filters: count only literalinclude references -./audit-cli analyze references ~/docs/source/code-examples/example.py -t literalinclude -c +./audit-cli analyze file-references ~/docs/source/code-examples/example.py -t literalinclude -c # Combine filters: list files that use this as an io-code-block -./audit-cli analyze references ~/docs/source/code-examples/query.js -t io-code-block --paths-only +./audit-cli analyze file-references ~/docs/source/code-examples/query.js -t io-code-block --paths-only ``` ### Compare Commands diff --git a/audit-cli/commands/analyze/analyze.go b/audit-cli/commands/analyze/analyze.go index c60aa34..4d7d63e 100644 --- a/audit-cli/commands/analyze/analyze.go +++ b/audit-cli/commands/analyze/analyze.go @@ -3,14 +3,14 @@ // This package serves as the parent command for various analysis operations. // Currently supports: // - includes: Analyze include directive relationships in RST files -// - references: Find all files that reference a target file +// - file-references: Find all files that reference a target file // // Future subcommands could include analyzing cross-references, broken links, or content metrics. package analyze import ( "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/includes" - "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/references" + filereferences "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/file-references" "github.com/spf13/cobra" ) @@ -26,14 +26,14 @@ func NewAnalyzeCommand() *cobra.Command { Currently supports: - includes: Analyze include directive relationships (forward dependencies) - - references: Find all files that reference a target file (reverse dependencies) + - file-references: Find all files that reference a target file (reverse dependencies) Future subcommands may support analyzing cross-references, broken links, or content metrics.`, } // Add subcommands cmd.AddCommand(includes.NewIncludesCommand()) - cmd.AddCommand(references.NewReferencesCommand()) + cmd.AddCommand(filereferences.NewFileReferencesCommand()) return cmd } diff --git a/audit-cli/commands/analyze/references/analyzer.go b/audit-cli/commands/analyze/file-references/analyzer.go similarity index 77% rename from audit-cli/commands/analyze/references/analyzer.go rename to audit-cli/commands/analyze/file-references/analyzer.go index 5894945..796fc91 100644 --- a/audit-cli/commands/analyze/references/analyzer.go +++ b/audit-cli/commands/analyze/file-references/analyzer.go @@ -1,4 +1,4 @@ -package references +package filereferences import ( "bufio" @@ -10,6 +10,7 @@ import ( "strings" "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" + "github.com/mongodb/code-example-tooling/audit-cli/internal/rst" ) // Regular expressions for matching directives @@ -32,9 +33,10 @@ var ( // AnalyzeReferences finds all files that reference the target file. // -// This function searches through all RST files in the source directory to find -// files that reference the target file using include, literalinclude, or -// io-code-block directives. +// This function searches through all RST files (.rst, .txt) and YAML files (.yaml, .yml) +// in the source directory to find files that reference the target file using include, +// literalinclude, or io-code-block directives. YAML files are included because extract +// and release files contain RST directives within their content blocks. // // Parameters: // - targetFile: Absolute path to the file to analyze @@ -62,7 +64,7 @@ func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { ReferencingFiles: []FileReference{}, } - // Walk through all RST files in the source directory + // Walk through all RST and YAML files in the source directory err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -73,9 +75,10 @@ func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { return nil } - // Only process RST files (.rst and .txt extensions) + // Only process RST files (.rst, .txt) and YAML files (.yaml, .yml) + // YAML files may contain RST directives in extract/release content blocks ext := filepath.Ext(path) - if ext != ".rst" && ext != ".txt" { + if ext != ".rst" && ext != ".txt" && ext != ".yaml" && ext != ".yml" { return nil } @@ -107,7 +110,7 @@ func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { // findReferencesInFile searches a single file for references to the target file. // // This function scans through the file line by line looking for include, -// literalinclude, and io-code-block directives that reference the target file. +// literalinclude, io-code-block, and toctree directives that reference the target file. // // Parameters: // - filePath: Path to the file to search @@ -129,12 +132,21 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen lineNum := 0 inIOCodeBlock := false ioCodeBlockStartLine := 0 + inToctree := false + toctreeStartLine := 0 for scanner.Scan() { lineNum++ line := scanner.Text() trimmedLine := strings.TrimSpace(line) + // Check for toctree start (use shared regex from rst package) + if rst.ToctreeDirectiveRegex.MatchString(trimmedLine) { + inToctree = true + toctreeStartLine = lineNum + continue + } + // Check for io-code-block start if ioCodeBlockRegex.MatchString(trimmedLine) { inIOCodeBlock = true @@ -142,6 +154,11 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen continue } + // Check if we're exiting toctree (unindented line that's not empty and not an option) + if inToctree && len(line) > 0 && line[0] != ' ' && line[0] != '\t' { + inToctree = false + } + // Check if we're exiting io-code-block (unindented line that's not empty) if inIOCodeBlock && len(line) > 0 && line[0] != ' ' && line[0] != '\t' { inIOCodeBlock = false @@ -205,6 +222,26 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen continue } } + + // Check for toctree entries (indented document names) + if inToctree { + // Skip empty lines and option lines (starting with :) + if trimmedLine == "" || strings.HasPrefix(trimmedLine, ":") { + continue + } + + // This is a document name in the toctree + // Document names can be relative or absolute (starting with /) + docName := trimmedLine + if referencesToctreeTarget(docName, targetFile, sourceDir, filePath) { + references = append(references, FileReference{ + FilePath: filePath, + DirectiveType: "toctree", + ReferencePath: docName, + LineNumber: toctreeStartLine, + }) + } + } } if err := scanner.Err(); err != nil { @@ -250,6 +287,31 @@ func referencesTarget(refPath, targetFile, sourceDir, currentFile string) bool { return absResolvedPath == targetFile } +// referencesToctreeTarget checks if a toctree document name points to the target file. +// +// This function uses the shared rst.ResolveToctreePath to resolve the document name +// and then compares it to the target file. +// +// Parameters: +// - docName: The document name from the toctree (e.g., "intro" or "/includes/intro") +// - targetFile: Absolute path to the target file +// - sourceDir: Source directory (for resolving relative paths) +// - currentFile: Path to the file containing the toctree +// +// Returns: +// - bool: true if the document name points to the target file +func referencesToctreeTarget(docName, targetFile, sourceDir, currentFile string) bool { + // Use the shared toctree path resolution from rst package + resolvedPath, err := rst.ResolveToctreePath(currentFile, docName) + if err != nil { + // If we can't resolve it, it doesn't match + return false + } + + // Compare with target file + return resolvedPath == targetFile +} + // FilterByDirectiveType filters the analysis results to only include references // of the specified directive type. // diff --git a/audit-cli/commands/analyze/references/references.go b/audit-cli/commands/analyze/file-references/file_references.go similarity index 70% rename from audit-cli/commands/analyze/references/references.go rename to audit-cli/commands/analyze/file-references/file_references.go index 21fb15a..b8f90ed 100644 --- a/audit-cli/commands/analyze/references/references.go +++ b/audit-cli/commands/analyze/file-references/file_references.go @@ -1,15 +1,18 @@ -// Package references provides functionality for analyzing which files reference a target file. +// Package filereferences provides functionality for analyzing which files reference a target file. // -// This package implements the "analyze references" subcommand, which finds all files -// that reference a given file through RST directives (include, literalinclude, io-code-block). +// This package implements the "analyze file-references" subcommand, which finds all files +// that reference a given file through RST directives (include, literalinclude, io-code-block, toctree). +// +// The command searches both RST files (.rst, .txt) and YAML files (.yaml, .yml) since +// extract and release YAML files contain RST directives within their content blocks. // // The command performs reverse dependency analysis, showing which files depend on the // target file. This is useful for: // - Understanding the impact of changes to a file // - Finding all usages of an include file // - Tracking code example references -// - Identifying orphaned files (files with no references) -package references +// - Identifying orphaned files (files with no references, including toctree entries) +package filereferences import ( "fmt" @@ -17,22 +20,22 @@ import ( "github.com/spf13/cobra" ) -// NewReferencesCommand creates the references subcommand. +// NewFileReferencesCommand creates the file-references subcommand. // // This command analyzes which files reference a given target file through -// RST directives (include, literalinclude, io-code-block). +// RST directives (include, literalinclude, io-code-block, toctree). // // Usage: -// analyze references /path/to/file.rst -// analyze references /path/to/code-example.js +// analyze file-references /path/to/file.rst +// analyze file-references /path/to/code-example.js // // Flags: // - --format: Output format (text or json) // - -v, --verbose: Show detailed information including line numbers // - -c, --count-only: Only show the count of references // - --paths-only: Only show the file paths -// - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block) -func NewReferencesCommand() *cobra.Command { +// - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block, toctree) +func NewFileReferencesCommand() *cobra.Command { var ( format string verbose bool @@ -42,46 +45,50 @@ func NewReferencesCommand() *cobra.Command { ) cmd := &cobra.Command{ - Use: "references [filepath]", + Use: "file-references [filepath]", Short: "Find all files that reference a target file", Long: `Find all files that reference a target file through RST directives. This command performs reverse dependency analysis, showing which files reference -the target file through include, literalinclude, or io-code-block directives. +the target file through include, literalinclude, io-code-block, or toctree directives. Supported directive types: - .. include:: RST content includes - .. literalinclude:: Code file references - .. io-code-block:: Input/output examples with file arguments + - .. toctree:: Table of contents entries + +The command searches all RST files (.rst, .txt) and YAML files (.yaml, .yml) in +the source directory tree. YAML files are included because extract and release +files contain RST directives within their content blocks. -The command searches all RST files in the source directory tree and identifies -files that reference the target file. This is useful for: +This is useful for: - Understanding the impact of changes to a file - Finding all usages of an include file - Tracking code example references - - Identifying orphaned files (files with no references) + - Identifying orphaned files (files with no references, including toctree entries) Examples: # Find what references an include file - analyze references /path/to/includes/fact.rst + analyze file-references /path/to/includes/fact.rst # Find what references a code example - analyze references /path/to/code-examples/example.js + analyze file-references /path/to/code-examples/example.js # Get JSON output - analyze references /path/to/file.rst --format json + analyze file-references /path/to/file.rst --format json # Show detailed information with line numbers - analyze references /path/to/file.rst --verbose + analyze file-references /path/to/file.rst --verbose # Just show the count - analyze references /path/to/file.rst --count-only + analyze file-references /path/to/file.rst --count-only # Just show the file paths - analyze references /path/to/file.rst --paths-only + analyze file-references /path/to/file.rst --paths-only # Filter by directive type - analyze references /path/to/file.rst --directive-type include`, + analyze file-references /path/to/file.rst --directive-type include`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runReferences(args[0], format, verbose, countOnly, pathsOnly, directiveType) @@ -92,7 +99,7 @@ Examples: cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed information including line numbers") cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of references") cmd.Flags().BoolVar(&pathsOnly, "paths-only", false, "Only show the file paths (one per line)") - cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block)") + cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block, toctree)") return cmd } @@ -118,9 +125,10 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool "include": true, "literalinclude": true, "io-code-block": true, + "toctree": true, } if !validTypes[directiveType] { - return fmt.Errorf("invalid directive type: %s (must be 'include', 'literalinclude', or 'io-code-block')", directiveType) + return fmt.Errorf("invalid directive type: %s (must be 'include', 'literalinclude', 'io-code-block', or 'toctree')", directiveType) } } diff --git a/audit-cli/commands/analyze/references/references_test.go b/audit-cli/commands/analyze/file-references/file_references_test.go similarity index 96% rename from audit-cli/commands/analyze/references/references_test.go rename to audit-cli/commands/analyze/file-references/file_references_test.go index b98dfda..a83b687 100644 --- a/audit-cli/commands/analyze/references/references_test.go +++ b/audit-cli/commands/analyze/file-references/file_references_test.go @@ -1,4 +1,4 @@ -package references +package filereferences import ( "path/filepath" @@ -19,13 +19,13 @@ func TestAnalyzeReferences(t *testing.T) { { name: "Include file with multiple references", targetFile: "includes/intro.rst", - expectedReferences: 4, + expectedReferences: 6, // 4 RST files + 1 YAML file + 1 toctree expectedDirectiveType: "include", }, { name: "Code example with literalinclude", targetFile: "code-examples/example.py", - expectedReferences: 1, + expectedReferences: 2, // 1 RST file + 1 YAML file expectedDirectiveType: "literalinclude", }, { @@ -134,6 +134,13 @@ func TestFindReferencesInFile(t *testing.T) { expectedReferences: 2, // Same file included twice expectedDirective: "include", }, + { + name: "Toctree directive", + searchFile: "index.rst", + targetFile: "include-test.rst", + expectedReferences: 1, + expectedDirective: "toctree", + }, { name: "No references", searchFile: "code-block-test.rst", diff --git a/audit-cli/commands/analyze/references/output.go b/audit-cli/commands/analyze/file-references/output.go similarity index 99% rename from audit-cli/commands/analyze/references/output.go rename to audit-cli/commands/analyze/file-references/output.go index 5785a8d..3a08d2d 100644 --- a/audit-cli/commands/analyze/references/output.go +++ b/audit-cli/commands/analyze/file-references/output.go @@ -1,4 +1,4 @@ -package references +package filereferences import ( "encoding/json" diff --git a/audit-cli/commands/analyze/references/types.go b/audit-cli/commands/analyze/file-references/types.go similarity index 98% rename from audit-cli/commands/analyze/references/types.go rename to audit-cli/commands/analyze/file-references/types.go index 638c8b4..ebba67e 100644 --- a/audit-cli/commands/analyze/references/types.go +++ b/audit-cli/commands/analyze/file-references/types.go @@ -1,4 +1,4 @@ -package references +package filereferences // ReferenceAnalysis contains the results of analyzing which files reference a target file. // @@ -33,7 +33,7 @@ type FileReference struct { FilePath string // DirectiveType is the type of directive used to reference the file - // Possible values: "include", "literalinclude", "io-code-block" + // Possible values: "include", "literalinclude", "io-code-block", "toctree" DirectiveType string // ReferencePath is the path used in the directive (as written in the file) diff --git a/audit-cli/commands/analyze/includes/analyzer.go b/audit-cli/commands/analyze/includes/analyzer.go index 52c1ff1..e94ef2a 100644 --- a/audit-cli/commands/analyze/includes/analyzer.go +++ b/audit-cli/commands/analyze/includes/analyzer.go @@ -103,19 +103,29 @@ func buildIncludeTree(filePath string, visited map[string]bool, verbose bool, de includeFiles, err := rst.FindIncludeDirectives(absPath) if err != nil { // Not a fatal error - file might not have includes - return node, nil + includeFiles = []string{} } - if verbose && len(includeFiles) > 0 { + // Find toctree entries in this file + toctreeFiles, err := rst.FindToctreeEntries(absPath) + if err != nil { + // Not a fatal error - file might not have toctree + toctreeFiles = []string{} + } + + // Combine include and toctree files + allFiles := append(includeFiles, toctreeFiles...) + + if verbose && len(allFiles) > 0 { indent := getIndent(depth) - fmt.Printf("%s📄 %s (%d includes)\n", indent, filepath.Base(absPath), len(includeFiles)) + fmt.Printf("%s📄 %s (%d includes, %d toctree entries)\n", indent, filepath.Base(absPath), len(includeFiles), len(toctreeFiles)) } - // Recursively process each included file - for _, includeFile := range includeFiles { + // Recursively process each included/toctree file + for _, includeFile := range allFiles { childNode, err := buildIncludeTree(includeFile, visited, verbose, depth+1) if err != nil { - fmt.Fprintf(os.Stderr, "Warning: failed to process include %s: %v\n", includeFile, err) + fmt.Fprintf(os.Stderr, "Warning: failed to process file %s: %v\n", includeFile, err) continue } node.Children = append(node.Children, childNode) diff --git a/audit-cli/commands/analyze/includes/includes.go b/audit-cli/commands/analyze/includes/includes.go index bb0d5f6..59dda39 100644 --- a/audit-cli/commands/analyze/includes/includes.go +++ b/audit-cli/commands/analyze/includes/includes.go @@ -1,11 +1,12 @@ -// Package includes provides functionality for analyzing include directive relationships. +// Package includes provides functionality for analyzing include and toctree relationships. // // This package implements the "analyze includes" subcommand, which analyzes RST files -// to understand their include directive relationships. It can display results as: -// - A hierarchical tree structure showing include relationships -// - A flat list of all files referenced through includes +// to understand their include directive and toctree relationships. It can display results as: +// - A hierarchical tree structure showing include and toctree relationships +// - A flat list of all files referenced through includes and toctree entries // -// This helps writers understand the impact of changes to files that are widely included. +// This helps writers understand the impact of changes to files that are widely included +// or referenced in table of contents. package includes import ( @@ -16,7 +17,7 @@ import ( // NewIncludesCommand creates the includes subcommand. // -// This command analyzes include directive relationships in RST files. +// This command analyzes include directive and toctree relationships in RST files. // Supports flags for different output formats (tree or list). // // Flags: @@ -32,16 +33,17 @@ func NewIncludesCommand() *cobra.Command { cmd := &cobra.Command{ Use: "includes [filepath]", - Short: "Analyze include directive relationships in RST files", - Long: `Analyze include directive relationships to understand file dependencies. + Short: "Analyze include and toctree relationships in RST files", + Long: `Analyze include directive and toctree relationships to understand file dependencies. -This command recursively follows .. include:: directives and shows all files -that are referenced. This helps writers understand the impact of changes to -files that are widely included across the documentation. +This command recursively follows .. include:: directives and .. toctree:: entries +and shows all files that are referenced. This helps writers understand the impact +of changes to files that are widely included or referenced in table of contents +across the documentation. Output formats: - --tree: Show hierarchical tree structure of includes - --list: Show flat list of all included files + --tree: Show hierarchical tree structure of includes and toctree entries + --list: Show flat list of all included and toctree files If neither flag is specified, shows a summary with basic statistics.`, Args: cobra.ExactArgs(1), diff --git a/audit-cli/commands/extract/code-examples/code_examples_test.go b/audit-cli/commands/extract/code-examples/code_examples_test.go index f1432de..4187c45 100644 --- a/audit-cli/commands/extract/code-examples/code_examples_test.go +++ b/audit-cli/commands/extract/code-examples/code_examples_test.go @@ -582,8 +582,8 @@ func TestNoFlagsOnDirectory(t *testing.T) { // Should NOT include files in includes/ subdirectory // Expected: code-block-test.rst, duplicate-include-test.rst, include-test.rst, // io-code-block-test.rst, literalinclude-test.rst, nested-code-block-test.rst, - // nested-include-test.rst (7 files) - expectedFiles := 7 + // nested-include-test.rst, index.rst (8 files) + expectedFiles := 8 if report.FilesTraversed != expectedFiles { t.Errorf("Expected %d files traversed (top-level only), got %d", expectedFiles, report.FilesTraversed) diff --git a/audit-cli/internal/rst/include_resolver.go b/audit-cli/internal/rst/include_resolver.go index 575bead..6d4d6f3 100644 --- a/audit-cli/internal/rst/include_resolver.go +++ b/audit-cli/internal/rst/include_resolver.go @@ -14,6 +14,9 @@ import ( // IncludeDirectiveRegex matches .. include:: directives in RST files. var IncludeDirectiveRegex = regexp.MustCompile(`^\.\.\s+include::\s+(.+)$`) +// ToctreeDirectiveRegex matches .. toctree:: directives in RST files. +var ToctreeDirectiveRegex = regexp.MustCompile(`^\.\.\s+toctree::`) + // FindIncludeDirectives finds all include directives in a file and resolves their paths. // // This function scans the file for .. include:: directives and resolves each path @@ -61,6 +64,120 @@ func FindIncludeDirectives(filePath string) ([]string, error) { return includePaths, nil } +// FindToctreeEntries finds all toctree entries in a file and resolves their paths. +// +// This function scans the file for .. toctree:: directives and extracts the document +// names listed in the toctree content. Document names are converted to file paths +// by trying common extensions (.rst, .txt). +// +// Parameters: +// - filePath: Path to the RST file to scan +// +// Returns: +// - []string: List of resolved absolute paths to toctree documents +// - error: Any error encountered during scanning +func FindToctreeEntries(filePath string) ([]string, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var toctreePaths []string + scanner := bufio.NewScanner(file) + inToctree := false + + for scanner.Scan() { + line := scanner.Text() + trimmedLine := strings.TrimSpace(line) + + // Check if this line starts a toctree directive + if ToctreeDirectiveRegex.MatchString(trimmedLine) { + inToctree = true + continue + } + + // Check if we're exiting toctree (unindented line that's not empty) + if inToctree && len(line) > 0 && line[0] != ' ' && line[0] != '\t' { + inToctree = false + } + + // If we're in a toctree, process document names + if inToctree { + // Skip empty lines and option lines (starting with :) + if trimmedLine == "" || strings.HasPrefix(trimmedLine, ":") { + continue + } + + // This is a document name in the toctree + docName := trimmedLine + + // Resolve the document name to a file path + resolvedPath, err := ResolveToctreePath(filePath, docName) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to resolve toctree entry %s: %v\n", docName, err) + continue + } + + toctreePaths = append(toctreePaths, resolvedPath) + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return toctreePaths, nil +} + +// ResolveToctreePath resolves a toctree document name to an absolute file path. +// +// Toctree entries are document names without extensions. This function tries to +// find the actual file by testing common extensions (.rst, .txt). +// +// Parameters: +// - currentFilePath: Path to the file containing the toctree +// - docName: Document name from the toctree (e.g., "intro" or "/includes/intro") +// +// Returns: +// - string: Resolved absolute path to the document file +// - error: Error if the document cannot be found +func ResolveToctreePath(currentFilePath, docName string) (string, error) { + // Find the source directory + sourceDir, err := pathresolver.FindSourceDirectory(currentFilePath) + if err != nil { + return "", err + } + + var basePath string + if strings.HasPrefix(docName, "/") { + // Absolute document name (relative to source directory) + basePath = filepath.Join(sourceDir, docName) + } else { + // Relative document name (relative to current file's directory) + currentDir := filepath.Dir(currentFilePath) + basePath = filepath.Join(currentDir, docName) + } + + // Clean the path + basePath = filepath.Clean(basePath) + + // Try common extensions + extensions := []string{".rst", ".txt", ""} + for _, ext := range extensions { + testPath := basePath + ext + if _, err := os.Stat(testPath); err == nil { + absPath, err := filepath.Abs(testPath) + if err != nil { + return "", err + } + return absPath, nil + } + } + + return "", fmt.Errorf("toctree document not found: %s (tried .rst, .txt, and no extension)", docName) +} + // ResolveIncludePath resolves an include path relative to the source directory // Handles multiple special cases: // - Template variables ({{var_name}}) diff --git a/audit-cli/testdata/input-files/source/includes/extracts-test.yaml b/audit-cli/testdata/input-files/source/includes/extracts-test.yaml new file mode 100644 index 0000000..90cf079 --- /dev/null +++ b/audit-cli/testdata/input-files/source/includes/extracts-test.yaml @@ -0,0 +1,17 @@ +--- +ref: test-extract-intro +content: | + This is a test extract that includes another file. + + .. include:: /includes/intro.rst + + And references a code example: + + .. literalinclude:: /code-examples/example.py + :language: python +--- +ref: test-extract-examples +content: | + This extract references the examples file. + + .. include:: /includes/examples.rst diff --git a/audit-cli/testdata/input-files/source/index.rst b/audit-cli/testdata/input-files/source/index.rst new file mode 100644 index 0000000..4f44bc9 --- /dev/null +++ b/audit-cli/testdata/input-files/source/index.rst @@ -0,0 +1,13 @@ +==================== +Documentation Index +==================== + +Welcome to the documentation! + +.. toctree:: + :maxdepth: 2 + + include-test + literalinclude-test + includes/intro + From a4761e083aa8f831ada299d1372fd0a7e9598a1a Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 11:13:31 -0400 Subject: [PATCH 03/14] docs(README): update for clarity re. orphaned files --- audit-cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audit-cli/README.md b/audit-cli/README.md index f16c19f..19b2158 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -298,7 +298,7 @@ This command helps writers: - Understand the impact of changes to a file (what pages will be affected) - Find all usages of an include file across the documentation - Track where code examples are referenced -- Identify orphaned files (files with no references, including toctree entries) +- Identify orphaned files (files with no references from include, literalinclude, io-code-block, or toctree directives) - Plan refactoring by understanding file dependencies **Basic Usage:** From dcef84e4a8dde7805a6ca72cd94cef4a89c57895 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 13:20:07 -0400 Subject: [PATCH 04/14] refactor(toctree matching): create new directive_regex.go and optional toctree flag --- audit-cli/README.md | 33 ++++++++++--- .../analyze/file-references/analyzer.go | 47 +++++++------------ .../file-references/file_references.go | 36 ++++++++------ .../file-references/file_references_test.go | 15 ++++-- audit-cli/internal/rst/directive_parser.go | 12 +++-- audit-cli/internal/rst/directive_regex.go | 33 +++++++++++++ audit-cli/internal/rst/include_resolver.go | 7 --- .../literalinclude-test.literalinclude.1.py | 4 ++ .../literalinclude-test.literalinclude.2.go | 7 +++ .../literalinclude-test.literalinclude.3.js | 5 ++ .../literalinclude-test.literalinclude.4.php | 6 +++ .../literalinclude-test.literalinclude.5.rb | 10 ++++ .../literalinclude-test.literalinclude.6.ts | 9 ++++ .../literalinclude-test.literalinclude.7.cpp | 8 ++++ 14 files changed, 167 insertions(+), 65 deletions(-) create mode 100644 audit-cli/internal/rst/directive_regex.go create mode 100644 audit-cli/output/literalinclude-test.literalinclude.1.py create mode 100644 audit-cli/output/literalinclude-test.literalinclude.2.go create mode 100644 audit-cli/output/literalinclude-test.literalinclude.3.js create mode 100644 audit-cli/output/literalinclude-test.literalinclude.4.php create mode 100644 audit-cli/output/literalinclude-test.literalinclude.5.rb create mode 100644 audit-cli/output/literalinclude-test.literalinclude.6.ts create mode 100644 audit-cli/output/literalinclude-test.literalinclude.7.cpp diff --git a/audit-cli/README.md b/audit-cli/README.md index 19b2158..78316b5 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -294,22 +294,29 @@ The command searches all RST files (`.rst` and `.txt` extensions) and YAML files **Use Cases:** +By default, this command searches for content inclusion directives (include, literalinclude, +io-code-block) that transclude content into pages. Use `--include-toctree` to also search +for toctree entries, which are navigation links rather than content transclusion. + This command helps writers: - Understand the impact of changes to a file (what pages will be affected) - Find all usages of an include file across the documentation - Track where code examples are referenced -- Identify orphaned files (files with no references from include, literalinclude, io-code-block, or toctree directives) +- Identify orphaned files (files with no references from content inclusion directives) - Plan refactoring by understanding file dependencies **Basic Usage:** ```bash -# Find what references an include file +# Find what references an include file (content inclusion only) ./audit-cli analyze file-references path/to/includes/fact.rst # Find what references a code example ./audit-cli analyze file-references path/to/code-examples/example.js +# Include toctree references (navigation links) +./audit-cli analyze file-references path/to/file.rst --include-toctree + # Get JSON output for automation ./audit-cli analyze file-references path/to/file.rst --format json @@ -323,7 +330,8 @@ This command helps writers: - `-v, --verbose` - Show detailed information including line numbers and reference paths - `-c, --count-only` - Only show the count of references (useful for quick checks and scripting) - `--paths-only` - Only show the file paths, one per line (useful for piping to other commands) -- `-t, --directive-type ` - Filter by directive type: `include`, `literalinclude`, or `io-code-block` +- `-t, --directive-type ` - Filter by directive type: `include`, `literalinclude`, `io-code-block`, or `toctree` +- `--include-toctree` - Include toctree entries (navigation links) in addition to content inclusion directives **Understanding the Counts:** @@ -339,20 +347,20 @@ This helps identify both the impact scope (how many files) and duplicate include **Supported Directive Types:** -The command tracks three types of RST directives: +By default, the command tracks content inclusion directives: -1. **`.. include::`** - RST content includes +1. **`.. include::`** - RST content includes (transcluded) ```rst .. include:: /includes/intro.rst ``` -2. **`.. literalinclude::`** - Code file references +2. **`.. literalinclude::`** - Code file references (transcluded) ```rst .. literalinclude:: /code-examples/example.py :language: python ``` -3. **`.. io-code-block::`** - Input/output examples with file arguments +3. **`.. io-code-block::`** - Input/output examples with file arguments (transcluded) ```rst .. io-code-block:: @@ -363,6 +371,17 @@ The command tracks three types of RST directives: :language: json ``` +With `--include-toctree`, also tracks: + +4. **`.. toctree::`** - Table of contents entries (navigation links, not transcluded) + ```rst + .. toctree:: + :maxdepth: 2 + + intro + getting-started + ``` + **Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) is not tracked. **Output Formats:** diff --git a/audit-cli/commands/analyze/file-references/analyzer.go b/audit-cli/commands/analyze/file-references/analyzer.go index 796fc91..b1d7716 100644 --- a/audit-cli/commands/analyze/file-references/analyzer.go +++ b/audit-cli/commands/analyze/file-references/analyzer.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "sort" "strings" @@ -13,24 +12,6 @@ import ( "github.com/mongodb/code-example-tooling/audit-cli/internal/rst" ) -// Regular expressions for matching directives -var ( - // includeRegex matches: .. include:: /path/to/file.rst - includeRegex = regexp.MustCompile(`^\.\.\s+include::\s+(.+)$`) - - // literalIncludeRegex matches: .. literalinclude:: /path/to/file.ext - literalIncludeRegex = regexp.MustCompile(`^\.\.\s+literalinclude::\s+(.+)$`) - - // ioCodeBlockRegex matches: .. io-code-block:: - ioCodeBlockRegex = regexp.MustCompile(`^\.\.\s+io-code-block::`) - - // inputRegex matches: .. input:: /path/to/file.ext (within io-code-block) - inputRegex = regexp.MustCompile(`^\.\.\s+input::\s+(.+)$`) - - // outputRegex matches: .. output:: /path/to/file.ext (within io-code-block) - outputRegex = regexp.MustCompile(`^\.\.\s+output::\s+(.+)$`) -) - // AnalyzeReferences finds all files that reference the target file. // // This function searches through all RST files (.rst, .txt) and YAML files (.yaml, .yml) @@ -38,13 +19,17 @@ var ( // literalinclude, or io-code-block directives. YAML files are included because extract // and release files contain RST directives within their content blocks. // +// By default, only content inclusion directives are searched. Set includeToctree to true +// to also search for toctree entries (navigation links). +// // Parameters: // - targetFile: Absolute path to the file to analyze +// - includeToctree: If true, include toctree entries in the search // // Returns: // - *ReferenceAnalysis: The analysis results // - error: Any error encountered during analysis -func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { +func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalysis, error) { // Get absolute path absTargetFile, err := filepath.Abs(targetFile) if err != nil { @@ -83,7 +68,7 @@ func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { } // Search for references in this file - refs, err := findReferencesInFile(path, absTargetFile, sourceDir) + refs, err := findReferencesInFile(path, absTargetFile, sourceDir, includeToctree) if err != nil { // Log error but continue processing other files fmt.Fprintf(os.Stderr, "Warning: failed to process %s: %v\n", path, err) @@ -110,17 +95,19 @@ func AnalyzeReferences(targetFile string) (*ReferenceAnalysis, error) { // findReferencesInFile searches a single file for references to the target file. // // This function scans through the file line by line looking for include, -// literalinclude, io-code-block, and toctree directives that reference the target file. +// literalinclude, and io-code-block directives that reference the target file. +// If includeToctree is true, also searches for toctree entries. // // Parameters: // - filePath: Path to the file to search // - targetFile: Absolute path to the target file // - sourceDir: Source directory (for resolving relative paths) +// - includeToctree: If true, include toctree entries in the search // // Returns: // - []FileReference: List of references found in this file // - error: Any error encountered during processing -func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReference, error) { +func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree bool) ([]FileReference, error) { file, err := os.Open(filePath) if err != nil { return nil, err @@ -140,15 +127,15 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen line := scanner.Text() trimmedLine := strings.TrimSpace(line) - // Check for toctree start (use shared regex from rst package) - if rst.ToctreeDirectiveRegex.MatchString(trimmedLine) { + // Check for toctree start (only if includeToctree is enabled) + if includeToctree && rst.ToctreeDirectiveRegex.MatchString(trimmedLine) { inToctree = true toctreeStartLine = lineNum continue } // Check for io-code-block start - if ioCodeBlockRegex.MatchString(trimmedLine) { + if rst.IOCodeBlockDirectiveRegex.MatchString(trimmedLine) { inIOCodeBlock = true ioCodeBlockStartLine = lineNum continue @@ -165,7 +152,7 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen } // Check for include directive - if matches := includeRegex.FindStringSubmatch(trimmedLine); matches != nil { + if matches := rst.IncludeDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { references = append(references, FileReference{ @@ -179,7 +166,7 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen } // Check for literalinclude directive - if matches := literalIncludeRegex.FindStringSubmatch(trimmedLine); matches != nil { + if matches := rst.LiteralIncludeDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { references = append(references, FileReference{ @@ -195,7 +182,7 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen // Check for input/output directives within io-code-block if inIOCodeBlock { // Check for input directive - if matches := inputRegex.FindStringSubmatch(trimmedLine); matches != nil { + if matches := rst.InputDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { references = append(references, FileReference{ @@ -209,7 +196,7 @@ func findReferencesInFile(filePath, targetFile, sourceDir string) ([]FileReferen } // Check for output directive - if matches := outputRegex.FindStringSubmatch(trimmedLine); matches != nil { + if matches := rst.OutputDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { references = append(references, FileReference{ diff --git a/audit-cli/commands/analyze/file-references/file_references.go b/audit-cli/commands/analyze/file-references/file_references.go index b8f90ed..8a36f44 100644 --- a/audit-cli/commands/analyze/file-references/file_references.go +++ b/audit-cli/commands/analyze/file-references/file_references.go @@ -37,11 +37,12 @@ import ( // - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block, toctree) func NewFileReferencesCommand() *cobra.Command { var ( - format string - verbose bool - countOnly bool - pathsOnly bool - directiveType string + format string + verbose bool + countOnly bool + pathsOnly bool + directiveType string + includeToctree bool ) cmd := &cobra.Command{ @@ -50,13 +51,15 @@ func NewFileReferencesCommand() *cobra.Command { Long: `Find all files that reference a target file through RST directives. This command performs reverse dependency analysis, showing which files reference -the target file through include, literalinclude, io-code-block, or toctree directives. +the target file through content inclusion directives (include, literalinclude, +io-code-block). Use --include-toctree to also search for toctree entries, which +are navigation links rather than content transclusion. Supported directive types: - - .. include:: RST content includes - - .. literalinclude:: Code file references - - .. io-code-block:: Input/output examples with file arguments - - .. toctree:: Table of contents entries + - .. include:: RST content includes (transcluded) + - .. literalinclude:: Code file references (transcluded) + - .. io-code-block:: Input/output examples with file arguments (transcluded) + - .. toctree:: Table of contents entries (navigation links, requires --include-toctree) The command searches all RST files (.rst, .txt) and YAML files (.yaml, .yml) in the source directory tree. YAML files are included because extract and release @@ -66,7 +69,7 @@ This is useful for: - Understanding the impact of changes to a file - Finding all usages of an include file - Tracking code example references - - Identifying orphaned files (files with no references, including toctree entries) + - Identifying orphaned files (files with no references from content inclusion directives) Examples: # Find what references an include file @@ -75,6 +78,9 @@ Examples: # Find what references a code example analyze file-references /path/to/code-examples/example.js + # Include toctree references (navigation links) + analyze file-references /path/to/file.rst --include-toctree + # Get JSON output analyze file-references /path/to/file.rst --format json @@ -91,7 +97,7 @@ Examples: analyze file-references /path/to/file.rst --directive-type include`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runReferences(args[0], format, verbose, countOnly, pathsOnly, directiveType) + return runReferences(args[0], format, verbose, countOnly, pathsOnly, directiveType, includeToctree) }, } @@ -100,6 +106,7 @@ Examples: cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of references") cmd.Flags().BoolVar(&pathsOnly, "paths-only", false, "Only show the file paths (one per line)") cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block, toctree)") + cmd.Flags().BoolVar(&includeToctree, "include-toctree", false, "Include toctree entries (navigation links) in addition to content inclusion directives") return cmd } @@ -115,10 +122,11 @@ Examples: // - countOnly: If true, only show the count // - pathsOnly: If true, only show the file paths // - directiveType: Filter by directive type (empty string means all types) +// - includeToctree: If true, include toctree entries in the search // // Returns: // - error: Any error encountered during analysis -func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool, directiveType string) error { +func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool, directiveType string, includeToctree bool) error { // Validate directive type if specified if directiveType != "" { validTypes := map[string]bool{ @@ -147,7 +155,7 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool } // Perform analysis - analysis, err := AnalyzeReferences(targetFile) + analysis, err := AnalyzeReferences(targetFile, includeToctree) if err != nil { return fmt.Errorf("failed to analyze references: %w", err) } diff --git a/audit-cli/commands/analyze/file-references/file_references_test.go b/audit-cli/commands/analyze/file-references/file_references_test.go index a83b687..0abb21b 100644 --- a/audit-cli/commands/analyze/file-references/file_references_test.go +++ b/audit-cli/commands/analyze/file-references/file_references_test.go @@ -19,7 +19,7 @@ func TestAnalyzeReferences(t *testing.T) { { name: "Include file with multiple references", targetFile: "includes/intro.rst", - expectedReferences: 6, // 4 RST files + 1 YAML file + 1 toctree + expectedReferences: 5, // 4 RST files + 1 YAML file (no toctree by default) expectedDirectiveType: "include", }, { @@ -51,8 +51,8 @@ func TestAnalyzeReferences(t *testing.T) { t.Fatalf("failed to get absolute path: %v", err) } - // Run analysis - analysis, err := AnalyzeReferences(absTargetPath) + // Run analysis (without toctree by default) + analysis, err := AnalyzeReferences(absTargetPath, false) if err != nil { t.Fatalf("AnalyzeReferences failed: %v", err) } @@ -105,6 +105,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile string expectedReferences int expectedDirective string + includeToctree bool }{ { name: "Include directive", @@ -112,6 +113,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "includes/intro.rst", expectedReferences: 1, expectedDirective: "include", + includeToctree: false, }, { name: "Literalinclude directive", @@ -119,6 +121,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "code-examples/example.py", expectedReferences: 1, expectedDirective: "literalinclude", + includeToctree: false, }, { name: "IO code block directive", @@ -126,6 +129,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "code-examples/example.js", expectedReferences: 1, expectedDirective: "io-code-block", + includeToctree: false, }, { name: "Duplicate includes", @@ -133,6 +137,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "includes/intro.rst", expectedReferences: 2, // Same file included twice expectedDirective: "include", + includeToctree: false, }, { name: "Toctree directive", @@ -140,6 +145,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "include-test.rst", expectedReferences: 1, expectedDirective: "toctree", + includeToctree: true, // Must enable toctree flag }, { name: "No references", @@ -147,6 +153,7 @@ func TestFindReferencesInFile(t *testing.T) { targetFile: "includes/intro.rst", expectedReferences: 0, expectedDirective: "", + includeToctree: false, }, } @@ -169,7 +176,7 @@ func TestFindReferencesInFile(t *testing.T) { t.Fatalf("failed to get absolute source dir: %v", err) } - refs, err := findReferencesInFile(absSearchPath, absTargetPath, absSourceDir) + refs, err := findReferencesInFile(absSearchPath, absTargetPath, absSourceDir, tt.includeToctree) if err != nil { t.Fatalf("findReferencesInFile failed: %v", err) } diff --git a/audit-cli/internal/rst/directive_parser.go b/audit-cli/internal/rst/directive_parser.go index 6539c2c..618257c 100644 --- a/audit-cli/internal/rst/directive_parser.go +++ b/audit-cli/internal/rst/directive_parser.go @@ -57,20 +57,26 @@ type SubDirective struct { } // Regular expressions for directive parsing +// +// Note: literalIncludeRegex is imported from directive_regex.go (LiteralIncludeDirectiveRegex) +// The other regexes here are specific to the parser and have different matching requirements. var ( - // Matches: .. literalinclude:: /path/to/file.php - literalIncludeRegex = regexp.MustCompile(`^\.\.\s+literalinclude::\s+(.+)$`) + // Alias for the shared literalinclude regex + literalIncludeRegex = LiteralIncludeDirectiveRegex // Matches: .. code-block:: python (language is optional) codeBlockRegex = regexp.MustCompile(`^\.\.\s+code-block::\s*(.*)$`) - // Matches: .. io-code-block:: + // Matches: .. io-code-block:: (strict - must end after directive) + // This is different from IOCodeBlockDirectiveRegex which is more permissive ioCodeBlockRegex = regexp.MustCompile(`^\.\.\s+io-code-block::\s*$`) // Matches: .. input:: /path/to/file.cs (filepath is optional) + // This is different from InputDirectiveRegex which requires an argument inputDirectiveRegex = regexp.MustCompile(`^\.\.\s+input::\s*(.*)$`) // Matches: .. output:: /path/to/file.txt (filepath is optional) + // This is different from OutputDirectiveRegex which requires an argument outputDirectiveRegex = regexp.MustCompile(`^\.\.\s+output::\s*(.*)$`) // Matches directive options like: :language: python diff --git a/audit-cli/internal/rst/directive_regex.go b/audit-cli/internal/rst/directive_regex.go new file mode 100644 index 0000000..9fc7ac0 --- /dev/null +++ b/audit-cli/internal/rst/directive_regex.go @@ -0,0 +1,33 @@ +package rst + +import "regexp" + +// RST Directive Regular Expressions +// +// This file contains all regular expressions for matching RST directives. +// These patterns are shared across the codebase to ensure consistency. + +// IncludeDirectiveRegex matches .. include:: directives in RST files. +// Example: .. include:: /path/to/file.rst +var IncludeDirectiveRegex = regexp.MustCompile(`^\.\.\s+include::\s+(.+)$`) + +// LiteralIncludeDirectiveRegex matches .. literalinclude:: directives in RST files. +// Example: .. literalinclude:: /path/to/file.py +var LiteralIncludeDirectiveRegex = regexp.MustCompile(`^\.\.\s+literalinclude::\s+(.+)$`) + +// IOCodeBlockDirectiveRegex matches .. io-code-block:: directives in RST files. +// Example: .. io-code-block:: +var IOCodeBlockDirectiveRegex = regexp.MustCompile(`^\.\.\s+io-code-block::`) + +// InputDirectiveRegex matches .. input:: directives within io-code-block in RST files. +// Example: .. input:: /path/to/file.js +var InputDirectiveRegex = regexp.MustCompile(`^\.\.\s+input::\s+(.+)$`) + +// OutputDirectiveRegex matches .. output:: directives within io-code-block in RST files. +// Example: .. output:: /path/to/file.json +var OutputDirectiveRegex = regexp.MustCompile(`^\.\.\s+output::\s+(.+)$`) + +// ToctreeDirectiveRegex matches .. toctree:: directives in RST files. +// Example: .. toctree:: +var ToctreeDirectiveRegex = regexp.MustCompile(`^\.\.\s+toctree::`) + diff --git a/audit-cli/internal/rst/include_resolver.go b/audit-cli/internal/rst/include_resolver.go index 6d4d6f3..d57243e 100644 --- a/audit-cli/internal/rst/include_resolver.go +++ b/audit-cli/internal/rst/include_resolver.go @@ -5,18 +5,11 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" "github.com/mongodb/code-example-tooling/audit-cli/internal/pathresolver" ) -// IncludeDirectiveRegex matches .. include:: directives in RST files. -var IncludeDirectiveRegex = regexp.MustCompile(`^\.\.\s+include::\s+(.+)$`) - -// ToctreeDirectiveRegex matches .. toctree:: directives in RST files. -var ToctreeDirectiveRegex = regexp.MustCompile(`^\.\.\s+toctree::`) - // FindIncludeDirectives finds all include directives in a file and resolves their paths. // // This function scans the file for .. include:: directives and resolves each path diff --git a/audit-cli/output/literalinclude-test.literalinclude.1.py b/audit-cli/output/literalinclude-test.literalinclude.1.py new file mode 100644 index 0000000..ac2eb81 --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.1.py @@ -0,0 +1,4 @@ +def hello_world(): + """Print hello world message.""" + print("Hello, World!") + return True \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.2.go b/audit-cli/output/literalinclude-test.literalinclude.2.go new file mode 100644 index 0000000..bc4d6fa --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.2.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello from Go!") +} \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.3.js b/audit-cli/output/literalinclude-test.literalinclude.3.js new file mode 100644 index 0000000..7c75f16 --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.3.js @@ -0,0 +1,5 @@ +function greet(name) { + return `Hello, ${name}!`; +} + +console.log(greet("World")); \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.4.php b/audit-cli/output/literalinclude-test.literalinclude.4.php new file mode 100644 index 0000000..5e921e5 --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.4.php @@ -0,0 +1,6 @@ + 'localhost', + 'port' => 27017 +]; \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.5.rb b/audit-cli/output/literalinclude-test.literalinclude.5.rb new file mode 100644 index 0000000..6201a21 --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.5.rb @@ -0,0 +1,10 @@ +# Ruby example +class Greeter + def initialize(name) + @name = name + end + + def greet + puts "Hello, #{@name}!" + end +end \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.6.ts b/audit-cli/output/literalinclude-test.literalinclude.6.ts new file mode 100644 index 0000000..721cc1e --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.6.ts @@ -0,0 +1,9 @@ +// TypeScript example +interface User { + name: string; + age: number; +} + +function greetUser(user: User): string { + return `Hello, ${user.name}!`; +} \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.7.cpp b/audit-cli/output/literalinclude-test.literalinclude.7.cpp new file mode 100644 index 0000000..28276a3 --- /dev/null +++ b/audit-cli/output/literalinclude-test.literalinclude.7.cpp @@ -0,0 +1,8 @@ +#include +#include + +int main() { + std::string message = "Hello from C++!"; + std::cout << message << std::endl; + return 0; +} \ No newline at end of file From 887919575b582ef73a34678c0f7ba678cac13206 Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 13:27:13 -0400 Subject: [PATCH 05/14] refactor(analyze-includes): revert toctree support --- audit-cli/README.md | 23 +++++++++------- .../commands/analyze/includes/analyzer.go | 18 +++---------- .../commands/analyze/includes/includes.go | 27 +++++++++---------- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/audit-cli/README.md b/audit-cli/README.md index 78316b5..fa9a47b 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -59,7 +59,7 @@ audit-cli │ └── find-string ├── analyze # Analyze RST file structures │ ├── includes -│ └── references +│ └── file-references └── compare # Compare files across versions └── file-contents ``` @@ -223,9 +223,9 @@ With `-v` flag, also shows: #### `analyze includes` -Analyze `include` directive and `toctree` relationships in RST files to understand file dependencies. +Analyze `include` directive relationships in RST files to understand file dependencies. -This command recursively follows both `.. include::` directives and `.. toctree::` entries to show all files that are referenced from a starting file. This provides a complete picture of file dependencies including both content includes and table of contents structure. +This command recursively follows `.. include::` directives to show all files that are referenced from a starting file. This helps you understand which content is transcluded into a page. **Use Cases:** @@ -234,7 +234,7 @@ This command helps writers: - Identify circular include dependencies (files included multiple times) - Document file relationships for maintenance - Plan refactoring of complex include structures -- Understand table of contents structure and page hierarchies +- See what content is actually pulled into a page **Basic Usage:** @@ -286,6 +286,12 @@ times (e.g., file A includes file C, and file B also includes file C), the file However, the tree view will show it in all locations where it appears, with subsequent occurrences marked as circular includes in verbose mode. +**Note on Toctree:** + +This command does **not** follow `.. toctree::` entries. Toctree entries are navigation links to other pages, not content +that's transcluded into the page. If you need to find which files reference a target file through toctree entries, use +the `analyze file-references` command with the `--include-toctree` flag. + #### `analyze file-references` Find all files that reference a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, `io-code-block`, or `toctree` directives. @@ -382,8 +388,7 @@ With `--include-toctree`, also tracks: getting-started ``` -**Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) is not tracked. - +**Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) aly **Output Formats:** **Text** (default): @@ -687,9 +692,9 @@ audit-cli/ │ │ │ ├── analyzer.go # Include tree building │ │ │ ├── output.go # Output formatting │ │ │ └── types.go # Type definitions -│ │ └── references/ # References analysis subcommand -│ │ ├── references.go # Command logic -│ │ ├── references_test.go # Tests +│ │ └── file-references/ # File-references analysis subcommand +│ │ ├── file-references.go # Command logic +│ │ ├── file-references_test.go # Tests │ │ ├── analyzer.go # Reference finding logic │ │ ├── output.go # Output formatting │ │ └── types.go # Type definitions diff --git a/audit-cli/commands/analyze/includes/analyzer.go b/audit-cli/commands/analyze/includes/analyzer.go index e94ef2a..2d02dad 100644 --- a/audit-cli/commands/analyze/includes/analyzer.go +++ b/audit-cli/commands/analyze/includes/analyzer.go @@ -106,23 +106,13 @@ func buildIncludeTree(filePath string, visited map[string]bool, verbose bool, de includeFiles = []string{} } - // Find toctree entries in this file - toctreeFiles, err := rst.FindToctreeEntries(absPath) - if err != nil { - // Not a fatal error - file might not have toctree - toctreeFiles = []string{} - } - - // Combine include and toctree files - allFiles := append(includeFiles, toctreeFiles...) - - if verbose && len(allFiles) > 0 { + if verbose && len(includeFiles) > 0 { indent := getIndent(depth) - fmt.Printf("%s📄 %s (%d includes, %d toctree entries)\n", indent, filepath.Base(absPath), len(includeFiles), len(toctreeFiles)) + fmt.Printf("%s📄 %s (%d includes)\n", indent, filepath.Base(absPath), len(includeFiles)) } - // Recursively process each included/toctree file - for _, includeFile := range allFiles { + // Recursively process each included file + for _, includeFile := range includeFiles { childNode, err := buildIncludeTree(includeFile, visited, verbose, depth+1) if err != nil { fmt.Fprintf(os.Stderr, "Warning: failed to process file %s: %v\n", includeFile, err) diff --git a/audit-cli/commands/analyze/includes/includes.go b/audit-cli/commands/analyze/includes/includes.go index 59dda39..a5f3a9b 100644 --- a/audit-cli/commands/analyze/includes/includes.go +++ b/audit-cli/commands/analyze/includes/includes.go @@ -1,12 +1,12 @@ -// Package includes provides functionality for analyzing include and toctree relationships. +// Package includes provides functionality for analyzing include relationships. // // This package implements the "analyze includes" subcommand, which analyzes RST files -// to understand their include directive and toctree relationships. It can display results as: -// - A hierarchical tree structure showing include and toctree relationships -// - A flat list of all files referenced through includes and toctree entries +// to understand their include directive relationships. It can display results as: +// - A hierarchical tree structure showing include relationships +// - A flat list of all files referenced through includes // // This helps writers understand the impact of changes to files that are widely included -// or referenced in table of contents. +// across the documentation. package includes import ( @@ -17,7 +17,7 @@ import ( // NewIncludesCommand creates the includes subcommand. // -// This command analyzes include directive and toctree relationships in RST files. +// This command analyzes include directive relationships in RST files. // Supports flags for different output formats (tree or list). // // Flags: @@ -33,17 +33,16 @@ func NewIncludesCommand() *cobra.Command { cmd := &cobra.Command{ Use: "includes [filepath]", - Short: "Analyze include and toctree relationships in RST files", - Long: `Analyze include directive and toctree relationships to understand file dependencies. + Short: "Analyze include relationships in RST files", + Long: `Analyze include directive relationships to understand file dependencies. -This command recursively follows .. include:: directives and .. toctree:: entries -and shows all files that are referenced. This helps writers understand the impact -of changes to files that are widely included or referenced in table of contents -across the documentation. +This command recursively follows .. include:: directives and shows all files +that are referenced. This helps writers understand the impact of changes to +files that are widely included across the documentation. Output formats: - --tree: Show hierarchical tree structure of includes and toctree entries - --list: Show flat list of all included and toctree files + --tree: Show hierarchical tree structure of includes + --list: Show flat list of all included files If neither flag is specified, shows a summary with basic statistics.`, Args: cobra.ExactArgs(1), From e42c4d556867988d4e8df9b0378bf503d5c13c2a Mon Sep 17 00:00:00 2001 From: cbullinger Date: Wed, 29 Oct 2025 13:36:32 -0400 Subject: [PATCH 06/14] fix file name so tests pass --- audit-cli/README.md | 76 +++++++++---------- ...alinclude-test.literalinclude.2.go.output} | 0 2 files changed, 38 insertions(+), 38 deletions(-) rename audit-cli/output/{literalinclude-test.literalinclude.2.go => literalinclude-test.literalinclude.2.go.output} (100%) diff --git a/audit-cli/README.md b/audit-cli/README.md index fa9a47b..e0d06f6 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -1338,44 +1338,44 @@ See the code in `internal/rst/` for implementation details. The tool normalizes language identifiers to standard file extensions: -| Input | Normalized | Extension | -|-------|-----------|-----------| -| `bash` | `bash` | `.sh` | -| `c` | `c` | `.c` | -| `c++` | `cpp` | `.cpp` | -| `c#` | `csharp` | `.cs` | -| `console` | `console` | `.sh` | -| `cpp` | `cpp` | `.cpp` | -| `cs` | `csharp` | `.cs` | -| `csharp` | `csharp` | `.cs` | -| `go` | `go` | `.go` | -| `golang` | `go` | `.go` | -| `java` | `java` | `.java` | -| `javascript` | `javascript` | `.js` | -| `js` | `javascript` | `.js` | -| `kotlin` | `kotlin` | `.kt` | -| `kt` | `kotlin` | `.kt` | -| `php` | `php` | `.php` | -| `powershell` | `powershell` | `.ps1` | -| `ps1` | `powershell` | `.ps1` | -| `ps5` | `ps5` | `.ps1` | -| `py` | `python` | `.py` | -| `python` | `python` | `.py` | -| `rb` | `ruby` | `.rb` | -| `rs` | `rust` | `.rs` | -| `ruby` | `ruby` | `.rb` | -| `rust` | `rust` | `.rs` | -| `scala` | `scala` | `.scala` | -| `sh` | `shell` | `.sh` | -| `shell` | `shell` | `.sh` | -| `swift` | `swift` | `.swift` | -| `text` | `text` | `.txt` | -| `ts` | `typescript` | `.ts` | -| `txt` | `text` | `.txt` | -| `typescript` | `typescript` | `.ts` | -| (empty string) | `undefined` | `.txt` | -| `none` | `undefined` | `.txt` | -| (unknown) | (unchanged) | `.txt` | +| Input | Normalized | Extension | +|----------------|--------------|-----------| +| `bash` | `bash` | `.sh` | +| `c` | `c` | `.c` | +| `c++` | `cpp` | `.cpp` | +| `c#` | `csharp` | `.cs` | +| `console` | `console` | `.sh` | +| `cpp` | `cpp` | `.cpp` | +| `cs` | `csharp` | `.cs` | +| `csharp` | `csharp` | `.cs` | +| `go` | `go` | `.go` | +| `golang` | `go` | `.go` | +| `java` | `java` | `.java` | +| `javascript` | `javascript` | `.js` | +| `js` | `javascript` | `.js` | +| `kotlin` | `kotlin` | `.kt` | +| `kt` | `kotlin` | `.kt` | +| `php` | `php` | `.php` | +| `powershell` | `powershell` | `.ps1` | +| `ps1` | `powershell` | `.ps1` | +| `ps5` | `ps5` | `.ps1` | +| `py` | `python` | `.py` | +| `python` | `python` | `.py` | +| `rb` | `ruby` | `.rb` | +| `rs` | `rust` | `.rs` | +| `ruby` | `ruby` | `.rb` | +| `rust` | `rust` | `.rs` | +| `scala` | `scala` | `.scala` | +| `sh` | `shell` | `.sh` | +| `shell` | `shell` | `.sh` | +| `swift` | `swift` | `.swift` | +| `text` | `text` | `.txt` | +| `ts` | `typescript` | `.ts` | +| `txt` | `text` | `.txt` | +| `typescript` | `typescript` | `.ts` | +| (empty string) | `undefined` | `.txt` | +| `none` | `undefined` | `.txt` | +| (unknown) | (unchanged) | `.txt` | **Notes:** - Language identifiers are case-insensitive diff --git a/audit-cli/output/literalinclude-test.literalinclude.2.go b/audit-cli/output/literalinclude-test.literalinclude.2.go.output similarity index 100% rename from audit-cli/output/literalinclude-test.literalinclude.2.go rename to audit-cli/output/literalinclude-test.literalinclude.2.go.output From 285170f1e4890f861d8363b129b7f32f5cefd487 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 14 Nov 2025 09:40:19 -0500 Subject: [PATCH 07/14] improve analyze references cmd and remove orphaned file usage --- audit-cli/README.md | 43 ++++++++++------ .../analyze/file-references/analyzer.go | 50 ++++++++++++++++++- .../file-references/file_references.go | 43 ++++++++++++---- .../file-references/file_references_test.go | 4 +- .../analyze/file-references/output.go | 43 +++++++++++++++- .../commands/analyze/file-references/types.go | 8 +-- 6 files changed, 159 insertions(+), 32 deletions(-) diff --git a/audit-cli/README.md b/audit-cli/README.md index e0d06f6..591ef5d 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -308,7 +308,6 @@ This command helps writers: - Understand the impact of changes to a file (what pages will be affected) - Find all usages of an include file across the documentation - Track where code examples are referenced -- Identify orphaned files (files with no references from content inclusion directives) - Plan refactoring by understanding file dependencies **Basic Usage:** @@ -336,8 +335,10 @@ This command helps writers: - `-v, --verbose` - Show detailed information including line numbers and reference paths - `-c, --count-only` - Only show the count of references (useful for quick checks and scripting) - `--paths-only` - Only show the file paths, one per line (useful for piping to other commands) +- `--summary` - Only show summary statistics (total files and references by type, without file list) - `-t, --directive-type ` - Filter by directive type: `include`, `literalinclude`, `io-code-block`, or `toctree` - `--include-toctree` - Include toctree entries (navigation links) in addition to content inclusion directives +- `--exclude ` - Exclude paths matching this glob pattern (e.g., `*/archive/*` or `*/deprecated/*`) **Understanding the Counts:** @@ -388,7 +389,8 @@ With `--include-toctree`, also tracks: getting-started ``` -**Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) aly +**Note:** Only file-based references are tracked. Inline content (e.g., `.. input::` with `:language:` but no file path) is not tracked since it doesn't reference external files. + **Output Formats:** **Text** (default): @@ -440,22 +442,22 @@ include : 3 files, 4 references "total_references": 4, "referencing_files": [ { - "FilePath": "/path/to/duplicate-include-test.rst", - "DirectiveType": "include", - "ReferencePath": "/includes/intro.rst", - "LineNumber": 6 + "file_path": "/path/to/duplicate-include-test.rst", + "directive_type": "include", + "reference_path": "/includes/intro.rst", + "line_number": 6 }, { - "FilePath": "/path/to/duplicate-include-test.rst", - "DirectiveType": "include", - "ReferencePath": "/includes/intro.rst", - "LineNumber": 13 + "file_path": "/path/to/duplicate-include-test.rst", + "directive_type": "include", + "reference_path": "/includes/intro.rst", + "line_number": 13 }, { - "FilePath": "/path/to/include-test.rst", - "DirectiveType": "include", - "ReferencePath": "/includes/intro.rst", - "LineNumber": 6 + "file_path": "/path/to/include-test.rst", + "directive_type": "include", + "reference_path": "/includes/intro.rst", + "line_number": 6 } ] } @@ -480,6 +482,15 @@ include : 3 files, 4 references ./audit-cli analyze file-references ~/docs/source/includes/fact.rst --count-only # Output: 5 +# Show summary statistics only +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --summary +# Output: +# Total Files: 3 +# Total References: 5 +# +# By Type: +# include : 3 files, 5 references + # Get list of files for piping to other commands ./audit-cli analyze file-references ~/docs/source/includes/fact.rst --paths-only # Output: @@ -498,6 +509,10 @@ include : 3 files, 4 references # Combine filters: list files that use this as an io-code-block ./audit-cli analyze file-references ~/docs/source/code-examples/query.js -t io-code-block --paths-only + +# Exclude archived or deprecated files from search +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --exclude "*/archive/*" +./audit-cli analyze file-references ~/docs/source/includes/fact.rst --exclude "*/deprecated/*" ``` ### Compare Commands diff --git a/audit-cli/commands/analyze/file-references/analyzer.go b/audit-cli/commands/analyze/file-references/analyzer.go index b1d7716..b586a0c 100644 --- a/audit-cli/commands/analyze/file-references/analyzer.go +++ b/audit-cli/commands/analyze/file-references/analyzer.go @@ -25,11 +25,18 @@ import ( // Parameters: // - targetFile: Absolute path to the file to analyze // - includeToctree: If true, include toctree entries in the search +// - verbose: If true, show progress information +// - excludePattern: Glob pattern for paths to exclude (empty string means no exclusion) // // Returns: // - *ReferenceAnalysis: The analysis results // - error: Any error encountered during analysis -func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalysis, error) { +func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, excludePattern string) (*ReferenceAnalysis, error) { + // Check if target file exists + if _, err := os.Stat(targetFile); os.IsNotExist(err) { + return nil, fmt.Errorf("target file does not exist: %s\n\nPlease check:\n - The file path is correct\n - The file hasn't been moved or deleted\n - You have permission to access the file", targetFile) + } + // Get absolute path absTargetFile, err := filepath.Abs(targetFile) if err != nil { @@ -39,7 +46,7 @@ func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalys // Find the source directory sourceDir, err := pathresolver.FindSourceDirectory(absTargetFile) if err != nil { - return nil, fmt.Errorf("failed to find source directory: %w", err) + return nil, fmt.Errorf("failed to find source directory: %w\n\nThe source directory is detected by looking for a 'source' directory in the file's path.\nMake sure the target file is within a documentation repository with a 'source' directory.", err) } // Initialize analysis result @@ -49,6 +56,15 @@ func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalys ReferencingFiles: []FileReference{}, } + // Track if we found any RST/YAML files + foundAnyFiles := false + filesProcessed := 0 + + // Show progress message if verbose + if verbose { + fmt.Fprintf(os.Stderr, "Scanning for references in %s...\n", sourceDir) + } + // Walk through all RST and YAML files in the source directory err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -67,6 +83,26 @@ func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalys return nil } + // Check if path should be excluded + if excludePattern != "" { + matched, err := filepath.Match(excludePattern, path) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: invalid exclude pattern: %v\n", err) + } else if matched { + // Skip this file + return nil + } + } + + // Mark that we found at least one file + foundAnyFiles = true + filesProcessed++ + + // Show progress every 100 files if verbose + if verbose && filesProcessed%100 == 0 { + fmt.Fprintf(os.Stderr, "Processed %d files...\n", filesProcessed) + } + // Search for references in this file refs, err := findReferencesInFile(path, absTargetFile, sourceDir, includeToctree) if err != nil { @@ -85,6 +121,16 @@ func AnalyzeReferences(targetFile string, includeToctree bool) (*ReferenceAnalys return nil, fmt.Errorf("failed to walk source directory: %w", err) } + // Check if we found any RST/YAML files + if !foundAnyFiles { + return nil, fmt.Errorf("no RST or YAML files found in source directory: %s\n\nThis might not be a documentation repository.\nExpected to find files with extensions: .rst, .txt, .yaml, .yml", sourceDir) + } + + // Show completion message if verbose + if verbose { + fmt.Fprintf(os.Stderr, "Scan complete. Processed %d files.\n", filesProcessed) + } + // Update total counts analysis.TotalReferences = len(analysis.ReferencingFiles) analysis.TotalFiles = countUniqueFiles(analysis.ReferencingFiles) diff --git a/audit-cli/commands/analyze/file-references/file_references.go b/audit-cli/commands/analyze/file-references/file_references.go index 8a36f44..c66f7ad 100644 --- a/audit-cli/commands/analyze/file-references/file_references.go +++ b/audit-cli/commands/analyze/file-references/file_references.go @@ -11,7 +11,6 @@ // - Understanding the impact of changes to a file // - Finding all usages of an include file // - Tracking code example references -// - Identifying orphaned files (files with no references, including toctree entries) package filereferences import ( @@ -41,8 +40,10 @@ func NewFileReferencesCommand() *cobra.Command { verbose bool countOnly bool pathsOnly bool + summaryOnly bool directiveType string includeToctree bool + excludePattern string ) cmd := &cobra.Command{ @@ -69,7 +70,6 @@ This is useful for: - Understanding the impact of changes to a file - Finding all usages of an include file - Tracking code example references - - Identifying orphaned files (files with no references from content inclusion directives) Examples: # Find what references an include file @@ -93,11 +93,17 @@ Examples: # Just show the file paths analyze file-references /path/to/file.rst --paths-only + # Show summary statistics only + analyze file-references /path/to/file.rst --summary + + # Exclude certain paths from search + analyze file-references /path/to/file.rst --exclude "*/archive/*" + # Filter by directive type analyze file-references /path/to/file.rst --directive-type include`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runReferences(args[0], format, verbose, countOnly, pathsOnly, directiveType, includeToctree) + return runReferences(args[0], format, verbose, countOnly, pathsOnly, summaryOnly, directiveType, includeToctree, excludePattern) }, } @@ -105,8 +111,10 @@ Examples: cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed information including line numbers") cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of references") cmd.Flags().BoolVar(&pathsOnly, "paths-only", false, "Only show the file paths (one per line)") + cmd.Flags().BoolVar(&summaryOnly, "summary", false, "Only show summary statistics (total files and references by type)") cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block, toctree)") cmd.Flags().BoolVar(&includeToctree, "include-toctree", false, "Include toctree entries (navigation links) in addition to content inclusion directives") + cmd.Flags().StringVar(&excludePattern, "exclude", "", "Exclude paths matching this glob pattern (e.g., '*/archive/*' or '*/deprecated/*')") return cmd } @@ -121,12 +129,14 @@ Examples: // - verbose: If true, show detailed information // - countOnly: If true, only show the count // - pathsOnly: If true, only show the file paths +// - summaryOnly: If true, only show summary statistics // - directiveType: Filter by directive type (empty string means all types) // - includeToctree: If true, include toctree entries in the search +// - excludePattern: Glob pattern for paths to exclude (empty string means no exclusion) // // Returns: // - error: Any error encountered during analysis -func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool, directiveType string, includeToctree bool) error { +func runReferences(targetFile, format string, verbose, countOnly, pathsOnly, summaryOnly bool, directiveType string, includeToctree bool, excludePattern string) error { // Validate directive type if specified if directiveType != "" { validTypes := map[string]bool{ @@ -147,15 +157,25 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool } // Validate flag combinations - if countOnly && pathsOnly { - return fmt.Errorf("cannot use --count-only and --paths-only together") + exclusiveFlags := 0 + if countOnly { + exclusiveFlags++ + } + if pathsOnly { + exclusiveFlags++ } - if (countOnly || pathsOnly) && outputFormat == FormatJSON { - return fmt.Errorf("--count-only and --paths-only are not compatible with --format json") + if summaryOnly { + exclusiveFlags++ + } + if exclusiveFlags > 1 { + return fmt.Errorf("cannot use --count-only, --paths-only, and --summary together") + } + if (countOnly || pathsOnly || summaryOnly) && outputFormat == FormatJSON { + return fmt.Errorf("--count-only, --paths-only, and --summary are not compatible with --format json") } // Perform analysis - analysis, err := AnalyzeReferences(targetFile, includeToctree) + analysis, err := AnalyzeReferences(targetFile, includeToctree, verbose, excludePattern) if err != nil { return fmt.Errorf("failed to analyze references: %w", err) } @@ -176,6 +196,11 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly bool return PrintPathsOnly(analysis) } + // Handle summary-only output + if summaryOnly { + return PrintSummary(analysis) + } + // Print full results return PrintAnalysis(analysis, outputFormat, verbose) } diff --git a/audit-cli/commands/analyze/file-references/file_references_test.go b/audit-cli/commands/analyze/file-references/file_references_test.go index 0abb21b..d23e576 100644 --- a/audit-cli/commands/analyze/file-references/file_references_test.go +++ b/audit-cli/commands/analyze/file-references/file_references_test.go @@ -51,8 +51,8 @@ func TestAnalyzeReferences(t *testing.T) { t.Fatalf("failed to get absolute path: %v", err) } - // Run analysis (without toctree by default) - analysis, err := AnalyzeReferences(absTargetPath, false) + // Run analysis (without toctree by default, not verbose, no exclude pattern) + analysis, err := AnalyzeReferences(absTargetPath, false, false, "") if err != nil { t.Fatalf("AnalyzeReferences failed: %v", err) } diff --git a/audit-cli/commands/analyze/file-references/output.go b/audit-cli/commands/analyze/file-references/output.go index 3a08d2d..cbb2bb5 100644 --- a/audit-cli/commands/analyze/file-references/output.go +++ b/audit-cli/commands/analyze/file-references/output.go @@ -51,6 +51,14 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { if analysis.TotalReferences == 0 { fmt.Println("No files reference this file.") fmt.Println() + fmt.Println("This could mean:") + fmt.Println(" - The file is not included in any documentation pages") + fmt.Println(" - The file might be orphaned (not used)") + fmt.Println(" - The file is referenced using a different path") + fmt.Println() + fmt.Println("Note: By default, only content inclusion directives are searched.") + fmt.Println("Use --include-toctree to also search for toctree navigation links.") + fmt.Println() return } @@ -58,7 +66,8 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { byDirectiveType := groupByDirectiveType(analysis.ReferencingFiles) // Print breakdown by directive type with file and reference counts - for _, directiveType := range []string{"include", "literalinclude", "io-code-block"} { + directiveTypes := []string{"include", "literalinclude", "io-code-block", "toctree"} + for _, directiveType := range directiveTypes { if refs, ok := byDirectiveType[directiveType]; ok { uniqueFiles := countUniqueFiles(refs) totalRefs := len(refs) @@ -213,3 +222,35 @@ func PrintPathsOnly(analysis *ReferenceAnalysis) error { return nil } +// PrintSummary prints only summary statistics without the file list. +// +// This is useful for getting a quick overview of reference counts. +// +// Parameters: +// - analysis: The analysis results +// +// Returns: +// - error: Any error encountered during printing +func PrintSummary(analysis *ReferenceAnalysis) error { + fmt.Printf("Total Files: %d\n", analysis.TotalFiles) + fmt.Printf("Total References: %d\n", analysis.TotalReferences) + + if analysis.TotalReferences > 0 { + // Group by directive type + byDirectiveType := groupByDirectiveType(analysis.ReferencingFiles) + + // Print breakdown by type + fmt.Println("\nBy Type:") + directiveTypes := []string{"include", "literalinclude", "io-code-block", "toctree"} + for _, directiveType := range directiveTypes { + if refs, ok := byDirectiveType[directiveType]; ok { + uniqueFiles := countUniqueFiles(refs) + totalRefs := len(refs) + fmt.Printf(" %-20s: %d files, %d references\n", directiveType, uniqueFiles, totalRefs) + } + } + } + + return nil +} + diff --git a/audit-cli/commands/analyze/file-references/types.go b/audit-cli/commands/analyze/file-references/types.go index ebba67e..22f705a 100644 --- a/audit-cli/commands/analyze/file-references/types.go +++ b/audit-cli/commands/analyze/file-references/types.go @@ -30,17 +30,17 @@ type ReferenceAnalysis struct { // This structure captures details about how and where the reference occurs. type FileReference struct { // FilePath is the absolute path to the file that references the target - FilePath string + FilePath string `json:"file_path"` // DirectiveType is the type of directive used to reference the file // Possible values: "include", "literalinclude", "io-code-block", "toctree" - DirectiveType string + DirectiveType string `json:"directive_type"` // ReferencePath is the path used in the directive (as written in the file) - ReferencePath string + ReferencePath string `json:"reference_path"` // LineNumber is the line number where the reference occurs - LineNumber int + LineNumber int `json:"line_number"` } // ReferenceNode represents a node in the reference tree. From a7dd6768536624cd8f7bd97d5b32de42b1813ceb Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Fri, 14 Nov 2025 10:15:43 -0500 Subject: [PATCH 08/14] Fix: Update flags comment to include all available flags Added missing flags to the NewFileReferencesCommand comment: - --summary - --include-toctree - --exclude These flags were added during improvements but the comment wasn't updated. --- audit-cli/commands/analyze/file-references/file_references.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/audit-cli/commands/analyze/file-references/file_references.go b/audit-cli/commands/analyze/file-references/file_references.go index c66f7ad..f00a37e 100644 --- a/audit-cli/commands/analyze/file-references/file_references.go +++ b/audit-cli/commands/analyze/file-references/file_references.go @@ -33,7 +33,10 @@ import ( // - -v, --verbose: Show detailed information including line numbers // - -c, --count-only: Only show the count of references // - --paths-only: Only show the file paths +// - --summary: Only show summary statistics (total files and references by type) // - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block, toctree) +// - --include-toctree: Include toctree entries (navigation links) in addition to content inclusion directives +// - --exclude: Exclude paths matching this glob pattern (e.g., '*/archive/*') func NewFileReferencesCommand() *cobra.Command { var ( format string From 5782b7c0134a9accf4b68fbd2b3ffefffb394df8 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Wed, 19 Nov 2025 16:28:31 -0500 Subject: [PATCH 09/14] chore(output): delete cli output --- audit-cli/.gitignore | 1 + .../output/literalinclude-test.literalinclude.1.py | 4 ---- .../literalinclude-test.literalinclude.2.go.output | 7 ------- .../output/literalinclude-test.literalinclude.3.js | 5 ----- .../output/literalinclude-test.literalinclude.4.php | 6 ------ .../output/literalinclude-test.literalinclude.5.rb | 10 ---------- .../output/literalinclude-test.literalinclude.6.ts | 9 --------- .../output/literalinclude-test.literalinclude.7.cpp | 8 -------- 8 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.1.py delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.2.go.output delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.3.js delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.4.php delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.5.rb delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.6.ts delete mode 100644 audit-cli/output/literalinclude-test.literalinclude.7.cpp diff --git a/audit-cli/.gitignore b/audit-cli/.gitignore index bf1138a..b028d6f 100644 --- a/audit-cli/.gitignore +++ b/audit-cli/.gitignore @@ -1 +1,2 @@ audit-cli +output/ diff --git a/audit-cli/output/literalinclude-test.literalinclude.1.py b/audit-cli/output/literalinclude-test.literalinclude.1.py deleted file mode 100644 index ac2eb81..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.1.py +++ /dev/null @@ -1,4 +0,0 @@ -def hello_world(): - """Print hello world message.""" - print("Hello, World!") - return True \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.2.go.output b/audit-cli/output/literalinclude-test.literalinclude.2.go.output deleted file mode 100644 index bc4d6fa..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.2.go.output +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "fmt" - -func main() { - fmt.Println("Hello from Go!") -} \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.3.js b/audit-cli/output/literalinclude-test.literalinclude.3.js deleted file mode 100644 index 7c75f16..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.3.js +++ /dev/null @@ -1,5 +0,0 @@ -function greet(name) { - return `Hello, ${name}!`; -} - -console.log(greet("World")); \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.4.php b/audit-cli/output/literalinclude-test.literalinclude.4.php deleted file mode 100644 index 5e921e5..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.4.php +++ /dev/null @@ -1,6 +0,0 @@ - 'localhost', - 'port' => 27017 -]; \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.5.rb b/audit-cli/output/literalinclude-test.literalinclude.5.rb deleted file mode 100644 index 6201a21..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.5.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Ruby example -class Greeter - def initialize(name) - @name = name - end - - def greet - puts "Hello, #{@name}!" - end -end \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.6.ts b/audit-cli/output/literalinclude-test.literalinclude.6.ts deleted file mode 100644 index 721cc1e..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.6.ts +++ /dev/null @@ -1,9 +0,0 @@ -// TypeScript example -interface User { - name: string; - age: number; -} - -function greetUser(user: User): string { - return `Hello, ${user.name}!`; -} \ No newline at end of file diff --git a/audit-cli/output/literalinclude-test.literalinclude.7.cpp b/audit-cli/output/literalinclude-test.literalinclude.7.cpp deleted file mode 100644 index 28276a3..0000000 --- a/audit-cli/output/literalinclude-test.literalinclude.7.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include -#include - -int main() { - std::string message = "Hello from C++!"; - std::cout << message << std::endl; - return 0; -} \ No newline at end of file From 3378e0c0ba9b701254f3db91a5db05358646c666 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Wed, 19 Nov 2025 21:24:13 -0500 Subject: [PATCH 10/14] Rename 'analyze file-references' command to 'analyze usage' - Rename directory from file-references/ to usage/ - Update package name from filereferences to usage - Change command from 'file-references' to 'usage' (shorter, clearer) - Update all documentation and help text - Update README with new command examples - All tests pass The new 'analyze usage' command is shorter (13 vs 24 chars) and more accurately describes what it does: finding where a file is used. --- audit-cli/README.md | 54 +++++++++---------- audit-cli/commands/analyze/analyze.go | 8 +-- .../{file-references => usage}/analyzer.go | 2 +- .../file_references.go | 44 +++++++-------- .../file_references_test.go | 2 +- .../{file-references => usage}/output.go | 2 +- .../{file-references => usage}/types.go | 2 +- 7 files changed, 57 insertions(+), 57 deletions(-) rename audit-cli/commands/analyze/{file-references => usage}/analyzer.go (99%) rename audit-cli/commands/analyze/{file-references => usage}/file_references.go (84%) rename audit-cli/commands/analyze/{file-references => usage}/file_references_test.go (99%) rename audit-cli/commands/analyze/{file-references => usage}/output.go (99%) rename audit-cli/commands/analyze/{file-references => usage}/types.go (99%) diff --git a/audit-cli/README.md b/audit-cli/README.md index 591ef5d..051b052 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -59,7 +59,7 @@ audit-cli │ └── find-string ├── analyze # Analyze RST file structures │ ├── includes -│ └── file-references +│ └── usage └── compare # Compare files across versions └── file-contents ``` @@ -290,11 +290,11 @@ includes in verbose mode. This command does **not** follow `.. toctree::` entries. Toctree entries are navigation links to other pages, not content that's transcluded into the page. If you need to find which files reference a target file through toctree entries, use -the `analyze file-references` command with the `--include-toctree` flag. +the `analyze usage` command with the `--include-toctree` flag. -#### `analyze file-references` +#### `analyze usage` -Find all files that reference a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, `io-code-block`, or `toctree` directives. +Find all files that use a target file through RST directives. This performs reverse dependency analysis, showing which files reference the target file through `include`, `literalinclude`, `io-code-block`, or `toctree` directives. The command searches all RST files (`.rst` and `.txt` extensions) and YAML files (`.yaml` and `.yml` extensions) in the source directory tree. YAML files are included because extract and release files contain RST directives within their content blocks. @@ -313,20 +313,20 @@ This command helps writers: **Basic Usage:** ```bash -# Find what references an include file (content inclusion only) -./audit-cli analyze file-references path/to/includes/fact.rst +# Find what uses an include file (content inclusion only) +./audit-cli analyze usage path/to/includes/fact.rst -# Find what references a code example -./audit-cli analyze file-references path/to/code-examples/example.js +# Find what uses a code example +./audit-cli analyze usage path/to/code-examples/example.js # Include toctree references (navigation links) -./audit-cli analyze file-references path/to/file.rst --include-toctree +./audit-cli analyze usage path/to/file.rst --include-toctree # Get JSON output for automation -./audit-cli analyze file-references path/to/file.rst --format json +./audit-cli analyze usage path/to/file.rst --format json # Show detailed information with line numbers -./audit-cli analyze file-references path/to/file.rst --verbose +./audit-cli analyze usage path/to/file.rst --verbose ``` **Flags:** @@ -467,23 +467,23 @@ include : 3 files, 4 references ```bash # Check if an include file is being used -./audit-cli analyze file-references ~/docs/source/includes/fact-atlas.rst +./audit-cli analyze usage ~/docs/source/includes/fact-atlas.rst # Find all pages that use a specific code example -./audit-cli analyze file-references ~/docs/source/code-examples/connect.py +./audit-cli analyze usage ~/docs/source/code-examples/connect.py # Get machine-readable output for scripting -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --format json | jq '.total_references' +./audit-cli analyze usage ~/docs/source/includes/fact.rst --format json | jq '.total_references' # See exactly where a file is referenced (with line numbers) -./audit-cli analyze file-references ~/docs/source/includes/intro.rst --verbose +./audit-cli analyze usage ~/docs/source/includes/intro.rst --verbose # Quick check: just show the count -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --count-only +./audit-cli analyze usage ~/docs/source/includes/fact.rst --count-only # Output: 5 # Show summary statistics only -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --summary +./audit-cli analyze usage ~/docs/source/includes/fact.rst --summary # Output: # Total Files: 3 # Total References: 5 @@ -492,27 +492,27 @@ include : 3 files, 4 references # include : 3 files, 5 references # Get list of files for piping to other commands -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --paths-only +./audit-cli analyze usage ~/docs/source/includes/fact.rst --paths-only # Output: # page1.rst # page2.rst # page3.rst # Filter to only show include directives (not literalinclude or io-code-block) -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --directive-type include +./audit-cli analyze usage ~/docs/source/includes/fact.rst --directive-type include # Filter to only show literalinclude references -./audit-cli analyze file-references ~/docs/source/code-examples/example.py --directive-type literalinclude +./audit-cli analyze usage ~/docs/source/code-examples/example.py --directive-type literalinclude # Combine filters: count only literalinclude references -./audit-cli analyze file-references ~/docs/source/code-examples/example.py -t literalinclude -c +./audit-cli analyze usage ~/docs/source/code-examples/example.py -t literalinclude -c # Combine filters: list files that use this as an io-code-block -./audit-cli analyze file-references ~/docs/source/code-examples/query.js -t io-code-block --paths-only +./audit-cli analyze usage ~/docs/source/code-examples/query.js -t io-code-block --paths-only # Exclude archived or deprecated files from search -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --exclude "*/archive/*" -./audit-cli analyze file-references ~/docs/source/includes/fact.rst --exclude "*/deprecated/*" +./audit-cli analyze usage ~/docs/source/includes/fact.rst --exclude "*/archive/*" +./audit-cli analyze usage ~/docs/source/includes/fact.rst --exclude "*/deprecated/*" ``` ### Compare Commands @@ -707,9 +707,9 @@ audit-cli/ │ │ │ ├── analyzer.go # Include tree building │ │ │ ├── output.go # Output formatting │ │ │ └── types.go # Type definitions -│ │ └── file-references/ # File-references analysis subcommand -│ │ ├── file-references.go # Command logic -│ │ ├── file-references_test.go # Tests +│ │ └── usage/ # Usage analysis subcommand +│ │ ├── file_references.go # Command logic +│ │ ├── file_references_test.go # Tests │ │ ├── analyzer.go # Reference finding logic │ │ ├── output.go # Output formatting │ │ └── types.go # Type definitions diff --git a/audit-cli/commands/analyze/analyze.go b/audit-cli/commands/analyze/analyze.go index 4d7d63e..222ea1b 100644 --- a/audit-cli/commands/analyze/analyze.go +++ b/audit-cli/commands/analyze/analyze.go @@ -3,14 +3,14 @@ // This package serves as the parent command for various analysis operations. // Currently supports: // - includes: Analyze include directive relationships in RST files -// - file-references: Find all files that reference a target file +// - usage: Find all files that use a target file // // Future subcommands could include analyzing cross-references, broken links, or content metrics. package analyze import ( "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/includes" - filereferences "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/file-references" + "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/usage" "github.com/spf13/cobra" ) @@ -26,14 +26,14 @@ func NewAnalyzeCommand() *cobra.Command { Currently supports: - includes: Analyze include directive relationships (forward dependencies) - - file-references: Find all files that reference a target file (reverse dependencies) + - usage: Find all files that use a target file (reverse dependencies) Future subcommands may support analyzing cross-references, broken links, or content metrics.`, } // Add subcommands cmd.AddCommand(includes.NewIncludesCommand()) - cmd.AddCommand(filereferences.NewFileReferencesCommand()) + cmd.AddCommand(usage.NewUsageCommand()) return cmd } diff --git a/audit-cli/commands/analyze/file-references/analyzer.go b/audit-cli/commands/analyze/usage/analyzer.go similarity index 99% rename from audit-cli/commands/analyze/file-references/analyzer.go rename to audit-cli/commands/analyze/usage/analyzer.go index b586a0c..ebb46d1 100644 --- a/audit-cli/commands/analyze/file-references/analyzer.go +++ b/audit-cli/commands/analyze/usage/analyzer.go @@ -1,4 +1,4 @@ -package filereferences +package usage import ( "bufio" diff --git a/audit-cli/commands/analyze/file-references/file_references.go b/audit-cli/commands/analyze/usage/file_references.go similarity index 84% rename from audit-cli/commands/analyze/file-references/file_references.go rename to audit-cli/commands/analyze/usage/file_references.go index f00a37e..8b20778 100644 --- a/audit-cli/commands/analyze/file-references/file_references.go +++ b/audit-cli/commands/analyze/usage/file_references.go @@ -1,6 +1,6 @@ -// Package filereferences provides functionality for analyzing which files reference a target file. +// Package usage provides functionality for analyzing which files reference a target file. // -// This package implements the "analyze file-references" subcommand, which finds all files +// This package implements the "analyze usage" subcommand, which finds all files // that reference a given file through RST directives (include, literalinclude, io-code-block, toctree). // // The command searches both RST files (.rst, .txt) and YAML files (.yaml, .yml) since @@ -11,7 +11,7 @@ // - Understanding the impact of changes to a file // - Finding all usages of an include file // - Tracking code example references -package filereferences +package usage import ( "fmt" @@ -19,14 +19,14 @@ import ( "github.com/spf13/cobra" ) -// NewFileReferencesCommand creates the file-references subcommand. +// NewUsageCommand creates the usage subcommand. // // This command analyzes which files reference a given target file through // RST directives (include, literalinclude, io-code-block, toctree). // // Usage: -// analyze file-references /path/to/file.rst -// analyze file-references /path/to/code-example.js +// analyze usage /path/to/file.rst +// analyze usage /path/to/code-example.js // // Flags: // - --format: Output format (text or json) @@ -37,7 +37,7 @@ import ( // - -t, --directive-type: Filter by directive type (include, literalinclude, io-code-block, toctree) // - --include-toctree: Include toctree entries (navigation links) in addition to content inclusion directives // - --exclude: Exclude paths matching this glob pattern (e.g., '*/archive/*') -func NewFileReferencesCommand() *cobra.Command { +func NewUsageCommand() *cobra.Command { var ( format string verbose bool @@ -50,9 +50,9 @@ func NewFileReferencesCommand() *cobra.Command { ) cmd := &cobra.Command{ - Use: "file-references [filepath]", - Short: "Find all files that reference a target file", - Long: `Find all files that reference a target file through RST directives. + Use: "usage [filepath]", + Short: "Find all files that use a target file", + Long: `Find all files that use a target file through RST directives. This command performs reverse dependency analysis, showing which files reference the target file through content inclusion directives (include, literalinclude, @@ -75,35 +75,35 @@ This is useful for: - Tracking code example references Examples: - # Find what references an include file - analyze file-references /path/to/includes/fact.rst + # Find what uses an include file + analyze usage /path/to/includes/fact.rst - # Find what references a code example - analyze file-references /path/to/code-examples/example.js + # Find what uses a code example + analyze usage /path/to/code-examples/example.js # Include toctree references (navigation links) - analyze file-references /path/to/file.rst --include-toctree + analyze usage /path/to/file.rst --include-toctree # Get JSON output - analyze file-references /path/to/file.rst --format json + analyze usage /path/to/file.rst --format json # Show detailed information with line numbers - analyze file-references /path/to/file.rst --verbose + analyze usage /path/to/file.rst --verbose # Just show the count - analyze file-references /path/to/file.rst --count-only + analyze usage /path/to/file.rst --count-only # Just show the file paths - analyze file-references /path/to/file.rst --paths-only + analyze usage /path/to/file.rst --paths-only # Show summary statistics only - analyze file-references /path/to/file.rst --summary + analyze usage /path/to/file.rst --summary # Exclude certain paths from search - analyze file-references /path/to/file.rst --exclude "*/archive/*" + analyze usage /path/to/file.rst --exclude "*/archive/*" # Filter by directive type - analyze file-references /path/to/file.rst --directive-type include`, + analyze usage /path/to/file.rst --directive-type include`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runReferences(args[0], format, verbose, countOnly, pathsOnly, summaryOnly, directiveType, includeToctree, excludePattern) diff --git a/audit-cli/commands/analyze/file-references/file_references_test.go b/audit-cli/commands/analyze/usage/file_references_test.go similarity index 99% rename from audit-cli/commands/analyze/file-references/file_references_test.go rename to audit-cli/commands/analyze/usage/file_references_test.go index d23e576..29cf7eb 100644 --- a/audit-cli/commands/analyze/file-references/file_references_test.go +++ b/audit-cli/commands/analyze/usage/file_references_test.go @@ -1,4 +1,4 @@ -package filereferences +package usage import ( "path/filepath" diff --git a/audit-cli/commands/analyze/file-references/output.go b/audit-cli/commands/analyze/usage/output.go similarity index 99% rename from audit-cli/commands/analyze/file-references/output.go rename to audit-cli/commands/analyze/usage/output.go index cbb2bb5..a525e27 100644 --- a/audit-cli/commands/analyze/file-references/output.go +++ b/audit-cli/commands/analyze/usage/output.go @@ -1,4 +1,4 @@ -package filereferences +package usage import ( "encoding/json" diff --git a/audit-cli/commands/analyze/file-references/types.go b/audit-cli/commands/analyze/usage/types.go similarity index 99% rename from audit-cli/commands/analyze/file-references/types.go rename to audit-cli/commands/analyze/usage/types.go index 22f705a..007813f 100644 --- a/audit-cli/commands/analyze/file-references/types.go +++ b/audit-cli/commands/analyze/usage/types.go @@ -1,4 +1,4 @@ -package filereferences +package usage // ReferenceAnalysis contains the results of analyzing which files reference a target file. // From 8dcb06120e2b49e2d1ae77e336459f8beb7a7ef3 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Wed, 19 Nov 2025 21:35:20 -0500 Subject: [PATCH 11/14] Rename usage command files to match package name - Rename file_references.go to usage.go - Rename file_references_test.go to usage_test.go - Update README to reflect correct filenames - All tests still pass --- audit-cli/README.md | 4 ++-- .../commands/analyze/usage/{file_references.go => usage.go} | 0 .../analyze/usage/{file_references_test.go => usage_test.go} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename audit-cli/commands/analyze/usage/{file_references.go => usage.go} (100%) rename audit-cli/commands/analyze/usage/{file_references_test.go => usage_test.go} (100%) diff --git a/audit-cli/README.md b/audit-cli/README.md index 051b052..5114a94 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -708,8 +708,8 @@ audit-cli/ │ │ │ ├── output.go # Output formatting │ │ │ └── types.go # Type definitions │ │ └── usage/ # Usage analysis subcommand -│ │ ├── file_references.go # Command logic -│ │ ├── file_references_test.go # Tests +│ │ ├── usage.go # Command logic +│ │ ├── usage_test.go # Tests │ │ ├── analyzer.go # Reference finding logic │ │ ├── output.go # Output formatting │ │ └── types.go # Type definitions diff --git a/audit-cli/commands/analyze/usage/file_references.go b/audit-cli/commands/analyze/usage/usage.go similarity index 100% rename from audit-cli/commands/analyze/usage/file_references.go rename to audit-cli/commands/analyze/usage/usage.go diff --git a/audit-cli/commands/analyze/usage/file_references_test.go b/audit-cli/commands/analyze/usage/usage_test.go similarity index 100% rename from audit-cli/commands/analyze/usage/file_references_test.go rename to audit-cli/commands/analyze/usage/usage_test.go From 321d9671906890d9044ae7bbffac5f9ae1039043 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Thu, 20 Nov 2025 08:01:07 -0500 Subject: [PATCH 12/14] Update output terminology from 'references' to 'usages' - Change 'REFERENCE ANALYSIS' to 'USAGE ANALYSIS' in output header - Change 'Total References' to 'Total Usages' throughout output - Update 'No files reference' to 'No files use' in messages - Update README examples to show 'Total Usages' instead of 'Total References' - Keep internal type names (ReferenceAnalysis, FileReference) unchanged for API stability - Keep JSON field names unchanged for backward compatibility --- audit-cli/README.md | 20 +++++++++--------- audit-cli/commands/analyze/usage/output.go | 24 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/audit-cli/README.md b/audit-cli/README.md index 5114a94..3dfdbbf 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -333,9 +333,9 @@ This command helps writers: - `--format ` - Output format: `text` (default) or `json` - `-v, --verbose` - Show detailed information including line numbers and reference paths -- `-c, --count-only` - Only show the count of references (useful for quick checks and scripting) +- `-c, --count-only` - Only show the count of usages (useful for quick checks and scripting) - `--paths-only` - Only show the file paths, one per line (useful for piping to other commands) -- `--summary` - Only show summary statistics (total files and references by type, without file list) +- `--summary` - Only show summary statistics (total files and usages by type, without file list) - `-t, --directive-type ` - Filter by directive type: `include`, `literalinclude`, `io-code-block`, or `toctree` - `--include-toctree` - Include toctree entries (navigation links) in addition to content inclusion directives - `--exclude ` - Exclude paths matching this glob pattern (e.g., `*/archive/*` or `*/deprecated/*`) @@ -343,14 +343,14 @@ This command helps writers: **Understanding the Counts:** The command shows two metrics: -- **Total Files**: Number of unique files that reference the target (deduplicated) -- **Total References**: Total number of directive occurrences (includes duplicates) +- **Total Files**: Number of unique files that use the target (deduplicated) +- **Total Usages**: Total number of directive occurrences (includes duplicates) When a file includes the target multiple times, it counts as: - 1 file (in Total Files) -- Multiple references (in Total References) +- Multiple usages (in Total Usages) -This helps identify both the impact scope (how many files) and duplicate includes (when references > files). +This helps identify both the impact scope (how many files) and duplicate includes (when usages > files). **Supported Directive Types:** @@ -400,7 +400,7 @@ REFERENCE ANALYSIS ============================================================ Target File: /path/to/includes/intro.rst Total Files: 3 -Total References: 4 +Total Usages: 4 ============================================================ include : 3 files, 4 references @@ -418,7 +418,7 @@ REFERENCE ANALYSIS ============================================================ Target File: /path/to/includes/intro.rst Total Files: 3 -Total References: 4 +Total Usages: 4 ============================================================ include : 3 files, 4 references @@ -486,10 +486,10 @@ include : 3 files, 4 references ./audit-cli analyze usage ~/docs/source/includes/fact.rst --summary # Output: # Total Files: 3 -# Total References: 5 +# Total Usages: 5 # # By Type: -# include : 3 files, 5 references +# include : 3 files, 5 usages # Get list of files for piping to other commands ./audit-cli analyze usage ~/docs/source/includes/fact.rst --paths-only diff --git a/audit-cli/commands/analyze/usage/output.go b/audit-cli/commands/analyze/usage/output.go index a525e27..55f14c2 100644 --- a/audit-cli/commands/analyze/usage/output.go +++ b/audit-cli/commands/analyze/usage/output.go @@ -40,21 +40,21 @@ func PrintAnalysis(analysis *ReferenceAnalysis, format OutputFormat, verbose boo // printText prints the analysis results in human-readable text format. func printText(analysis *ReferenceAnalysis, verbose bool) { fmt.Println("============================================================") - fmt.Println("REFERENCE ANALYSIS") + fmt.Println("USAGE ANALYSIS") fmt.Println("============================================================") fmt.Printf("Target File: %s\n", analysis.TargetFile) fmt.Printf("Total Files: %d\n", analysis.TotalFiles) - fmt.Printf("Total References: %d\n", analysis.TotalReferences) + fmt.Printf("Total Usages: %d\n", analysis.TotalReferences) fmt.Println("============================================================") fmt.Println() if analysis.TotalReferences == 0 { - fmt.Println("No files reference this file.") + fmt.Println("No files use this file.") fmt.Println() fmt.Println("This could mean:") fmt.Println(" - The file is not included in any documentation pages") fmt.Println(" - The file might be orphaned (not used)") - fmt.Println(" - The file is referenced using a different path") + fmt.Println(" - The file is used with a different path") fmt.Println() fmt.Println("Note: By default, only content inclusion directives are searched.") fmt.Println("Use --include-toctree to also search for toctree navigation links.") @@ -77,9 +77,9 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { } else { // Has duplicates - show both counts if uniqueFiles == 1 { - fmt.Printf("%-20s: %d file, %d references\n", directiveType, uniqueFiles, totalRefs) + fmt.Printf("%-20s: %d file, %d usages\n", directiveType, uniqueFiles, totalRefs) } else { - fmt.Printf("%-20s: %d files, %d references\n", directiveType, uniqueFiles, totalRefs) + fmt.Printf("%-20s: %d files, %d usages\n", directiveType, uniqueFiles, totalRefs) } } } @@ -99,10 +99,10 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { // Print file path with directive type label if group.Count > 1 { - // Multiple references from this file - fmt.Printf("%3d. [%s] %s (%d references)\n", i+1, group.DirectiveType, relPath, group.Count) + // Multiple usages from this file + fmt.Printf("%3d. [%s] %s (%d usages)\n", i+1, group.DirectiveType, relPath, group.Count) } else { - // Single reference + // Single usage fmt.Printf("%3d. [%s] %s\n", i+1, group.DirectiveType, relPath) } @@ -224,7 +224,7 @@ func PrintPathsOnly(analysis *ReferenceAnalysis) error { // PrintSummary prints only summary statistics without the file list. // -// This is useful for getting a quick overview of reference counts. +// This is useful for getting a quick overview of usage counts. // // Parameters: // - analysis: The analysis results @@ -233,7 +233,7 @@ func PrintPathsOnly(analysis *ReferenceAnalysis) error { // - error: Any error encountered during printing func PrintSummary(analysis *ReferenceAnalysis) error { fmt.Printf("Total Files: %d\n", analysis.TotalFiles) - fmt.Printf("Total References: %d\n", analysis.TotalReferences) + fmt.Printf("Total Usages: %d\n", analysis.TotalReferences) if analysis.TotalReferences > 0 { // Group by directive type @@ -246,7 +246,7 @@ func PrintSummary(analysis *ReferenceAnalysis) error { if refs, ok := byDirectiveType[directiveType]; ok { uniqueFiles := countUniqueFiles(refs) totalRefs := len(refs) - fmt.Printf(" %-20s: %d files, %d references\n", directiveType, uniqueFiles, totalRefs) + fmt.Printf(" %-20s: %d files, %d usages\n", directiveType, uniqueFiles, totalRefs) } } } From 90d67751dd456b20cd42c058d0ed973cdc8d82c6 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Thu, 20 Nov 2025 08:09:43 -0500 Subject: [PATCH 13/14] Update internal types and fields from 'reference' to 'usage' terminology - Rename AnalyzeReferences() to AnalyzeUsage() - Rename findReferencesInFile() to findUsagesInFile() - Rename GroupReferencesByFile() to GroupUsagesByFile() - Rename runReferences() to runUsage() - Update all type names: - ReferenceAnalysis -> UsageAnalysis - FileReference -> FileUsage - ReferenceNode -> UsageNode - GroupedFileReference -> GroupedFileUsage - Update all field names: - ReferencingFiles -> UsingFiles - ReferenceTree -> UsageTree - TotalReferences -> TotalUsages - ReferencePath -> UsagePath - References -> Usages - Update JSON field names for consistency: - total_references -> total_usages - referencing_files -> using_files - reference_path -> usage_path - Update all test functions and test data - Update all comments and documentation All tests pass. No backward compatibility concerns per user request. --- audit-cli/commands/analyze/usage/analyzer.go | 130 ++++++------- audit-cli/commands/analyze/usage/output.go | 78 ++++---- audit-cli/commands/analyze/usage/types.go | 68 +++---- audit-cli/commands/analyze/usage/usage.go | 16 +- .../commands/analyze/usage/usage_test.go | 176 +++++++++--------- 5 files changed, 234 insertions(+), 234 deletions(-) diff --git a/audit-cli/commands/analyze/usage/analyzer.go b/audit-cli/commands/analyze/usage/analyzer.go index ebb46d1..e1b152f 100644 --- a/audit-cli/commands/analyze/usage/analyzer.go +++ b/audit-cli/commands/analyze/usage/analyzer.go @@ -12,10 +12,10 @@ import ( "github.com/mongodb/code-example-tooling/audit-cli/internal/rst" ) -// AnalyzeReferences finds all files that reference the target file. +// AnalyzeUsage finds all files that use the target file. // // This function searches through all RST files (.rst, .txt) and YAML files (.yaml, .yml) -// in the source directory to find files that reference the target file using include, +// in the source directory to find files that use the target file through include, // literalinclude, or io-code-block directives. YAML files are included because extract // and release files contain RST directives within their content blocks. // @@ -29,9 +29,9 @@ import ( // - excludePattern: Glob pattern for paths to exclude (empty string means no exclusion) // // Returns: -// - *ReferenceAnalysis: The analysis results +// - *UsageAnalysis: The analysis results // - error: Any error encountered during analysis -func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, excludePattern string) (*ReferenceAnalysis, error) { +func AnalyzeUsage(targetFile string, includeToctree bool, verbose bool, excludePattern string) (*UsageAnalysis, error) { // Check if target file exists if _, err := os.Stat(targetFile); os.IsNotExist(err) { return nil, fmt.Errorf("target file does not exist: %s\n\nPlease check:\n - The file path is correct\n - The file hasn't been moved or deleted\n - You have permission to access the file", targetFile) @@ -50,10 +50,10 @@ func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, exc } // Initialize analysis result - analysis := &ReferenceAnalysis{ - TargetFile: absTargetFile, - SourceDir: sourceDir, - ReferencingFiles: []FileReference{}, + analysis := &UsageAnalysis{ + TargetFile: absTargetFile, + SourceDir: sourceDir, + UsingFiles: []FileUsage{}, } // Track if we found any RST/YAML files @@ -62,7 +62,7 @@ func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, exc // Show progress message if verbose if verbose { - fmt.Fprintf(os.Stderr, "Scanning for references in %s...\n", sourceDir) + fmt.Fprintf(os.Stderr, "Scanning for usages in %s...\n", sourceDir) } // Walk through all RST and YAML files in the source directory @@ -103,16 +103,16 @@ func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, exc fmt.Fprintf(os.Stderr, "Processed %d files...\n", filesProcessed) } - // Search for references in this file - refs, err := findReferencesInFile(path, absTargetFile, sourceDir, includeToctree) + // Search for usages in this file + usages, err := findUsagesInFile(path, absTargetFile, sourceDir, includeToctree) if err != nil { // Log error but continue processing other files fmt.Fprintf(os.Stderr, "Warning: failed to process %s: %v\n", path, err) return nil } - // Add any found references - analysis.ReferencingFiles = append(analysis.ReferencingFiles, refs...) + // Add any found usages + analysis.UsingFiles = append(analysis.UsingFiles, usages...) return nil }) @@ -132,16 +132,16 @@ func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, exc } // Update total counts - analysis.TotalReferences = len(analysis.ReferencingFiles) - analysis.TotalFiles = countUniqueFiles(analysis.ReferencingFiles) + analysis.TotalUsages = len(analysis.UsingFiles) + analysis.TotalFiles = countUniqueFiles(analysis.UsingFiles) return analysis, nil } -// findReferencesInFile searches a single file for references to the target file. +// findUsagesInFile searches a single file for usages of the target file. // // This function scans through the file line by line looking for include, -// literalinclude, and io-code-block directives that reference the target file. +// literalinclude, and io-code-block directives that use the target file. // If includeToctree is true, also searches for toctree entries. // // Parameters: @@ -151,16 +151,16 @@ func AnalyzeReferences(targetFile string, includeToctree bool, verbose bool, exc // - includeToctree: If true, include toctree entries in the search // // Returns: -// - []FileReference: List of references found in this file +// - []FileUsage: List of usages found in this file // - error: Any error encountered during processing -func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree bool) ([]FileReference, error) { +func findUsagesInFile(filePath, targetFile, sourceDir string, includeToctree bool) ([]FileUsage, error) { file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() - var references []FileReference + var usages []FileUsage scanner := bufio.NewScanner(file) lineNum := 0 inIOCodeBlock := false @@ -201,10 +201,10 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree if matches := rst.IncludeDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { - references = append(references, FileReference{ + usages = append(usages, FileUsage{ FilePath: filePath, DirectiveType: "include", - ReferencePath: refPath, + UsagePath: refPath, LineNumber: lineNum, }) } @@ -215,10 +215,10 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree if matches := rst.LiteralIncludeDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { - references = append(references, FileReference{ + usages = append(usages, FileUsage{ FilePath: filePath, DirectiveType: "literalinclude", - ReferencePath: refPath, + UsagePath: refPath, LineNumber: lineNum, }) } @@ -231,10 +231,10 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree if matches := rst.InputDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { - references = append(references, FileReference{ + usages = append(usages, FileUsage{ FilePath: filePath, DirectiveType: "io-code-block", - ReferencePath: refPath, + UsagePath: refPath, LineNumber: ioCodeBlockStartLine, }) } @@ -245,10 +245,10 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree if matches := rst.OutputDirectiveRegex.FindStringSubmatch(trimmedLine); matches != nil { refPath := strings.TrimSpace(matches[1]) if referencesTarget(refPath, targetFile, sourceDir, filePath) { - references = append(references, FileReference{ + usages = append(usages, FileUsage{ FilePath: filePath, DirectiveType: "io-code-block", - ReferencePath: refPath, + UsagePath: refPath, LineNumber: ioCodeBlockStartLine, }) } @@ -267,10 +267,10 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree // Document names can be relative or absolute (starting with /) docName := trimmedLine if referencesToctreeTarget(docName, targetFile, sourceDir, filePath) { - references = append(references, FileReference{ + usages = append(usages, FileUsage{ FilePath: filePath, DirectiveType: "toctree", - ReferencePath: docName, + UsagePath: docName, LineNumber: toctreeStartLine, }) } @@ -281,7 +281,7 @@ func findReferencesInFile(filePath, targetFile, sourceDir string, includeToctree return nil, err } - return references, nil + return usages, nil } // referencesTarget checks if a reference path points to the target file. @@ -345,7 +345,7 @@ func referencesToctreeTarget(docName, targetFile, sourceDir, currentFile string) return resolvedPath == targetFile } -// FilterByDirectiveType filters the analysis results to only include references +// FilterByDirectiveType filters the analysis results to only include usages // of the specified directive type. // // Parameters: @@ -353,75 +353,75 @@ func referencesToctreeTarget(docName, targetFile, sourceDir, currentFile string) // - directiveType: The directive type to filter by (include, literalinclude, io-code-block) // // Returns: -// - *ReferenceAnalysis: A new analysis with filtered results -func FilterByDirectiveType(analysis *ReferenceAnalysis, directiveType string) *ReferenceAnalysis { - filtered := &ReferenceAnalysis{ - TargetFile: analysis.TargetFile, - SourceDir: analysis.SourceDir, - ReferencingFiles: []FileReference{}, - ReferenceTree: analysis.ReferenceTree, +// - *UsageAnalysis: A new analysis with filtered results +func FilterByDirectiveType(analysis *UsageAnalysis, directiveType string) *UsageAnalysis { + filtered := &UsageAnalysis{ + TargetFile: analysis.TargetFile, + SourceDir: analysis.SourceDir, + UsingFiles: []FileUsage{}, + UsageTree: analysis.UsageTree, } - // Filter references - for _, ref := range analysis.ReferencingFiles { - if ref.DirectiveType == directiveType { - filtered.ReferencingFiles = append(filtered.ReferencingFiles, ref) + // Filter usages + for _, usage := range analysis.UsingFiles { + if usage.DirectiveType == directiveType { + filtered.UsingFiles = append(filtered.UsingFiles, usage) } } // Update counts - filtered.TotalReferences = len(filtered.ReferencingFiles) - filtered.TotalFiles = countUniqueFiles(filtered.ReferencingFiles) + filtered.TotalUsages = len(filtered.UsingFiles) + filtered.TotalFiles = countUniqueFiles(filtered.UsingFiles) return filtered } -// countUniqueFiles counts the number of unique files in the reference list. +// countUniqueFiles counts the number of unique files in the usage list. // // Parameters: -// - refs: List of file references +// - usages: List of file usages // // Returns: // - int: Number of unique files -func countUniqueFiles(refs []FileReference) int { +func countUniqueFiles(usages []FileUsage) int { uniqueFiles := make(map[string]bool) - for _, ref := range refs { - uniqueFiles[ref.FilePath] = true + for _, usage := range usages { + uniqueFiles[usage.FilePath] = true } return len(uniqueFiles) } -// GroupReferencesByFile groups references by file path and directive type. +// GroupUsagesByFile groups usages by file path and directive type. // -// This function takes a flat list of references and groups them by file, -// counting how many times each file references the target. +// This function takes a flat list of usages and groups them by file, +// counting how many times each file uses the target. // // Parameters: -// - refs: List of file references +// - usages: List of file usages // // Returns: -// - []GroupedFileReference: List of grouped references, sorted by file path -func GroupReferencesByFile(refs []FileReference) []GroupedFileReference { +// - []GroupedFileUsage: List of grouped usages, sorted by file path +func GroupUsagesByFile(usages []FileUsage) []GroupedFileUsage { // Group by file path and directive type type groupKey struct { filePath string directiveType string } - groups := make(map[groupKey][]FileReference) + groups := make(map[groupKey][]FileUsage) - for _, ref := range refs { - key := groupKey{ref.FilePath, ref.DirectiveType} - groups[key] = append(groups[key], ref) + for _, usage := range usages { + key := groupKey{usage.FilePath, usage.DirectiveType} + groups[key] = append(groups[key], usage) } // Convert to slice - var grouped []GroupedFileReference - for key, refs := range groups { - grouped = append(grouped, GroupedFileReference{ + var grouped []GroupedFileUsage + for key, usages := range groups { + grouped = append(grouped, GroupedFileUsage{ FilePath: key.filePath, DirectiveType: key.directiveType, - References: refs, - Count: len(refs), + Usages: usages, + Count: len(usages), }) } diff --git a/audit-cli/commands/analyze/usage/output.go b/audit-cli/commands/analyze/usage/output.go index 55f14c2..6af9fa0 100644 --- a/audit-cli/commands/analyze/usage/output.go +++ b/audit-cli/commands/analyze/usage/output.go @@ -25,7 +25,7 @@ const ( // - analysis: The analysis results to print // - format: The output format (text or json) // - verbose: If true, show additional details -func PrintAnalysis(analysis *ReferenceAnalysis, format OutputFormat, verbose bool) error { +func PrintAnalysis(analysis *UsageAnalysis, format OutputFormat, verbose bool) error { switch format { case FormatJSON: return printJSON(analysis) @@ -38,17 +38,17 @@ func PrintAnalysis(analysis *ReferenceAnalysis, format OutputFormat, verbose boo } // printText prints the analysis results in human-readable text format. -func printText(analysis *ReferenceAnalysis, verbose bool) { +func printText(analysis *UsageAnalysis, verbose bool) { fmt.Println("============================================================") fmt.Println("USAGE ANALYSIS") fmt.Println("============================================================") fmt.Printf("Target File: %s\n", analysis.TargetFile) fmt.Printf("Total Files: %d\n", analysis.TotalFiles) - fmt.Printf("Total Usages: %d\n", analysis.TotalReferences) + fmt.Printf("Total Usages: %d\n", analysis.TotalUsages) fmt.Println("============================================================") fmt.Println() - if analysis.TotalReferences == 0 { + if analysis.TotalUsages == 0 { fmt.Println("No files use this file.") fmt.Println() fmt.Println("This could mean:") @@ -62,8 +62,8 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { return } - // Group references by directive type - byDirectiveType := groupByDirectiveType(analysis.ReferencingFiles) + // Group usages by directive type + byDirectiveType := groupByDirectiveType(analysis.UsingFiles) // Print breakdown by directive type with file and reference counts directiveTypes := []string{"include", "literalinclude", "io-code-block", "toctree"} @@ -86,10 +86,10 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { } fmt.Println() - // Group references by file - grouped := GroupReferencesByFile(analysis.ReferencingFiles) + // Group usages by file + grouped := GroupUsagesByFile(analysis.UsingFiles) - // Print detailed list of referencing files + // Print detailed list of files using the target for i, group := range grouped { // Get relative path from source directory for cleaner output relPath, err := filepath.Rel(analysis.SourceDir, group.FilePath) @@ -108,8 +108,8 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { // Print line numbers in verbose mode if verbose { - for _, ref := range group.References { - fmt.Printf(" Line %d: %s\n", ref.LineNumber, ref.ReferencePath) + for _, usage := range group.Usages { + fmt.Printf(" Line %d: %s\n", usage.LineNumber, usage.UsagePath) } } } @@ -118,20 +118,20 @@ func printText(analysis *ReferenceAnalysis, verbose bool) { } // printJSON prints the analysis results in JSON format. -func printJSON(analysis *ReferenceAnalysis) error { +func printJSON(analysis *UsageAnalysis) error { // Create a JSON-friendly structure output := struct { - TargetFile string `json:"target_file"` - SourceDir string `json:"source_dir"` - TotalFiles int `json:"total_files"` - TotalReferences int `json:"total_references"` - ReferencingFiles []FileReference `json:"referencing_files"` + TargetFile string `json:"target_file"` + SourceDir string `json:"source_dir"` + TotalFiles int `json:"total_files"` + TotalUsages int `json:"total_usages"` + UsingFiles []FileUsage `json:"using_files"` }{ - TargetFile: analysis.TargetFile, - SourceDir: analysis.SourceDir, - TotalFiles: analysis.TotalFiles, - TotalReferences: analysis.TotalReferences, - ReferencingFiles: analysis.ReferencingFiles, + TargetFile: analysis.TargetFile, + SourceDir: analysis.SourceDir, + TotalFiles: analysis.TotalFiles, + TotalUsages: analysis.TotalUsages, + UsingFiles: analysis.UsingFiles, } encoder := json.NewEncoder(os.Stdout) @@ -139,12 +139,12 @@ func printJSON(analysis *ReferenceAnalysis) error { return encoder.Encode(output) } -// groupByDirectiveType groups references by their directive type. -func groupByDirectiveType(refs []FileReference) map[string][]FileReference { - groups := make(map[string][]FileReference) +// groupByDirectiveType groups usages by their directive type. +func groupByDirectiveType(usages []FileUsage) map[string][]FileUsage { + groups := make(map[string][]FileUsage) - for _, ref := range refs { - groups[ref.DirectiveType] = append(groups[ref.DirectiveType], ref) + for _, usage := range usages { + groups[usage.DirectiveType] = append(groups[usage.DirectiveType], usage) } return groups @@ -193,16 +193,16 @@ func GetDirectiveTypeLabel(directiveType string) string { // // Returns: // - error: Any error encountered during printing -func PrintPathsOnly(analysis *ReferenceAnalysis) error { +func PrintPathsOnly(analysis *UsageAnalysis) error { // Get unique file paths (in case there are duplicates) seen := make(map[string]bool) var paths []string - for _, ref := range analysis.ReferencingFiles { + for _, usage := range analysis.UsingFiles { // Get relative path from source directory for cleaner output - relPath, err := filepath.Rel(analysis.SourceDir, ref.FilePath) + relPath, err := filepath.Rel(analysis.SourceDir, usage.FilePath) if err != nil { - relPath = ref.FilePath + relPath = usage.FilePath } if !seen[relPath] { @@ -231,22 +231,22 @@ func PrintPathsOnly(analysis *ReferenceAnalysis) error { // // Returns: // - error: Any error encountered during printing -func PrintSummary(analysis *ReferenceAnalysis) error { +func PrintSummary(analysis *UsageAnalysis) error { fmt.Printf("Total Files: %d\n", analysis.TotalFiles) - fmt.Printf("Total Usages: %d\n", analysis.TotalReferences) + fmt.Printf("Total Usages: %d\n", analysis.TotalUsages) - if analysis.TotalReferences > 0 { + if analysis.TotalUsages > 0 { // Group by directive type - byDirectiveType := groupByDirectiveType(analysis.ReferencingFiles) + byDirectiveType := groupByDirectiveType(analysis.UsingFiles) // Print breakdown by type fmt.Println("\nBy Type:") directiveTypes := []string{"include", "literalinclude", "io-code-block", "toctree"} for _, directiveType := range directiveTypes { - if refs, ok := byDirectiveType[directiveType]; ok { - uniqueFiles := countUniqueFiles(refs) - totalRefs := len(refs) - fmt.Printf(" %-20s: %d files, %d usages\n", directiveType, uniqueFiles, totalRefs) + if usages, ok := byDirectiveType[directiveType]; ok { + uniqueFiles := countUniqueFiles(usages) + totalUsages := len(usages) + fmt.Printf(" %-20s: %d files, %d usages\n", directiveType, uniqueFiles, totalUsages) } } } diff --git a/audit-cli/commands/analyze/usage/types.go b/audit-cli/commands/analyze/usage/types.go index 007813f..7e36694 100644 --- a/audit-cli/commands/analyze/usage/types.go +++ b/audit-cli/commands/analyze/usage/types.go @@ -1,82 +1,82 @@ package usage -// ReferenceAnalysis contains the results of analyzing which files reference a target file. +// UsageAnalysis contains the results of analyzing which files use a target file. // -// This structure holds both a flat list of referencing files and a hierarchical -// tree structure showing the reference relationships. -type ReferenceAnalysis struct { +// This structure holds both a flat list of files that use the target and a hierarchical +// tree structure showing the usage relationships. +type UsageAnalysis struct { // TargetFile is the absolute path to the file being analyzed TargetFile string - // ReferencingFiles is a flat list of all files that reference the target - ReferencingFiles []FileReference + // UsingFiles is a flat list of all files that use the target + UsingFiles []FileUsage - // ReferenceTree is a hierarchical tree structure of references - // (for future use - showing nested references) - ReferenceTree *ReferenceNode + // UsageTree is a hierarchical tree structure of usages + // (for future use - showing nested usages) + UsageTree *UsageNode - // TotalReferences is the total number of directive occurrences - TotalReferences int + // TotalUsages is the total number of directive occurrences + TotalUsages int - // TotalFiles is the total number of unique files that reference the target + // TotalFiles is the total number of unique files that use the target TotalFiles int // SourceDir is the source directory that was searched SourceDir string } -// FileReference represents a single file that references the target file. +// FileUsage represents a single file that uses the target file. // -// This structure captures details about how and where the reference occurs. -type FileReference struct { - // FilePath is the absolute path to the file that references the target +// This structure captures details about how and where the usage occurs. +type FileUsage struct { + // FilePath is the absolute path to the file that uses the target FilePath string `json:"file_path"` // DirectiveType is the type of directive used to reference the file // Possible values: "include", "literalinclude", "io-code-block", "toctree" DirectiveType string `json:"directive_type"` - // ReferencePath is the path used in the directive (as written in the file) - ReferencePath string `json:"reference_path"` + // UsagePath is the path used in the directive (as written in the file) + UsagePath string `json:"usage_path"` - // LineNumber is the line number where the reference occurs + // LineNumber is the line number where the usage occurs LineNumber int `json:"line_number"` } -// ReferenceNode represents a node in the reference tree. +// UsageNode represents a node in the usage tree. // -// This structure is used to build a hierarchical view of references, -// showing which files reference the target and which files reference those files. -type ReferenceNode struct { +// This structure is used to build a hierarchical view of usages, +// showing which files use the target and which files use those files. +type UsageNode struct { // FilePath is the absolute path to this file FilePath string // DirectiveType is the type of directive used to reference the file DirectiveType string - // ReferencePath is the path used in the directive - ReferencePath string + // UsagePath is the path used in the directive + UsagePath string // Children are files that include this file - // (for building nested reference trees) - Children []*ReferenceNode + // (for building nested usage trees) + Children []*UsageNode } -// GroupedFileReference represents a file with all its references to the target. +// GroupedFileUsage represents a file with all its usages of the target. // -// This structure groups multiple references from the same file together, -// showing how many times a file references the target and where. -type GroupedFileReference struct { +// This structure groups multiple usages from the same file together, +// showing how many times a file uses the target and where. +type GroupedFileUsage struct { // FilePath is the absolute path to the file FilePath string // DirectiveType is the type of directive used DirectiveType string - // References is the list of all references from this file - References []FileReference + // Usages is the list of all usages from this file + Usages []FileUsage - // Count is the number of references from this file + // Count is the number of usages from this file Count int } diff --git a/audit-cli/commands/analyze/usage/usage.go b/audit-cli/commands/analyze/usage/usage.go index 8b20778..94198e4 100644 --- a/audit-cli/commands/analyze/usage/usage.go +++ b/audit-cli/commands/analyze/usage/usage.go @@ -106,15 +106,15 @@ Examples: analyze usage /path/to/file.rst --directive-type include`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runReferences(args[0], format, verbose, countOnly, pathsOnly, summaryOnly, directiveType, includeToctree, excludePattern) + return runUsage(args[0], format, verbose, countOnly, pathsOnly, summaryOnly, directiveType, includeToctree, excludePattern) }, } cmd.Flags().StringVar(&format, "format", "text", "Output format (text or json)") cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed information including line numbers") - cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of references") + cmd.Flags().BoolVarP(&countOnly, "count-only", "c", false, "Only show the count of usages") cmd.Flags().BoolVar(&pathsOnly, "paths-only", false, "Only show the file paths (one per line)") - cmd.Flags().BoolVar(&summaryOnly, "summary", false, "Only show summary statistics (total files and references by type)") + cmd.Flags().BoolVar(&summaryOnly, "summary", false, "Only show summary statistics (total files and usages by type)") cmd.Flags().StringVarP(&directiveType, "directive-type", "t", "", "Filter by directive type (include, literalinclude, io-code-block, toctree)") cmd.Flags().BoolVar(&includeToctree, "include-toctree", false, "Include toctree entries (navigation links) in addition to content inclusion directives") cmd.Flags().StringVar(&excludePattern, "exclude", "", "Exclude paths matching this glob pattern (e.g., '*/archive/*' or '*/deprecated/*')") @@ -122,7 +122,7 @@ Examples: return cmd } -// runReferences executes the references analysis. +// runUsage executes the usage analysis. // // This function performs the analysis and prints the results in the specified format. // @@ -139,7 +139,7 @@ Examples: // // Returns: // - error: Any error encountered during analysis -func runReferences(targetFile, format string, verbose, countOnly, pathsOnly, summaryOnly bool, directiveType string, includeToctree bool, excludePattern string) error { +func runUsage(targetFile, format string, verbose, countOnly, pathsOnly, summaryOnly bool, directiveType string, includeToctree bool, excludePattern string) error { // Validate directive type if specified if directiveType != "" { validTypes := map[string]bool{ @@ -178,9 +178,9 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly, sum } // Perform analysis - analysis, err := AnalyzeReferences(targetFile, includeToctree, verbose, excludePattern) + analysis, err := AnalyzeUsage(targetFile, includeToctree, verbose, excludePattern) if err != nil { - return fmt.Errorf("failed to analyze references: %w", err) + return fmt.Errorf("failed to analyze usage: %w", err) } // Filter by directive type if specified @@ -190,7 +190,7 @@ func runReferences(targetFile, format string, verbose, countOnly, pathsOnly, sum // Handle count-only output if countOnly { - fmt.Println(analysis.TotalReferences) + fmt.Println(analysis.TotalUsages) return nil } diff --git a/audit-cli/commands/analyze/usage/usage_test.go b/audit-cli/commands/analyze/usage/usage_test.go index 29cf7eb..5c67413 100644 --- a/audit-cli/commands/analyze/usage/usage_test.go +++ b/audit-cli/commands/analyze/usage/usage_test.go @@ -5,39 +5,39 @@ import ( "testing" ) -// TestAnalyzeReferences tests the AnalyzeReferences function with various scenarios. -func TestAnalyzeReferences(t *testing.T) { +// TestAnalyzeUsage tests the AnalyzeUsage function with various scenarios. +func TestAnalyzeUsage(t *testing.T) { // Get the testdata directory testDataDir := "../../../testdata/input-files/source" tests := []struct { name string targetFile string - expectedReferences int + expectedUsages int expectedDirectiveType string }{ { - name: "Include file with multiple references", + name: "Include file with multiple usages", targetFile: "includes/intro.rst", - expectedReferences: 5, // 4 RST files + 1 YAML file (no toctree by default) + expectedUsages: 5, // 4 RST files + 1 YAML file (no toctree by default) expectedDirectiveType: "include", }, { name: "Code example with literalinclude", targetFile: "code-examples/example.py", - expectedReferences: 2, // 1 RST file + 1 YAML file + expectedUsages: 2, // 1 RST file + 1 YAML file expectedDirectiveType: "literalinclude", }, { name: "Code example with multiple directive types", targetFile: "code-examples/example.js", - expectedReferences: 2, // literalinclude + io-code-block + expectedUsages: 2, // literalinclude + io-code-block expectedDirectiveType: "", // mixed types }, { - name: "File with no references", + name: "File with no usages", targetFile: "code-block-test.rst", - expectedReferences: 0, + expectedUsages: 0, expectedDirectiveType: "", }, } @@ -52,26 +52,26 @@ func TestAnalyzeReferences(t *testing.T) { } // Run analysis (without toctree by default, not verbose, no exclude pattern) - analysis, err := AnalyzeReferences(absTargetPath, false, false, "") + analysis, err := AnalyzeUsage(absTargetPath, false, false, "") if err != nil { - t.Fatalf("AnalyzeReferences failed: %v", err) + t.Fatalf("AnalyzeUsage failed: %v", err) } - // Check total references - if analysis.TotalReferences != tt.expectedReferences { - t.Errorf("expected %d references, got %d", tt.expectedReferences, analysis.TotalReferences) + // Check total usages + if analysis.TotalUsages != tt.expectedUsages { + t.Errorf("expected %d usages, got %d", tt.expectedUsages, analysis.TotalUsages) } - // Check that we got the expected number of referencing files - if len(analysis.ReferencingFiles) != tt.expectedReferences { - t.Errorf("expected %d referencing files, got %d", tt.expectedReferences, len(analysis.ReferencingFiles)) + // Check that we got the expected number of files using the target + if len(analysis.UsingFiles) != tt.expectedUsages { + t.Errorf("expected %d using files, got %d", tt.expectedUsages, len(analysis.UsingFiles)) } // If we expect a specific directive type, check it - if tt.expectedDirectiveType != "" && tt.expectedReferences > 0 { + if tt.expectedDirectiveType != "" && tt.expectedUsages > 0 { foundExpectedType := false - for _, ref := range analysis.ReferencingFiles { - if ref.DirectiveType == tt.expectedDirectiveType { + for _, usage := range analysis.UsingFiles { + if usage.DirectiveType == tt.expectedDirectiveType { foundExpectedType = true break } @@ -94,66 +94,66 @@ func TestAnalyzeReferences(t *testing.T) { } } -// TestFindReferencesInFile tests the findReferencesInFile function. -func TestFindReferencesInFile(t *testing.T) { +// TestFindUsagesInFile tests the findUsagesInFile function. +func TestFindUsagesInFile(t *testing.T) { testDataDir := "../../../testdata/input-files/source" sourceDir := testDataDir tests := []struct { - name string - searchFile string - targetFile string - expectedReferences int - expectedDirective string - includeToctree bool + name string + searchFile string + targetFile string + expectedUsages int + expectedDirective string + includeToctree bool }{ { - name: "Include directive", - searchFile: "include-test.rst", - targetFile: "includes/intro.rst", - expectedReferences: 1, - expectedDirective: "include", - includeToctree: false, + name: "Include directive", + searchFile: "include-test.rst", + targetFile: "includes/intro.rst", + expectedUsages: 1, + expectedDirective: "include", + includeToctree: false, }, { - name: "Literalinclude directive", - searchFile: "literalinclude-test.rst", - targetFile: "code-examples/example.py", - expectedReferences: 1, - expectedDirective: "literalinclude", - includeToctree: false, + name: "Literalinclude directive", + searchFile: "literalinclude-test.rst", + targetFile: "code-examples/example.py", + expectedUsages: 1, + expectedDirective: "literalinclude", + includeToctree: false, }, { - name: "IO code block directive", - searchFile: "io-code-block-test.rst", - targetFile: "code-examples/example.js", - expectedReferences: 1, - expectedDirective: "io-code-block", - includeToctree: false, + name: "IO code block directive", + searchFile: "io-code-block-test.rst", + targetFile: "code-examples/example.js", + expectedUsages: 1, + expectedDirective: "io-code-block", + includeToctree: false, }, { - name: "Duplicate includes", - searchFile: "duplicate-include-test.rst", - targetFile: "includes/intro.rst", - expectedReferences: 2, // Same file included twice - expectedDirective: "include", - includeToctree: false, + name: "Duplicate includes", + searchFile: "duplicate-include-test.rst", + targetFile: "includes/intro.rst", + expectedUsages: 2, // Same file included twice + expectedDirective: "include", + includeToctree: false, }, { - name: "Toctree directive", - searchFile: "index.rst", - targetFile: "include-test.rst", - expectedReferences: 1, - expectedDirective: "toctree", - includeToctree: true, // Must enable toctree flag + name: "Toctree directive", + searchFile: "index.rst", + targetFile: "include-test.rst", + expectedUsages: 1, + expectedDirective: "toctree", + includeToctree: true, // Must enable toctree flag }, { - name: "No references", - searchFile: "code-block-test.rst", - targetFile: "includes/intro.rst", - expectedReferences: 0, - expectedDirective: "", - includeToctree: false, + name: "No usages", + searchFile: "code-block-test.rst", + targetFile: "includes/intro.rst", + expectedUsages: 0, + expectedDirective: "", + includeToctree: false, }, } @@ -176,37 +176,37 @@ func TestFindReferencesInFile(t *testing.T) { t.Fatalf("failed to get absolute source dir: %v", err) } - refs, err := findReferencesInFile(absSearchPath, absTargetPath, absSourceDir, tt.includeToctree) + usages, err := findUsagesInFile(absSearchPath, absTargetPath, absSourceDir, tt.includeToctree) if err != nil { - t.Fatalf("findReferencesInFile failed: %v", err) + t.Fatalf("findUsagesInFile failed: %v", err) } - if len(refs) != tt.expectedReferences { - t.Errorf("expected %d references, got %d", tt.expectedReferences, len(refs)) + if len(usages) != tt.expectedUsages { + t.Errorf("expected %d usages, got %d", tt.expectedUsages, len(usages)) } - // Check directive type if we expect references - if tt.expectedReferences > 0 && tt.expectedDirective != "" { - for _, ref := range refs { - if ref.DirectiveType != tt.expectedDirective { - t.Errorf("expected directive type %s, got %s", tt.expectedDirective, ref.DirectiveType) + // Check directive type if we expect usages + if tt.expectedUsages > 0 && tt.expectedDirective != "" { + for _, usage := range usages { + if usage.DirectiveType != tt.expectedDirective { + t.Errorf("expected directive type %s, got %s", tt.expectedDirective, usage.DirectiveType) } } } - // Verify all references have required fields - for _, ref := range refs { - if ref.FilePath == "" { - t.Error("reference should have a file path") + // Verify all usages have required fields + for _, usage := range usages { + if usage.FilePath == "" { + t.Error("usage should have a file path") } - if ref.DirectiveType == "" { - t.Error("reference should have a directive type") + if usage.DirectiveType == "" { + t.Error("usage should have a directive type") } - if ref.ReferencePath == "" { - t.Error("reference should have a reference path") + if usage.UsagePath == "" { + t.Error("usage should have a usage path") } - if ref.LineNumber == 0 { - t.Error("reference should have a line number") + if usage.LineNumber == 0 { + t.Error("usage should have a line number") } } }) @@ -272,7 +272,7 @@ func TestReferencesTarget(t *testing.T) { // TestGroupByDirectiveType tests the groupByDirectiveType function. func TestGroupByDirectiveType(t *testing.T) { - refs := []FileReference{ + usages := []FileUsage{ {DirectiveType: "include", FilePath: "file1.rst"}, {DirectiveType: "include", FilePath: "file2.rst"}, {DirectiveType: "literalinclude", FilePath: "file3.rst"}, @@ -280,7 +280,7 @@ func TestGroupByDirectiveType(t *testing.T) { {DirectiveType: "include", FilePath: "file5.rst"}, } - groups := groupByDirectiveType(refs) + groups := groupByDirectiveType(usages) // Check that we have 3 groups if len(groups) != 3 { @@ -289,17 +289,17 @@ func TestGroupByDirectiveType(t *testing.T) { // Check include group if len(groups["include"]) != 3 { - t.Errorf("expected 3 include references, got %d", len(groups["include"])) + t.Errorf("expected 3 include usages, got %d", len(groups["include"])) } // Check literalinclude group if len(groups["literalinclude"]) != 1 { - t.Errorf("expected 1 literalinclude reference, got %d", len(groups["literalinclude"])) + t.Errorf("expected 1 literalinclude usage, got %d", len(groups["literalinclude"])) } // Check io-code-block group if len(groups["io-code-block"]) != 1 { - t.Errorf("expected 1 io-code-block reference, got %d", len(groups["io-code-block"])) + t.Errorf("expected 1 io-code-block usage, got %d", len(groups["io-code-block"])) } } From 9f17c8b1cb27b033d229164f8f17d3ebaf8c1178 Mon Sep 17 00:00:00 2001 From: Cory Bullinger Date: Thu, 20 Nov 2025 08:17:32 -0500 Subject: [PATCH 14/14] Update README example output to use 'usage' terminology - Change 'REFERENCE ANALYSIS' header to 'USAGE ANALYSIS' - Update '4 references' to '4 usages' in example output - Update JSON field names in examples: - total_references -> total_usages - referencing_files -> using_files - reference_path -> usage_path - Update command examples to use 'usages' instead of 'references' - Update jq example to query '.total_usages' instead of '.total_references' --- audit-cli/README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/audit-cli/README.md b/audit-cli/README.md index 3dfdbbf..b057ae5 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -396,16 +396,16 @@ With `--include-toctree`, also tracks: **Text** (default): ``` ============================================================ -REFERENCE ANALYSIS +USAGE ANALYSIS ============================================================ Target File: /path/to/includes/intro.rst Total Files: 3 Total Usages: 4 ============================================================ -include : 3 files, 4 references +include : 3 files, 4 usages - 1. [include] duplicate-include-test.rst (2 references) + 1. [include] duplicate-include-test.rst (2 usages) 2. [include] include-test.rst 3. [include] page.rst @@ -414,16 +414,16 @@ include : 3 files, 4 references **Text with --verbose:** ``` ============================================================ -REFERENCE ANALYSIS +USAGE ANALYSIS ============================================================ Target File: /path/to/includes/intro.rst Total Files: 3 Total Usages: 4 ============================================================ -include : 3 files, 4 references +include : 3 files, 4 usages - 1. [include] duplicate-include-test.rst (2 references) + 1. [include] duplicate-include-test.rst (2 usages) Line 6: /includes/intro.rst Line 13: /includes/intro.rst 2. [include] include-test.rst @@ -439,24 +439,24 @@ include : 3 files, 4 references "target_file": "/path/to/includes/intro.rst", "source_dir": "/path/to/source", "total_files": 3, - "total_references": 4, - "referencing_files": [ + "total_usages": 4, + "using_files": [ { "file_path": "/path/to/duplicate-include-test.rst", "directive_type": "include", - "reference_path": "/includes/intro.rst", + "usage_path": "/includes/intro.rst", "line_number": 6 }, { "file_path": "/path/to/duplicate-include-test.rst", "directive_type": "include", - "reference_path": "/includes/intro.rst", + "usage_path": "/includes/intro.rst", "line_number": 13 }, { "file_path": "/path/to/include-test.rst", "directive_type": "include", - "reference_path": "/includes/intro.rst", + "usage_path": "/includes/intro.rst", "line_number": 6 } ] @@ -473,7 +473,7 @@ include : 3 files, 4 references ./audit-cli analyze usage ~/docs/source/code-examples/connect.py # Get machine-readable output for scripting -./audit-cli analyze usage ~/docs/source/includes/fact.rst --format json | jq '.total_references' +./audit-cli analyze usage ~/docs/source/includes/fact.rst --format json | jq '.total_usages' # See exactly where a file is referenced (with line numbers) ./audit-cli analyze usage ~/docs/source/includes/intro.rst --verbose @@ -501,10 +501,10 @@ include : 3 files, 4 references # Filter to only show include directives (not literalinclude or io-code-block) ./audit-cli analyze usage ~/docs/source/includes/fact.rst --directive-type include -# Filter to only show literalinclude references +# Filter to only show literalinclude usages ./audit-cli analyze usage ~/docs/source/code-examples/example.py --directive-type literalinclude -# Combine filters: count only literalinclude references +# Combine filters: count only literalinclude usages ./audit-cli analyze usage ~/docs/source/code-examples/example.py -t literalinclude -c # Combine filters: list files that use this as an io-code-block