Skip to content
Merged
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
75 changes: 66 additions & 9 deletions pkg/linters/doc_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ import (
"strings"
"testing"

"github.com/github/gh-aw/pkg/linters"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var (
docBulletRe = regexp.MustCompile(`^//\s+-\s+([a-z0-9-]+)\s+—`)
readmeTableRe = regexp.MustCompile(`^\|\s+` + "`" + `([a-z0-9-]+)` + "`" + `\s+\|`)
)

// TestDocGo_CountMatchesBullets validates that the "All N active analyzers:"
// header count in doc.go matches the actual number of bullet entries.
// This prevents the header from silently drifting from the bullet list
Expand Down Expand Up @@ -52,23 +58,58 @@ func TestDocGo_CountMatchesBullets(t *testing.T) {
}

func TestDocGo_AnalyzersMatchREADME(t *testing.T) {
docSet := parseDocBulletSet(t)
readmeSet := parseReadmeSubpackageSet(t)
assert.Equal(t, sortedKeys(docSet), sortedKeys(readmeSet),
"doc.go analyzer bullets and README Subpackages table must list the same analyzers; update both files together")
}

func TestDocSurfacesMatchRegistryAndSpecList(t *testing.T) {
registrySet := make(map[string]struct{})
for _, analyzer := range linters.All() {
registrySet[analyzer.Name] = struct{}{}
}

docAsAnalyzerNames := normalizedDocSlugSetToAnalyzerNames(parseDocBulletSet(t))
readmeSlugs := parseReadmeSubpackageSet(t)
readmeAsAnalyzerNames := normalizedDocSlugSetToAnalyzerNames(readmeSlugs)

assert.Equal(t, sortedKeys(registrySet), sortedKeys(docAsAnalyzerNames),
"doc.go analyzer bullets must match linters.All(); update pkg/linters/doc.go when adding/removing linters")
assert.Equal(t, sortedKeys(registrySet), sortedKeys(readmeAsAnalyzerNames),
"README Subpackages analyzer entries must match linters.All(); update pkg/linters/README.md when adding/removing linters")

documentedLabels := make(map[string]struct{})
for _, d := range documentedAnalyzers() {
documentedLabels[d.label] = struct{}{}
}
assert.Equal(t, sortedKeys(readmeSlugs), sortedKeys(documentedLabels),
"documentedAnalyzers() labels in spec_test.go must match README Subpackages table labels exactly")
}

func parseDocBulletSet(t *testing.T) map[string]struct{} {
t.Helper()

docBytes, err := os.ReadFile("doc.go")
require.NoError(t, err, "doc.go must be present in pkg/linters")

readmeBytes, err := os.ReadFile("README.md")
require.NoError(t, err, "README.md must be present in pkg/linters")

docSet := make(map[string]struct{})
readmeSet := make(map[string]struct{})

docBulletRe := regexp.MustCompile(`^//\s+-\s+([a-z0-9-]+)\s+—`)
for line := range strings.SplitSeq(string(docBytes), "\n") {
if m := docBulletRe.FindStringSubmatch(line); m != nil {
docSet[m[1]] = struct{}{}
}
}

readmeTableRe := regexp.MustCompile(`^\|\s+` + "`" + `([a-z0-9-]+)` + "`" + `\s+\|`)
return docSet
}

func parseReadmeSubpackageSet(t *testing.T) map[string]struct{} {
t.Helper()

readmeBytes, err := os.ReadFile("README.md")
require.NoError(t, err, "README.md must be present in pkg/linters")

readmeSet := make(map[string]struct{})
for line := range strings.SplitSeq(string(readmeBytes), "\n") {
if !strings.HasPrefix(line, "| `") {
continue
Expand All @@ -83,8 +124,24 @@ func TestDocGo_AnalyzersMatchREADME(t *testing.T) {
readmeSet[m[1]] = struct{}{}
}

assert.Equal(t, sortedKeys(docSet), sortedKeys(readmeSet),
"doc.go analyzer bullets and README Subpackages table must list the same analyzers; update both files together")
return readmeSet
}

func normalizedDocSlugSetToAnalyzerNames(slugSet map[string]struct{}) map[string]struct{} {
normalized := make(map[string]struct{}, len(slugSet))
for slug := range slugSet {
normalized[docSlugToAnalyzerName(slug)] = struct{}{}
}
return normalized
}

func docSlugToAnalyzerName(slug string) string {
switch slug {
case "panic-in-library-code":
return "panicinlibrarycode"
default:
return slug
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docSlugToAnalyzerName function uses a hardcoded switch with a single special case for "panic-in-library-code". The conversion rule (strip hyphens) is actually a general one: any hyphenated slug maps to an analyzer name by strings.ReplaceAll(slug, "-", ""). This hardcoded switch will silently need updating every time a new hyphenated analyzer is added.

Consider replacing with:

func docSlugToAnalyzerName(slug string) string {
	return strings.ReplaceAll(slug, "-", "")
}

This is simpler, future-proof, and consistent with how all existing analyzer names are formed.

@copilot please address this.

}

func sortedKeys(set map[string]struct{}) []string {
Expand Down
Loading