diff --git a/cmd/docgen/main.go b/cmd/docgen/main.go index e20ff5b..87dda61 100644 --- a/cmd/docgen/main.go +++ b/cmd/docgen/main.go @@ -16,7 +16,11 @@ import ( "github.com/spf13/pflag" ) -const outputFile = "docs/cli-reference.md" +const ( + outputFile = "docs/cli-reference.md" + initialHeadingLevel = 2 + dirPermissions = 0o750 +) func main() { rootCmd, err := cmd.NewRootCommand() @@ -30,7 +34,7 @@ func main() { rootCmd.DisableAutoGenTag = true dir := filepath.Dir(outputFile) - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, dirPermissions); err != nil { fmt.Fprintf(os.Stderr, "Error creating directory %s: %v\n", dir, err) os.Exit(1) } @@ -40,31 +44,36 @@ func main() { fmt.Fprintf(os.Stderr, "Error creating file %s: %v\n", outputFile, err) os.Exit(1) } - defer f.Close() - fmt.Fprintln(f, "# CLI Reference") - fmt.Fprintln(f) - fmt.Fprintln(f, "") - fmt.Fprintln(f) + defer func() { + if cerr := f.Close(); cerr != nil { + fmt.Fprintf(os.Stderr, "Error closing file %s: %v\n", outputFile, cerr) + } + }() + + _, _ = fmt.Fprintln(f, "# CLI Reference") + _, _ = fmt.Fprintln(f) + _, _ = fmt.Fprintln(f, "") + _, _ = fmt.Fprintln(f) - generateCommandDoc(f, rootCmd, 2) + generateCommandDoc(f, rootCmd, initialHeadingLevel) fmt.Printf("CLI reference generated at %s\n", outputFile) } func generateCommandDoc(f *os.File, command *cobra.Command, headingLevel int) { heading := strings.Repeat("#", headingLevel) - fmt.Fprintf(f, "%s %s\n\n", heading, command.CommandPath()) + _, _ = fmt.Fprintf(f, "%s %s\n\n", heading, command.CommandPath()) if command.Long != "" { - fmt.Fprintf(f, "%s\n\n", command.Long) + _, _ = fmt.Fprintf(f, "%s\n\n", command.Long) } else if command.Short != "" { - fmt.Fprintf(f, "%s\n\n", command.Short) + _, _ = fmt.Fprintf(f, "%s\n\n", command.Short) } if command.UseLine() != "" { - fmt.Fprintf(f, "**Usage:**\n\n") - fmt.Fprintf(f, "```\n%s\n```\n\n", command.UseLine()) + _, _ = fmt.Fprintf(f, "**Usage:**\n\n") + _, _ = fmt.Fprintf(f, "```\n%s\n```\n\n", command.UseLine()) } writeFlags(f, "Flags", command.NonInheritedFlags()) @@ -74,6 +83,7 @@ func generateCommandDoc(f *os.File, command *cobra.Command, headingLevel int) { if sub.IsAdditionalHelpTopicCommand() { continue } + generateCommandDoc(f, sub, headingLevel+1) } } @@ -84,6 +94,7 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) { } hasVisible := false + flags.VisitAll(func(flag *pflag.Flag) { if !flag.Hidden { hasVisible = true @@ -94,9 +105,9 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) { return } - fmt.Fprintf(f, "**%s:**\n\n", title) - fmt.Fprintln(f, "| Flag | Shorthand | Default | Description |") - fmt.Fprintln(f, "|------|-----------|---------|-------------|") + _, _ = fmt.Fprintf(f, "**%s:**\n\n", title) + _, _ = fmt.Fprintln(f, "| Flag | Shorthand | Default | Description |") + _, _ = fmt.Fprintln(f, "|------|-----------|---------|-------------|") flags.VisitAll(func(flag *pflag.Flag) { if flag.Hidden { @@ -113,9 +124,9 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) { defValue = `""` } - fmt.Fprintf(f, "| --%s | %s | %s | %s |\n", + _, _ = fmt.Fprintf(f, "| --%s | %s | %s | %s |\n", flag.Name, shorthand, defValue, flag.Usage) }) - fmt.Fprintln(f) + _, _ = fmt.Fprintln(f) } diff --git a/cmd/docgen/main_test.go b/cmd/docgen/main_test.go index 0d0be25..59633fd 100644 --- a/cmd/docgen/main_test.go +++ b/cmd/docgen/main_test.go @@ -42,7 +42,12 @@ func generateCLIReference(t *testing.T, rootCmd *cobra.Command) string { if err != nil { t.Fatalf("failed to create temp file: %v", err) } - defer tmpFile.Close() + + defer func() { + if cerr := tmpFile.Close(); cerr != nil { + t.Logf("warning: failed to close temp file: %v", cerr) + } + }() generateCommandDoc(tmpFile, rootCmd, 2) @@ -57,10 +62,12 @@ func generateCLIReference(t *testing.T, rootCmd *cobra.Command) string { // collectCommands recursively collects all commands in the Cobra tree. func collectCommands(command *cobra.Command) []*cobra.Command { commands := []*cobra.Command{command} + for _, sub := range command.Commands() { if sub.IsAdditionalHelpTopicCommand() { continue } + commands = append(commands, collectCommands(sub)...) } @@ -91,6 +98,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) { if description == "" { description = command.Short } + if description != "" && !strings.Contains(output, description) { t.Fatalf("description %q for command %q not found in CLI reference output", description, commandPath) } @@ -100,6 +108,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) { if flag.Hidden { return } + if !strings.Contains(output, flag.Name) { t.Fatalf("flag name %q for command %q not found in CLI reference output", flag.Name, commandPath) } @@ -108,6 +117,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) { if defValue == "" { defValue = `""` } + if !strings.Contains(output, defValue) { t.Fatalf("flag default value %q for flag %q of command %q not found in CLI reference output", defValue, flag.Name, commandPath) } @@ -122,6 +132,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) { if flag.Hidden { return } + if !strings.Contains(output, flag.Name) { t.Fatalf("inherited flag name %q for command %q not found in CLI reference output", flag.Name, commandPath) } @@ -130,6 +141,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) { if defValue == "" { defValue = `""` } + if !strings.Contains(output, defValue) { t.Fatalf("inherited flag default value %q for flag %q of command %q not found in CLI reference output", defValue, flag.Name, commandPath) } @@ -165,6 +177,7 @@ func extractNavPages(nav []map[string]interface{}) []string { subNav = append(subNav, m) } } + pages = append(pages, extractNavPages(subNav)...) } } @@ -192,6 +205,7 @@ func findProjectRoot(t *testing.T) string { if parent == dir { t.Fatal("could not find project root (no mkdocs.yml found)") } + dir = parent } } @@ -200,7 +214,7 @@ func findProjectRoot(t *testing.T) string { func loadNavPages(t *testing.T, projectRoot string) []string { t.Helper() - data, err := os.ReadFile(filepath.Join(projectRoot, "mkdocs.yml")) + data, err := os.ReadFile(filepath.Join(projectRoot, "mkdocs.yml")) //nolint:gosec // path is constructed from known project root if err != nil { t.Fatalf("failed to read mkdocs.yml: %v", err) } @@ -234,7 +248,7 @@ func TestPropertyDocumentationPageStructure(t *testing.T) { fullPath := filepath.Join(projectRoot, "docs", page) // Property: the file must exist - content, err := os.ReadFile(fullPath) + content, err := os.ReadFile(fullPath) //nolint:gosec // path is constructed from known project root and nav entries if err != nil { t.Fatalf("page %q listed in nav does not exist at %q: %v", page, fullPath, err) } @@ -261,6 +275,7 @@ func TestPropertyDocumentationPageStructure(t *testing.T) { // Property: after the heading, there must be at least one non-empty paragraph // of introductory text (skip blank lines between heading and paragraph) foundParagraph := false + for i := 1; i < len(lines); i++ { line := strings.TrimSpace(lines[i])