Skip to content

Commit eb1eb1c

Browse files
mpywclaude
andauthored
feat: add file filtering options for test and generated files (#3)
* feat: add file filtering options for test and generated files - Add -test flag (default: true) to control analysis of *_test.go files - Auto-exclude generated files (// Code generated ... DO NOT EDIT.) using ast.IsGenerated - Generated files are always excluded (no opt-out) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add tests and docs for file filtering options - Add TestFileFilterDefault: verify generated files are skipped, test files analyzed by default - Add TestFileFilterSkipTests: verify test files skipped when -test=false - Add testdata fixtures for filefilter and filefilterskip packages - Document -test flag and generated file exclusion in README.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2 parents 68ede3c + 9c34165 commit eb1eb1c

File tree

11 files changed

+195
-8
lines changed

11 files changed

+195
-8
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,22 @@ Available flags:
364364
- `-spawnerlabel` (default: false) - Check that spawner functions are properly labeled
365365
- `-gotask` (default: true, requires `-goroutine-deriver`)
366366

367+
### File Filtering
368+
369+
| Flag | Default | Description |
370+
|------|---------|-------------|
371+
| `-test` | `true` | Analyze test files (`*_test.go`) |
372+
373+
Generated files (containing `// Code generated ... DO NOT EDIT.`) are always excluded and cannot be opted in.
374+
375+
```bash
376+
# Exclude test files from analysis
377+
goroutinectx -test=false ./...
378+
379+
# With go vet
380+
go vet -vettool=$(which goroutinectx) -goroutinectx.test=false ./...
381+
```
382+
367383
### `-spawnerlabel`
368384

369385
When enabled, checks that functions calling spawn methods with func arguments have the `//goroutinectx:spawner` directive:

analyzer.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ var (
3939
enableSpawner bool
4040
enableSpawnerlabel bool
4141
enableGotask bool
42+
43+
// File filtering flags.
44+
analyzeTests bool
4245
)
4346

4447
func init() {
@@ -56,6 +59,9 @@ func init() {
5659
Analyzer.Flags.BoolVar(&enableSpawner, "spawner", true, "enable spawner checker")
5760
Analyzer.Flags.BoolVar(&enableSpawnerlabel, "spawnerlabel", false, "enable spawnerlabel checker")
5861
Analyzer.Flags.BoolVar(&enableGotask, "gotask", true, "enable gotask checker (requires -goroutine-deriver)")
62+
63+
// File filtering flags
64+
Analyzer.Flags.BoolVar(&analyzeTests, "test", true, "analyze test files (*_test.go)")
5965
}
6066

6167
// Analyzer is the main analyzer for goroutinectx.
@@ -75,11 +81,14 @@ func run(pass *analysis.Pass) (any, error) {
7581
return nil, ErrNoInspector
7682
}
7783

84+
// Build set of files to skip
85+
skipFiles := buildSkipFiles(pass)
86+
7887
// Parse configuration
7988
carriers := carrier.Parse(contextCarriers)
8089

81-
// Build ignore maps for each file
82-
ignoreMaps := buildIgnoreMaps(pass)
90+
// Build ignore maps for each file (excluding skipped files)
91+
ignoreMaps := buildIgnoreMaps(pass, skipFiles)
8392

8493
// Build spawner map from //goroutinectx:spawner directives and -external-spawner flag
8594
spawners := spawnerdir.Build(pass, externalSpawner)
@@ -88,12 +97,12 @@ func run(pass *analysis.Pass) (any, error) {
8897
enabled := buildEnabledCheckers(spawners)
8998

9099
// Run AST-based checks (goroutine, errgroup, waitgroup)
91-
runASTChecks(pass, insp, ignoreMaps, carriers, spawners)
100+
runASTChecks(pass, insp, ignoreMaps, carriers, spawners, skipFiles)
92101

93102
// Run spawnerlabel checker if enabled
94103
if enableSpawnerlabel {
95104
spawnerlabelChecker := spawnerlabel.New(spawners)
96-
spawnerlabelChecker.Check(pass, ignoreMaps)
105+
spawnerlabelChecker.Check(pass, ignoreMaps, skipFiles)
97106
}
98107

99108
// Report unused ignore directives
@@ -102,12 +111,39 @@ func run(pass *analysis.Pass) (any, error) {
102111
return nil, nil
103112
}
104113

114+
// buildSkipFiles creates a set of filenames to skip based on flags.
115+
// Generated files are always skipped.
116+
// Test files are skipped when analyzeTests is false.
117+
func buildSkipFiles(pass *analysis.Pass) map[string]bool {
118+
skipFiles := make(map[string]bool)
119+
120+
for _, file := range pass.Files {
121+
filename := pass.Fset.Position(file.Pos()).Filename
122+
123+
// Always skip generated files
124+
if ast.IsGenerated(file) {
125+
skipFiles[filename] = true
126+
continue
127+
}
128+
129+
// Skip test files if -test=false
130+
if !analyzeTests && strings.HasSuffix(filename, "_test.go") {
131+
skipFiles[filename] = true
132+
}
133+
}
134+
135+
return skipFiles
136+
}
137+
105138
// buildIgnoreMaps creates ignore maps for each file in the pass.
106-
func buildIgnoreMaps(pass *analysis.Pass) map[string]ignore.Map {
139+
func buildIgnoreMaps(pass *analysis.Pass, skipFiles map[string]bool) map[string]ignore.Map {
107140
ignoreMaps := make(map[string]ignore.Map)
108141

109142
for _, file := range pass.Files {
110143
filename := pass.Fset.Position(file.Pos()).Filename
144+
if skipFiles[filename] {
145+
continue
146+
}
111147
ignoreMaps[filename] = ignore.Build(pass.Fset, file)
112148
}
113149

@@ -121,6 +157,7 @@ func runASTChecks(
121157
ignoreMaps map[string]ignore.Map,
122158
carriers []carrier.Carrier,
123159
spawners *spawnerdir.Map,
160+
skipFiles map[string]bool,
124161
) {
125162
// Build context scopes for functions with context parameters
126163
funcScopes := buildFuncScopes(pass, insp, carriers)
@@ -171,12 +208,16 @@ func runASTChecks(
171208
return true
172209
}
173210

211+
filename := pass.Fset.Position(n.Pos()).Filename
212+
if skipFiles[filename] {
213+
return true
214+
}
215+
174216
scope := findEnclosingScope(funcScopes, stack)
175217
if scope == nil {
176218
return true // No context in scope
177219
}
178220

179-
filename := pass.Fset.Position(n.Pos()).Filename
180221
cctx := &context.CheckContext{
181222
Pass: pass,
182223
Scope: scope,

analyzer_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,24 @@ func TestGotask(t *testing.T) {
131131

132132
analysistest.Run(t, testdata, goroutinectx.Analyzer, "gotask")
133133
}
134+
135+
func TestFileFilterDefault(t *testing.T) {
136+
testdata := analysistest.TestData()
137+
// Default: -test=true, so test files are analyzed
138+
analysistest.Run(t, testdata, goroutinectx.Analyzer, "filefilter")
139+
}
140+
141+
func TestFileFilterSkipTests(t *testing.T) {
142+
testdata := analysistest.TestData()
143+
144+
// Set -test=false to skip test files
145+
if err := goroutinectx.Analyzer.Flags.Set("test", "false"); err != nil {
146+
t.Fatal(err)
147+
}
148+
149+
defer func() {
150+
_ = goroutinectx.Analyzer.Flags.Set("test", "true")
151+
}()
152+
153+
analysistest.Run(t, testdata, goroutinectx.Analyzer, "filefilterskip")
154+
}

internal/checkers/spawnerlabel/checker.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ func New(spawners *spawner.Map) *Checker {
2323
}
2424

2525
// Check runs the spawnerlabel analysis on the given pass.
26-
func (c *Checker) Check(pass *analysis.Pass, ignoreMaps map[string]ignore.Map) {
26+
func (c *Checker) Check(pass *analysis.Pass, ignoreMaps map[string]ignore.Map, skipFiles map[string]bool) {
2727
for _, file := range pass.Files {
2828
filename := pass.Fset.Position(file.Pos()).Filename
29+
if skipFiles[filename] {
30+
continue
31+
}
2932
ignoreMap := ignoreMaps[filename]
3033

3134
for _, decl := range file.Decls {

testdata/metatest/options.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"excludeDirs": [
33
"github.com",
4-
"golang.org"
4+
"golang.org",
5+
"filefilter",
6+
"filefilterskip"
57
]
68
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package filefilter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// badGoroutineInTest is reported when -test=true (default).
9+
func badGoroutineInTest(ctx context.Context) {
10+
go func() { // want `goroutine does not propagate context "ctx"`
11+
fmt.Println("no context in test file")
12+
}()
13+
}

testdata/src/filefilter/generated.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testdata/src/filefilter/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Package filefilter tests file filtering functionality.
2+
// Tests that:
3+
// - Generated files are always skipped (see generated.go)
4+
// - Test files are analyzed by default (see code_test.go)
5+
package filefilter
6+
7+
import (
8+
"context"
9+
"fmt"
10+
)
11+
12+
// badGoroutine should be reported in regular files.
13+
func badGoroutine(ctx context.Context) {
14+
go func() { // want `goroutine does not propagate context "ctx"`
15+
fmt.Println("no context")
16+
}()
17+
}
18+
19+
// goodGoroutine properly uses context.
20+
func goodGoroutine(ctx context.Context) {
21+
go func() {
22+
_ = ctx
23+
}()
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package filefilterskip
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// badGoroutineInTest is NOT reported when -test=false.
9+
func badGoroutineInTest(ctx context.Context) {
10+
go func() {
11+
fmt.Println("no context in test file - but skipped")
12+
}()
13+
}

testdata/src/filefilterskip/generated.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)