Skip to content

Commit c4261c8

Browse files
SimplyLizclaude
andauthored
fix: generated file detection, check summary reconciliation, glob matching (#181)
Three CKB review infrastructure fixes: 1. Generated file marker detection was defined but never called — detectGeneratedFile now reads the first 10 lines of files and checks for GeneratedMarkers (DO NOT EDIT, @generated, etc.) 2. Add dist/*.js and dist/*.css to default generated patterns so bundled output (webpack, ncc, etc.) is automatically excluded from review. 3. Fix matchGlob ** suffix matching — was only checking filepath.Base(), now tries all path tail segments so patterns like **/dist/*.js work. 4. After HoldTheLine and dismissal filtering, reconcile check summaries with surviving findings. A check that reported "5 new bug patterns" but had all findings dropped (on unchanged lines) is now downgraded to pass with a note. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16fad6f commit c4261c8

2 files changed

Lines changed: 164 additions & 5 deletions

File tree

internal/query/review.go

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package query
22

33
import (
4+
"bufio"
45
"context"
56
"fmt"
7+
"os"
68
"path/filepath"
79
"sort"
810
"strings"
@@ -176,6 +178,7 @@ func DefaultReviewPolicy() *ReviewPolicy {
176178
"*.swagger.json", "*.openapi.json",
177179
"*_generated.go", "*_gen.go",
178180
"*.min.js", "*.min.css",
181+
"**/dist/*.js", "**/dist/*.css",
179182
},
180183
GeneratedMarkers: []string{
181184
"DO NOT EDIT", "Generated by", "AUTO-GENERATED", "This file is generated",
@@ -284,7 +287,7 @@ func (e *Engine) ReviewPR(ctx context.Context, opts ReviewPROptions) (*ReviewPRR
284287
generatedSet := make(map[string]bool)
285288
var generatedFiles []GeneratedFileInfo
286289
for _, df := range diffStats {
287-
if info, ok := detectGeneratedFile(df.FilePath, opts.Policy); ok {
290+
if info, ok := detectGeneratedFile(e.repoRoot, df.FilePath, opts.Policy); ok {
288291
generatedSet[df.FilePath] = true
289292
generatedFiles = append(generatedFiles, info)
290293
}
@@ -551,6 +554,12 @@ func (e *Engine) ReviewPR(ctx context.Context, opts ReviewPROptions) (*ReviewPRR
551554
findings, _ = dismissals.FilterDismissed(findings)
552555
}
553556

557+
// Reconcile check summaries with post-filtered findings. Checks set
558+
// their summary before HoldTheLine/dismissal filtering, so a check
559+
// may report "5 new bug patterns" while all 5 findings were dropped
560+
// because they're on unchanged lines. Update summaries to match.
561+
reconcileCheckSummaries(checks, findings)
562+
554563
// Sort checks by severity (fail first, then warn, then pass)
555564
sortChecks(checks)
556565

@@ -1197,7 +1206,8 @@ func determineVerdict(checks []ReviewCheck, policy *ReviewPolicy) string {
11971206
}
11981207

11991208
// detectGeneratedFile checks if a file is generated based on policy patterns and markers.
1200-
func detectGeneratedFile(filePath string, policy *ReviewPolicy) (GeneratedFileInfo, bool) {
1209+
// Checks (in order): glob patterns, flex/yacc mappings, marker strings in first 10 lines.
1210+
func detectGeneratedFile(repoRoot, filePath string, policy *ReviewPolicy) (GeneratedFileInfo, bool) {
12011211
// Check glob patterns
12021212
for _, pattern := range policy.GeneratedPatterns {
12031213
matched, _ := matchGlob(pattern, filePath)
@@ -1227,9 +1237,40 @@ func detectGeneratedFile(filePath string, policy *ReviewPolicy) (GeneratedFileIn
12271237
}, true
12281238
}
12291239

1240+
// Check marker strings in the first 10 lines of the file
1241+
if len(policy.GeneratedMarkers) > 0 && repoRoot != "" {
1242+
if marker := checkFileForMarkers(filepath.Join(repoRoot, filePath), policy.GeneratedMarkers); marker != "" {
1243+
return GeneratedFileInfo{
1244+
File: filePath,
1245+
Reason: fmt.Sprintf("Contains marker: %s", marker),
1246+
}, true
1247+
}
1248+
}
1249+
12301250
return GeneratedFileInfo{}, false
12311251
}
12321252

1253+
// checkFileForMarkers reads the first 10 lines of a file and returns the first
1254+
// matching marker string, or empty if none match.
1255+
func checkFileForMarkers(absPath string, markers []string) string {
1256+
f, err := os.Open(absPath)
1257+
if err != nil {
1258+
return ""
1259+
}
1260+
defer f.Close()
1261+
1262+
scanner := bufio.NewScanner(f)
1263+
for i := 0; i < 10 && scanner.Scan(); i++ {
1264+
line := scanner.Text()
1265+
for _, marker := range markers {
1266+
if strings.Contains(line, marker) {
1267+
return marker
1268+
}
1269+
}
1270+
}
1271+
return ""
1272+
}
1273+
12331274
// matchGlob performs simple glob matching (supports ** and *).
12341275
func matchGlob(pattern, path string) (bool, error) {
12351276
// Use filepath.Match for patterns without **
@@ -1270,8 +1311,17 @@ func matchGlob(pattern, path string) (bool, error) {
12701311
return false, nil
12711312
}
12721313

1273-
// Simple suffix: check if it matches the file name or path tail
1274-
return matchSimpleGlob(suffix, filepath.Base(path)), nil
1314+
// Simple suffix: try matching against every possible tail of the path.
1315+
// e.g., for suffix "dist/*.js" and path "a/b/dist/index.js",
1316+
// try "a/b/dist/index.js", "b/dist/index.js", "dist/index.js".
1317+
parts := strings.Split(remaining, "/")
1318+
for i := range parts {
1319+
candidate := strings.Join(parts[i:], "/")
1320+
if matchSimpleGlob(suffix, candidate) {
1321+
return true, nil
1322+
}
1323+
}
1324+
return false, nil
12751325
}
12761326

12771327
// matchSimpleGlob matches a pattern with * wildcards against a string.
@@ -1597,3 +1647,23 @@ func filterByChangedLines(findings []ReviewFinding, changedLines map[string]map[
15971647
}
15981648
return filtered
15991649
}
1650+
1651+
// reconcileCheckSummaries updates check summaries to reflect post-filtered
1652+
// findings. Without this, a check may report "5 new bug patterns" while all
1653+
// 5 were dropped by HoldTheLine because they're on unchanged lines.
1654+
func reconcileCheckSummaries(checks []ReviewCheck, findings []ReviewFinding) {
1655+
// Count surviving findings per check
1656+
countByCheck := make(map[string]int)
1657+
for _, f := range findings {
1658+
countByCheck[f.Check]++
1659+
}
1660+
1661+
for i, c := range checks {
1662+
remaining := countByCheck[c.Name]
1663+
if c.Status == "warn" && remaining == 0 {
1664+
// All findings were filtered — downgrade to pass and note it
1665+
checks[i].Status = "pass"
1666+
checks[i].Summary = c.Summary + " (all on unchanged lines)"
1667+
}
1668+
}
1669+
}

internal/query/review_test.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"strings"
910
"testing"
1011
)
1112

@@ -444,14 +445,102 @@ func TestDetectGeneratedFile(t *testing.T) {
444445

445446
for _, tt := range tests {
446447
t.Run(tt.path, func(t *testing.T) {
447-
_, detected := detectGeneratedFile(tt.path, policy)
448+
_, detected := detectGeneratedFile("", tt.path, policy)
448449
if detected != tt.expected {
449450
t.Errorf("detectGeneratedFile(%q) = %v, want %v", tt.path, detected, tt.expected)
450451
}
451452
})
452453
}
453454
}
454455

456+
func TestDetectGeneratedFile_DistPattern(t *testing.T) {
457+
t.Parallel()
458+
policy := DefaultReviewPolicy()
459+
460+
tests := []struct {
461+
path string
462+
expected bool
463+
}{
464+
{".github/actions/pr-analysis/dist/index.js", true},
465+
{"frontend/dist/bundle.js", true},
466+
{"frontend/dist/styles.css", true},
467+
{"src/dist.go", false}, // not a dist/ directory
468+
{"dist/README.md", false}, // not JS/CSS
469+
{"src/components/app.js", false}, // not in dist/
470+
}
471+
472+
for _, tt := range tests {
473+
t.Run(tt.path, func(t *testing.T) {
474+
_, detected := detectGeneratedFile("", tt.path, policy)
475+
if detected != tt.expected {
476+
t.Errorf("detectGeneratedFile(%q) = %v, want %v", tt.path, detected, tt.expected)
477+
}
478+
})
479+
}
480+
}
481+
482+
func TestDetectGeneratedFile_MarkerInFile(t *testing.T) {
483+
t.Parallel()
484+
dir := t.TempDir()
485+
policy := DefaultReviewPolicy()
486+
487+
// File with a generated marker in the first 10 lines
488+
genFile := filepath.Join(dir, "gen.go")
489+
if err := os.WriteFile(genFile, []byte("// Code generated by protoc-gen-go. DO NOT EDIT.\npackage pb\n"), 0644); err != nil {
490+
t.Fatal(err)
491+
}
492+
// File without any marker
493+
normalFile := filepath.Join(dir, "normal.go")
494+
if err := os.WriteFile(normalFile, []byte("package main\n\nfunc main() {}\n"), 0644); err != nil {
495+
t.Fatal(err)
496+
}
497+
498+
info, detected := detectGeneratedFile(dir, "gen.go", policy)
499+
if !detected {
500+
t.Error("expected gen.go to be detected as generated via marker")
501+
}
502+
if !strings.Contains(info.Reason, "marker") {
503+
t.Errorf("reason should mention marker, got %q", info.Reason)
504+
}
505+
506+
_, detected = detectGeneratedFile(dir, "normal.go", policy)
507+
if detected {
508+
t.Error("normal.go should not be detected as generated")
509+
}
510+
}
511+
512+
func TestReconcileCheckSummaries(t *testing.T) {
513+
t.Parallel()
514+
515+
checks := []ReviewCheck{
516+
{Name: "bug-patterns", Status: "warn", Summary: "5 new bug pattern(s)"},
517+
{Name: "secrets", Status: "pass", Summary: "No secrets detected"},
518+
{Name: "coupling", Status: "warn", Summary: "3 missing co-change files"},
519+
}
520+
// Only coupling has surviving findings
521+
findings := []ReviewFinding{
522+
{Check: "coupling", Message: "Missing co-change: foo.go"},
523+
}
524+
525+
reconcileCheckSummaries(checks, findings)
526+
527+
// bug-patterns had warn but 0 surviving findings → should be downgraded
528+
if checks[0].Status != "pass" {
529+
t.Errorf("bug-patterns status = %q, want pass", checks[0].Status)
530+
}
531+
if !strings.Contains(checks[0].Summary, "unchanged lines") {
532+
t.Errorf("bug-patterns summary should note unchanged lines, got %q", checks[0].Summary)
533+
}
534+
// secrets was already pass → unchanged
535+
if checks[1].Status != "pass" {
536+
t.Errorf("secrets status = %q, want pass", checks[1].Status)
537+
}
538+
// coupling has surviving findings → stays warn
539+
if checks[2].Status != "warn" {
540+
t.Errorf("coupling status = %q, want warn", checks[2].Status)
541+
}
542+
}
543+
455544
func TestMatchGlob(t *testing.T) {
456545
t.Parallel()
457546

0 commit comments

Comments
 (0)