From 7518bfb9ce8fdc874f0894d408ffbf667b4e0097 Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 10:09:00 +0000 Subject: [PATCH] fix(ax): preserve docblock partial results --- cmd/qa/cmd_docblock.go | 47 ++++++++++++++++++++++++++++--------- cmd/qa/cmd_docblock_test.go | 36 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 cmd/qa/cmd_docblock_test.go diff --git a/cmd/qa/cmd_docblock.go b/cmd/qa/cmd_docblock.go index 5b33593..7f05b34 100644 --- a/cmd/qa/cmd_docblock.go +++ b/cmd/qa/cmd_docblock.go @@ -59,6 +59,7 @@ type DocblockResult struct { Total int `json:"total"` Documented int `json:"documented"` Missing []MissingDocblock `json:"missing,omitempty"` + Warnings []DocblockWarning `json:"warnings,omitempty"` Passed bool `json:"passed"` } @@ -71,6 +72,13 @@ type MissingDocblock struct { Reason string `json:"reason,omitempty"` } +// DocblockWarning captures a partial parse failure while preserving valid +// results from the same directory. +type DocblockWarning struct { + Path string `json:"path"` + Error string `json:"error"` +} + // RunDocblockCheck checks docblock coverage for the given packages. func RunDocblockCheck(paths []string, threshold float64, verbose, jsonOutput bool) error { result, err := CheckDocblockCoverage(paths) @@ -92,14 +100,6 @@ func RunDocblockCheck(paths []string, threshold float64, verbose, jsonOutput boo return nil } - // Sort missing by file then line - slices.SortFunc(result.Missing, func(a, b MissingDocblock) int { - return cmp.Or( - cmp.Compare(a.File, b.File), - cmp.Compare(a.Line, b.Line), - ) - }) - // Print result if verbose && len(result.Missing) > 0 { cli.Print("%s\n\n", i18n.T("cmd.qa.docblock.missing_docs")) @@ -114,6 +114,13 @@ func RunDocblockCheck(paths []string, threshold float64, verbose, jsonOutput boo cli.Blank() } + if len(result.Warnings) > 0 { + for _, warning := range result.Warnings { + cli.Warnf("failed to parse %s: %s", warning.Path, warning.Error) + } + cli.Blank() + } + // Summary coverageStr := fmt.Sprintf("%.1f%%", result.Coverage) thresholdStr := fmt.Sprintf("%.1f%%", threshold) @@ -167,9 +174,12 @@ func CheckDocblockCoverage(patterns []string) (*DocblockResult, error) { return !strings.HasSuffix(fi.Name(), "_test.go") }, parser.ParseComments) if err != nil { - // Log parse errors but continue to check other directories - cli.Warnf("failed to parse %s: %v", dir, err) - continue + // Preserve partial results when a directory contains both valid and + // invalid files. The caller decides how to present the warning. + result.Warnings = append(result.Warnings, DocblockWarning{ + Path: dir, + Error: err.Error(), + }) } for _, pkg := range pkgs { @@ -183,6 +193,21 @@ func CheckDocblockCoverage(patterns []string) (*DocblockResult, error) { result.Coverage = float64(result.Documented) / float64(result.Total) * 100 } + slices.SortFunc(result.Missing, func(a, b MissingDocblock) int { + return cmp.Or( + cmp.Compare(a.File, b.File), + cmp.Compare(a.Line, b.Line), + cmp.Compare(a.Kind, b.Kind), + cmp.Compare(a.Name, b.Name), + ) + }) + slices.SortFunc(result.Warnings, func(a, b DocblockWarning) int { + return cmp.Or( + cmp.Compare(a.Path, b.Path), + cmp.Compare(a.Error, b.Error), + ) + }) + return result, nil } diff --git a/cmd/qa/cmd_docblock_test.go b/cmd/qa/cmd_docblock_test.go new file mode 100644 index 0000000..a41e706 --- /dev/null +++ b/cmd/qa/cmd_docblock_test.go @@ -0,0 +1,36 @@ +package qa + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunDocblockCheckJSONOutput_IsDeterministicAndKeepsWarnings(t *testing.T) { + dir := t.TempDir() + writeTestFile(t, filepath.Join(dir, "b.go"), "package sample\n\nfunc Beta() {}\n") + writeTestFile(t, filepath.Join(dir, "a.go"), "package sample\n\nfunc Alpha() {}\n") + writeTestFile(t, filepath.Join(dir, "broken.go"), "package sample\n\nfunc Broken(\n") + + restoreWorkingDir(t, dir) + + var result DocblockResult + output := captureStdout(t, func() { + err := RunDocblockCheck([]string{"."}, 100, false, true) + require.Error(t, err) + }) + + require.NoError(t, json.Unmarshal([]byte(output), &result)) + assert.False(t, result.Passed) + assert.Equal(t, 2, result.Total) + assert.Equal(t, 0, result.Documented) + require.Len(t, result.Missing, 2) + assert.Equal(t, "a.go", result.Missing[0].File) + assert.Equal(t, "b.go", result.Missing[1].File) + require.Len(t, result.Warnings, 1) + assert.Equal(t, ".", result.Warnings[0].Path) + assert.NotEmpty(t, result.Warnings[0].Error) +}