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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 340 additions & 68 deletions audit-cli/README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions audit-cli/commands/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
// Currently supports:
// - includes: Analyze include directive relationships in RST files
// - usage: Find all files that use a target file
// - procedures: Analyze procedure variations and statistics
//
// 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/procedures"
"github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/usage"
"github.com/spf13/cobra"
)
Expand All @@ -27,13 +29,15 @@ func NewAnalyzeCommand() *cobra.Command {
Currently supports:
- includes: Analyze include directive relationships (forward dependencies)
- usage: Find all files that use a target file (reverse dependencies)
- procedures: Analyze procedure variations and statistics

Future subcommands may support analyzing cross-references, broken links, or content metrics.`,
}

// Add subcommands
cmd.AddCommand(includes.NewIncludesCommand())
cmd.AddCommand(usage.NewUsageCommand())
cmd.AddCommand(procedures.NewProceduresCommand())

return cmd
}
Expand Down
144 changes: 144 additions & 0 deletions audit-cli/commands/analyze/procedures/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package procedures

import (
"fmt"

"github.com/mongodb/code-example-tooling/audit-cli/internal/rst"
)

// AnalyzeFile analyzes procedures in a file and returns a report.
//
// This function parses all procedures from the file and generates analysis
// information including variation counts, step counts, implementation types,
// and sub-procedure detection.
//
// This function expands include directives to properly detect variations that
// may be defined in included files.
//
// Parameters:
// - filePath: Path to the RST file to analyze
//
// Returns:
// - *AnalysisReport: Analysis report containing all procedure information
// - error: Any error encountered during analysis
func AnalyzeFile(filePath string) (*AnalysisReport, error) {
return AnalyzeFileWithOptions(filePath, true)
}

// AnalyzeFileWithOptions analyzes procedures in a file with options and returns a report.
//
// This function parses all procedures from the file and generates analysis
// information including variation counts, step counts, implementation types,
// and sub-procedure detection.
//
// Parameters:
// - filePath: Path to the RST file to analyze
// - expandIncludes: If true, expands include directives inline
//
// Returns:
// - *AnalysisReport: Analysis report containing all procedure information
// - error: Any error encountered during analysis
func AnalyzeFileWithOptions(filePath string, expandIncludes bool) (*AnalysisReport, error) {
// Parse all procedures from the file
procedures, err := rst.ParseProceduresWithOptions(filePath, expandIncludes)
if err != nil {
return nil, fmt.Errorf("failed to parse procedures from %s: %w", filePath, err)
}

// Create the report
report := NewAnalysisReport(filePath)

// Group procedures from the same tab set
// Track which tab sets we've already processed
processedTabSets := make(map[*rst.TabSetInfo]bool)

for _, procedure := range procedures {
// If this procedure is part of a tab set and we haven't processed it yet
if procedure.TabSet != nil && !processedTabSets[procedure.TabSet] {
// Mark this tab set as processed
processedTabSets[procedure.TabSet] = true

// Create a grouped analysis for all procedures in this tab set
analysis := analyzeTabSet(procedure.TabSet)
report.AddProcedure(analysis)
} else if procedure.TabSet == nil {
// Regular procedure (not part of a tab set)
analysis := analyzeProcedure(procedure)
report.AddProcedure(analysis)
}
// Skip procedures that are part of an already-processed tab set
}

return report, nil
}

// analyzeProcedure analyzes a single procedure and returns analysis results.
func analyzeProcedure(procedure rst.Procedure) ProcedureAnalysis {
// Get variations
variations := rst.GetProcedureVariations(procedure)

// If no variations, count as 1 (single variation)
variationCount := len(variations)
if variationCount == 0 {
variationCount = 1
variations = []string{"(no variations)"}
}

// Count steps
stepCount := len(procedure.Steps)

// Determine implementation type
implementation := string(procedure.Type)

// Check for sub-steps
hasSubSteps := procedure.HasSubSteps

return ProcedureAnalysis{
Procedure: procedure,
Variations: variations,
VariationCount: variationCount,
StepCount: stepCount,
HasSubSteps: hasSubSteps,
Implementation: implementation,
}
}

// analyzeTabSet analyzes a tab set containing multiple procedure variations.
// This groups all procedures from the same tab set for reporting purposes.
func analyzeTabSet(tabSet *rst.TabSetInfo) ProcedureAnalysis {
// Use the first procedure as the representative
// (they all have the same title/heading)
var firstProc rst.Procedure
for _, tabID := range tabSet.TabIDs {
if proc, ok := tabSet.Procedures[tabID]; ok {
firstProc = proc
break
}
}

// Get all tab IDs as variations
variations := tabSet.TabIDs

// Count total variations
variationCount := len(variations)

// Use the step count from the first procedure
// (each tab may have different step counts, but we report the first one)
stepCount := len(firstProc.Steps)

// Determine implementation type
implementation := string(firstProc.Type)

// Check for sub-steps
hasSubSteps := firstProc.HasSubSteps

return ProcedureAnalysis{
Procedure: firstProc,
Variations: variations,
VariationCount: variationCount,
StepCount: stepCount,
HasSubSteps: hasSubSteps,
Implementation: implementation,
}
}

197 changes: 197 additions & 0 deletions audit-cli/commands/analyze/procedures/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package procedures

import (
"fmt"
"strings"
)

// OutputOptions controls what information is displayed in the output.
type OutputOptions struct {
ListAll bool // List all variations with their selection/tabid values
ListSummary bool // List procedures grouped by heading without selection details
Implementation bool // Show how each procedure is implemented
SubProcedures bool // Indicate if procedures contain nested sub-procedures
StepCount bool // Show step count for each procedure
}

// PrintReport prints the analysis report to stdout based on the output options.
func PrintReport(report *AnalysisReport, options OutputOptions) {
// If no special options are set, just print the count
if !options.ListAll && !options.ListSummary && !options.Implementation && !options.SubProcedures && !options.StepCount {
printSummary(report)
return
}

// Print detailed report
printDetailedReport(report, options)
}

// groupProceduresByHeading groups procedures by their heading and returns the groups and order.
func groupProceduresByHeading(procedures []ProcedureAnalysis) (map[string][]ProcedureAnalysis, []string) {
headingGroups := make(map[string][]ProcedureAnalysis)
headingOrder := []string{}

for _, analysis := range procedures {
heading := analysis.Procedure.Title
if heading == "" {
heading = "(Untitled)"
}

if _, exists := headingGroups[heading]; !exists {
headingOrder = append(headingOrder, heading)
}
headingGroups[heading] = append(headingGroups[heading], analysis)
}

return headingGroups, headingOrder
}

// calculateTotals calculates total unique procedures and appearances from grouped data.
func calculateTotals(headingGroups map[string][]ProcedureAnalysis) (int, int) {
totalUniqueProcedures := 0
totalAppearances := 0

for _, procedures := range headingGroups {
totalUniqueProcedures += len(procedures)
for _, proc := range procedures {
totalAppearances += proc.VariationCount
}
}

return totalUniqueProcedures, totalAppearances
}

// printSummary prints a summary of the analysis.
func printSummary(report *AnalysisReport) {
fmt.Printf("File: %s\n", report.FilePath)
fmt.Printf("Total unique procedures: %d\n", len(report.Procedures))
fmt.Printf("Total procedure appearances: %d\n", report.TotalVariations)
}

// printDetailedReport prints a detailed analysis report.
func printDetailedReport(report *AnalysisReport, options OutputOptions) {
fmt.Printf("Procedure Analysis for: %s\n", report.FilePath)
fmt.Println(strings.Repeat("=", 80))

// Group procedures by heading first to get accurate counts
headingGroups, headingOrder := groupProceduresByHeading(report.Procedures)
totalUniqueProcedures, totalAppearances := calculateTotals(headingGroups)

fmt.Printf("\nTotal unique procedures: %d\n", totalUniqueProcedures)
fmt.Printf("Total procedure appearances: %d\n\n", totalAppearances)

// Print implementation type summary if requested
if options.Implementation {
fmt.Println("Procedures by implementation type:")
for implType, count := range report.ProceduresByType {
fmt.Printf(" - %s: %d\n", implType, count)
}
fmt.Println()
}

// Print details grouped by heading (headingGroups already created above)
fmt.Println("Procedures by Heading:")
fmt.Println(strings.Repeat("-", 80))

headingNum := 1
for _, heading := range headingOrder {
procedures := headingGroups[heading]

fmt.Printf("\n%d. %s\n", headingNum, heading)
fmt.Printf(" Unique procedures: %d\n", len(procedures))

// Calculate total appearances for this heading
totalAppearances := 0
for _, proc := range procedures {
totalAppearances += proc.VariationCount
}
fmt.Printf(" Total appearances: %d\n", totalAppearances)

// If only showing summary, skip the individual procedure details
if options.ListSummary && !options.ListAll {
headingNum++
continue
}

// Determine if we need sub-numbering (only when there are multiple unique procedures)
useSubNumbering := len(procedures) > 1

// Show each unique procedure under this heading
for i, analysis := range procedures {
fmt.Printf("\n ")

// Only show sub-numbering if there are multiple unique procedures
if useSubNumbering {
fmt.Printf("%d.%d. ", headingNum, i+1)
}

// Show the first step to distinguish procedures (only if there are multiple)
if useSubNumbering {
if len(analysis.Procedure.Steps) > 0 && analysis.Procedure.Steps[0].Title != "" {
fmt.Printf("%s\n", analysis.Procedure.Steps[0].Title)
} else if len(analysis.Procedure.Steps) > 0 {
fmt.Printf("(Untitled first step)\n")
} else {
fmt.Printf("(No steps)\n")
}
} else {
// For single procedures, just show the step count
fmt.Printf("Steps: %d\n", len(analysis.Procedure.Steps))
}

// Indent based on whether we're using sub-numbering
indent := " "
if !useSubNumbering {
indent = " "
}

// Only show step count if we already showed the first step title
if useSubNumbering {
fmt.Printf("%sSteps: %d\n", indent, len(analysis.Procedure.Steps))
}

// Print implementation type if requested
if options.Implementation {
fmt.Printf("%sImplementation: %s\n", indent, analysis.Implementation)
}

// Print sub-procedures flag if requested
if options.SubProcedures {
if analysis.HasSubSteps {
fmt.Printf("%sContains sub-procedures: yes\n", indent)
} else {
fmt.Printf("%sContains sub-procedures: no\n", indent)
}
}

// Print selections if requested
if options.ListAll {
if analysis.VariationCount == 1 {
fmt.Printf("%sAppears in 1 selection:\n", indent)
} else {
fmt.Printf("%sAppears in %d selections:\n", indent, analysis.VariationCount)
}

if len(analysis.Variations) > 0 && analysis.Variations[0] != "(no variations)" {
for _, variation := range analysis.Variations {
fmt.Printf("%s - %s\n", indent, variation)
}
} else {
fmt.Printf("%s (single variation, no tabs or selections)\n", indent)
}
} else if options.ListSummary {
// For summary, just show the count without listing all selections
if analysis.VariationCount == 1 {
fmt.Printf("%sAppears in 1 selection\n", indent)
} else {
fmt.Printf("%sAppears in %d selections\n", indent, analysis.VariationCount)
}
}
}

headingNum++
}

fmt.Println()
}

Loading