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

Commit c057a78

Browse files
authored
Merge pull request #100 from mongodb/add-procedure-commands
Add commands to analyze and extract procedures
2 parents 873685a + 0dad27a commit c057a78

29 files changed

+5168
-72
lines changed

audit-cli/README.md

Lines changed: 340 additions & 68 deletions
Large diffs are not rendered by default.

audit-cli/commands/analyze/analyze.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
// Currently supports:
55
// - includes: Analyze include directive relationships in RST files
66
// - usage: Find all files that use a target file
7+
// - procedures: Analyze procedure variations and statistics
78
//
89
// Future subcommands could include analyzing cross-references, broken links, or content metrics.
910
package analyze
1011

1112
import (
1213
"github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/includes"
14+
"github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/procedures"
1315
"github.com/mongodb/code-example-tooling/audit-cli/commands/analyze/usage"
1416
"github.com/spf13/cobra"
1517
)
@@ -27,13 +29,15 @@ func NewAnalyzeCommand() *cobra.Command {
2729
Currently supports:
2830
- includes: Analyze include directive relationships (forward dependencies)
2931
- usage: Find all files that use a target file (reverse dependencies)
32+
- procedures: Analyze procedure variations and statistics
3033
3134
Future subcommands may support analyzing cross-references, broken links, or content metrics.`,
3235
}
3336

3437
// Add subcommands
3538
cmd.AddCommand(includes.NewIncludesCommand())
3639
cmd.AddCommand(usage.NewUsageCommand())
40+
cmd.AddCommand(procedures.NewProceduresCommand())
3741

3842
return cmd
3943
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package procedures
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mongodb/code-example-tooling/audit-cli/internal/rst"
7+
)
8+
9+
// AnalyzeFile analyzes procedures in a file and returns a report.
10+
//
11+
// This function parses all procedures from the file and generates analysis
12+
// information including variation counts, step counts, implementation types,
13+
// and sub-procedure detection.
14+
//
15+
// This function expands include directives to properly detect variations that
16+
// may be defined in included files.
17+
//
18+
// Parameters:
19+
// - filePath: Path to the RST file to analyze
20+
//
21+
// Returns:
22+
// - *AnalysisReport: Analysis report containing all procedure information
23+
// - error: Any error encountered during analysis
24+
func AnalyzeFile(filePath string) (*AnalysisReport, error) {
25+
return AnalyzeFileWithOptions(filePath, true)
26+
}
27+
28+
// AnalyzeFileWithOptions analyzes procedures in a file with options and returns a report.
29+
//
30+
// This function parses all procedures from the file and generates analysis
31+
// information including variation counts, step counts, implementation types,
32+
// and sub-procedure detection.
33+
//
34+
// Parameters:
35+
// - filePath: Path to the RST file to analyze
36+
// - expandIncludes: If true, expands include directives inline
37+
//
38+
// Returns:
39+
// - *AnalysisReport: Analysis report containing all procedure information
40+
// - error: Any error encountered during analysis
41+
func AnalyzeFileWithOptions(filePath string, expandIncludes bool) (*AnalysisReport, error) {
42+
// Parse all procedures from the file
43+
procedures, err := rst.ParseProceduresWithOptions(filePath, expandIncludes)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to parse procedures from %s: %w", filePath, err)
46+
}
47+
48+
// Create the report
49+
report := NewAnalysisReport(filePath)
50+
51+
// Group procedures from the same tab set
52+
// Track which tab sets we've already processed
53+
processedTabSets := make(map[*rst.TabSetInfo]bool)
54+
55+
for _, procedure := range procedures {
56+
// If this procedure is part of a tab set and we haven't processed it yet
57+
if procedure.TabSet != nil && !processedTabSets[procedure.TabSet] {
58+
// Mark this tab set as processed
59+
processedTabSets[procedure.TabSet] = true
60+
61+
// Create a grouped analysis for all procedures in this tab set
62+
analysis := analyzeTabSet(procedure.TabSet)
63+
report.AddProcedure(analysis)
64+
} else if procedure.TabSet == nil {
65+
// Regular procedure (not part of a tab set)
66+
analysis := analyzeProcedure(procedure)
67+
report.AddProcedure(analysis)
68+
}
69+
// Skip procedures that are part of an already-processed tab set
70+
}
71+
72+
return report, nil
73+
}
74+
75+
// analyzeProcedure analyzes a single procedure and returns analysis results.
76+
func analyzeProcedure(procedure rst.Procedure) ProcedureAnalysis {
77+
// Get variations
78+
variations := rst.GetProcedureVariations(procedure)
79+
80+
// If no variations, count as 1 (single variation)
81+
variationCount := len(variations)
82+
if variationCount == 0 {
83+
variationCount = 1
84+
variations = []string{"(no variations)"}
85+
}
86+
87+
// Count steps
88+
stepCount := len(procedure.Steps)
89+
90+
// Determine implementation type
91+
implementation := string(procedure.Type)
92+
93+
// Check for sub-steps
94+
hasSubSteps := procedure.HasSubSteps
95+
96+
return ProcedureAnalysis{
97+
Procedure: procedure,
98+
Variations: variations,
99+
VariationCount: variationCount,
100+
StepCount: stepCount,
101+
HasSubSteps: hasSubSteps,
102+
Implementation: implementation,
103+
}
104+
}
105+
106+
// analyzeTabSet analyzes a tab set containing multiple procedure variations.
107+
// This groups all procedures from the same tab set for reporting purposes.
108+
func analyzeTabSet(tabSet *rst.TabSetInfo) ProcedureAnalysis {
109+
// Use the first procedure as the representative
110+
// (they all have the same title/heading)
111+
var firstProc rst.Procedure
112+
for _, tabID := range tabSet.TabIDs {
113+
if proc, ok := tabSet.Procedures[tabID]; ok {
114+
firstProc = proc
115+
break
116+
}
117+
}
118+
119+
// Get all tab IDs as variations
120+
variations := tabSet.TabIDs
121+
122+
// Count total variations
123+
variationCount := len(variations)
124+
125+
// Use the step count from the first procedure
126+
// (each tab may have different step counts, but we report the first one)
127+
stepCount := len(firstProc.Steps)
128+
129+
// Determine implementation type
130+
implementation := string(firstProc.Type)
131+
132+
// Check for sub-steps
133+
hasSubSteps := firstProc.HasSubSteps
134+
135+
return ProcedureAnalysis{
136+
Procedure: firstProc,
137+
Variations: variations,
138+
VariationCount: variationCount,
139+
StepCount: stepCount,
140+
HasSubSteps: hasSubSteps,
141+
Implementation: implementation,
142+
}
143+
}
144+
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package procedures
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// OutputOptions controls what information is displayed in the output.
9+
type OutputOptions struct {
10+
ListAll bool // List all variations with their selection/tabid values
11+
ListSummary bool // List procedures grouped by heading without selection details
12+
Implementation bool // Show how each procedure is implemented
13+
SubProcedures bool // Indicate if procedures contain nested sub-procedures
14+
StepCount bool // Show step count for each procedure
15+
}
16+
17+
// PrintReport prints the analysis report to stdout based on the output options.
18+
func PrintReport(report *AnalysisReport, options OutputOptions) {
19+
// If no special options are set, just print the count
20+
if !options.ListAll && !options.ListSummary && !options.Implementation && !options.SubProcedures && !options.StepCount {
21+
printSummary(report)
22+
return
23+
}
24+
25+
// Print detailed report
26+
printDetailedReport(report, options)
27+
}
28+
29+
// groupProceduresByHeading groups procedures by their heading and returns the groups and order.
30+
func groupProceduresByHeading(procedures []ProcedureAnalysis) (map[string][]ProcedureAnalysis, []string) {
31+
headingGroups := make(map[string][]ProcedureAnalysis)
32+
headingOrder := []string{}
33+
34+
for _, analysis := range procedures {
35+
heading := analysis.Procedure.Title
36+
if heading == "" {
37+
heading = "(Untitled)"
38+
}
39+
40+
if _, exists := headingGroups[heading]; !exists {
41+
headingOrder = append(headingOrder, heading)
42+
}
43+
headingGroups[heading] = append(headingGroups[heading], analysis)
44+
}
45+
46+
return headingGroups, headingOrder
47+
}
48+
49+
// calculateTotals calculates total unique procedures and appearances from grouped data.
50+
func calculateTotals(headingGroups map[string][]ProcedureAnalysis) (int, int) {
51+
totalUniqueProcedures := 0
52+
totalAppearances := 0
53+
54+
for _, procedures := range headingGroups {
55+
totalUniqueProcedures += len(procedures)
56+
for _, proc := range procedures {
57+
totalAppearances += proc.VariationCount
58+
}
59+
}
60+
61+
return totalUniqueProcedures, totalAppearances
62+
}
63+
64+
// printSummary prints a summary of the analysis.
65+
func printSummary(report *AnalysisReport) {
66+
fmt.Printf("File: %s\n", report.FilePath)
67+
fmt.Printf("Total unique procedures: %d\n", len(report.Procedures))
68+
fmt.Printf("Total procedure appearances: %d\n", report.TotalVariations)
69+
}
70+
71+
// printDetailedReport prints a detailed analysis report.
72+
func printDetailedReport(report *AnalysisReport, options OutputOptions) {
73+
fmt.Printf("Procedure Analysis for: %s\n", report.FilePath)
74+
fmt.Println(strings.Repeat("=", 80))
75+
76+
// Group procedures by heading first to get accurate counts
77+
headingGroups, headingOrder := groupProceduresByHeading(report.Procedures)
78+
totalUniqueProcedures, totalAppearances := calculateTotals(headingGroups)
79+
80+
fmt.Printf("\nTotal unique procedures: %d\n", totalUniqueProcedures)
81+
fmt.Printf("Total procedure appearances: %d\n\n", totalAppearances)
82+
83+
// Print implementation type summary if requested
84+
if options.Implementation {
85+
fmt.Println("Procedures by implementation type:")
86+
for implType, count := range report.ProceduresByType {
87+
fmt.Printf(" - %s: %d\n", implType, count)
88+
}
89+
fmt.Println()
90+
}
91+
92+
// Print details grouped by heading (headingGroups already created above)
93+
fmt.Println("Procedures by Heading:")
94+
fmt.Println(strings.Repeat("-", 80))
95+
96+
headingNum := 1
97+
for _, heading := range headingOrder {
98+
procedures := headingGroups[heading]
99+
100+
fmt.Printf("\n%d. %s\n", headingNum, heading)
101+
fmt.Printf(" Unique procedures: %d\n", len(procedures))
102+
103+
// Calculate total appearances for this heading
104+
totalAppearances := 0
105+
for _, proc := range procedures {
106+
totalAppearances += proc.VariationCount
107+
}
108+
fmt.Printf(" Total appearances: %d\n", totalAppearances)
109+
110+
// If only showing summary, skip the individual procedure details
111+
if options.ListSummary && !options.ListAll {
112+
headingNum++
113+
continue
114+
}
115+
116+
// Determine if we need sub-numbering (only when there are multiple unique procedures)
117+
useSubNumbering := len(procedures) > 1
118+
119+
// Show each unique procedure under this heading
120+
for i, analysis := range procedures {
121+
fmt.Printf("\n ")
122+
123+
// Only show sub-numbering if there are multiple unique procedures
124+
if useSubNumbering {
125+
fmt.Printf("%d.%d. ", headingNum, i+1)
126+
}
127+
128+
// Show the first step to distinguish procedures (only if there are multiple)
129+
if useSubNumbering {
130+
if len(analysis.Procedure.Steps) > 0 && analysis.Procedure.Steps[0].Title != "" {
131+
fmt.Printf("%s\n", analysis.Procedure.Steps[0].Title)
132+
} else if len(analysis.Procedure.Steps) > 0 {
133+
fmt.Printf("(Untitled first step)\n")
134+
} else {
135+
fmt.Printf("(No steps)\n")
136+
}
137+
} else {
138+
// For single procedures, just show the step count
139+
fmt.Printf("Steps: %d\n", len(analysis.Procedure.Steps))
140+
}
141+
142+
// Indent based on whether we're using sub-numbering
143+
indent := " "
144+
if !useSubNumbering {
145+
indent = " "
146+
}
147+
148+
// Only show step count if we already showed the first step title
149+
if useSubNumbering {
150+
fmt.Printf("%sSteps: %d\n", indent, len(analysis.Procedure.Steps))
151+
}
152+
153+
// Print implementation type if requested
154+
if options.Implementation {
155+
fmt.Printf("%sImplementation: %s\n", indent, analysis.Implementation)
156+
}
157+
158+
// Print sub-procedures flag if requested
159+
if options.SubProcedures {
160+
if analysis.HasSubSteps {
161+
fmt.Printf("%sContains sub-procedures: yes\n", indent)
162+
} else {
163+
fmt.Printf("%sContains sub-procedures: no\n", indent)
164+
}
165+
}
166+
167+
// Print selections if requested
168+
if options.ListAll {
169+
if analysis.VariationCount == 1 {
170+
fmt.Printf("%sAppears in 1 selection:\n", indent)
171+
} else {
172+
fmt.Printf("%sAppears in %d selections:\n", indent, analysis.VariationCount)
173+
}
174+
175+
if len(analysis.Variations) > 0 && analysis.Variations[0] != "(no variations)" {
176+
for _, variation := range analysis.Variations {
177+
fmt.Printf("%s - %s\n", indent, variation)
178+
}
179+
} else {
180+
fmt.Printf("%s (single variation, no tabs or selections)\n", indent)
181+
}
182+
} else if options.ListSummary {
183+
// For summary, just show the count without listing all selections
184+
if analysis.VariationCount == 1 {
185+
fmt.Printf("%sAppears in 1 selection\n", indent)
186+
} else {
187+
fmt.Printf("%sAppears in %d selections\n", indent, analysis.VariationCount)
188+
}
189+
}
190+
}
191+
192+
headingNum++
193+
}
194+
195+
fmt.Println()
196+
}
197+

0 commit comments

Comments
 (0)