Skip to content

Commit 05163d9

Browse files
mpywclaude
andcommitted
feat(ssa): implement SSA FreeVars-based context capture detection
This commit switches context capture detection from AST-based to SSA-based analysis using FreeVars. Key improvements: - Add ClosureCapturesContext to check FreeVars for context/carrier types - Add FindFuncLit to locate SSA function for FuncLit AST nodes - Fix isContextType to unwrap pointer types (SSA uses *context.Context) - Support carrier types in SSA-based detection Behavioral changes: - Context used in nested closures (defer, IIFE) is now correctly detected - Former LIMITATION cases now pass: ctx in deferred closures, recovery closures, and IIFE are properly recognized as using context Test updates: - Update test cases from [LIMITATION]/[BAD] to [GOOD] for nested closure ctx - Update deriver test cases to [PARTIAL] where ctx capture works but deriver calls in nested closures are not yet traced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4c933a5 commit 05163d9

20 files changed

+1034
-415
lines changed

analyzer.go

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@ import (
1212
"golang.org/x/tools/go/analysis/passes/inspect"
1313
"golang.org/x/tools/go/ast/inspector"
1414

15-
"github.com/mpyw/goroutinectx/internal/checkers"
16-
"github.com/mpyw/goroutinectx/internal/checkers/errgroup"
17-
"github.com/mpyw/goroutinectx/internal/checkers/goroutine"
18-
"github.com/mpyw/goroutinectx/internal/checkers/goroutinederive"
15+
"github.com/mpyw/goroutinectx/internal/checker"
1916
"github.com/mpyw/goroutinectx/internal/checkers/gotask"
2017
"github.com/mpyw/goroutinectx/internal/checkers/spawner"
2118
"github.com/mpyw/goroutinectx/internal/checkers/spawnerlabel"
22-
"github.com/mpyw/goroutinectx/internal/checkers/waitgroup"
2319
"github.com/mpyw/goroutinectx/internal/context"
2420
"github.com/mpyw/goroutinectx/internal/directives/carrier"
21+
"github.com/mpyw/goroutinectx/internal/directives/deriver"
2522
"github.com/mpyw/goroutinectx/internal/directives/ignore"
2623
spawnerdir "github.com/mpyw/goroutinectx/internal/directives/spawner"
24+
"github.com/mpyw/goroutinectx/internal/patterns"
25+
"github.com/mpyw/goroutinectx/internal/registry"
2726
internalssa "github.com/mpyw/goroutinectx/internal/ssa"
2827
)
2928

@@ -139,7 +138,7 @@ func buildIgnoreMaps(pass *analysis.Pass, skipFiles map[string]bool) map[string]
139138
return ignoreMaps
140139
}
141140

142-
// runASTChecks runs AST-based checkers on the pass.
141+
// runASTChecks runs checkers on the pass using the unified SSA-based checker.
143142
func runASTChecks(
144143
pass *analysis.Pass,
145144
insp *inspector.Inspector,
@@ -148,49 +147,73 @@ func runASTChecks(
148147
spawners *spawnerdir.Map,
149148
skipFiles map[string]bool,
150149
) {
151-
// Build SSA program for future use (currently unused but required for buildssa dependency)
152-
_ = internalssa.Build(pass)
150+
// Build SSA program
151+
ssaProg := internalssa.Build(pass)
153152

154-
// Build context scopes for functions with context parameters
155-
funcScopes := buildFuncScopes(pass, insp, carriers)
153+
// Create registry and register APIs
154+
reg := registry.New()
156155

157-
// Build checkers based on flags
158-
var (
159-
callCheckers []checkers.CallChecker
160-
goStmtCheckers []checkers.GoStmtChecker
161-
)
156+
// Register errgroup/waitgroup/conc APIs with ClosureCapturesCtx pattern
157+
checker.RegisterDefaultAPIs(reg, enableErrgroup, enableWaitgroup)
158+
159+
// Build GoStmt patterns
160+
var goPatterns []patterns.GoStmtPattern
162161

163162
if enableGoroutine {
164-
goStmtCheckers = append(goStmtCheckers, goroutine.New())
163+
goPatterns = append(goPatterns, &patterns.GoStmtCapturesCtx{})
165164
}
166165

167166
if goroutineDeriver != "" {
168-
goStmtCheckers = append(goStmtCheckers, goroutinederive.New(goroutineDeriver))
167+
matcher := deriver.NewMatcher(goroutineDeriver)
168+
goPatterns = append(goPatterns, &patterns.GoStmtCallsDeriver{Matcher: matcher})
169169
}
170170

171-
if enableWaitgroup {
172-
callCheckers = append(callCheckers, waitgroup.New())
171+
// Map pattern names to ignore checker names
172+
checkerNames := map[string]ignore.CheckerName{
173+
"GoStmtCapturesCtx": ignore.Goroutine,
174+
"GoStmtCallsDeriver": ignore.GoroutineDerive,
175+
"ClosureCapturesCtx": ignore.Errgroup, // errgroup/waitgroup use this
173176
}
174177

175-
if enableErrgroup {
176-
callCheckers = append(callCheckers, errgroup.New())
177-
}
178+
// Create and run unified checker
179+
unifiedChecker := checker.New(
180+
reg,
181+
goPatterns,
182+
ssaProg,
183+
carriers,
184+
ignoreMaps,
185+
skipFiles,
186+
checkerNames,
187+
)
188+
unifiedChecker.Run(pass, insp)
178189

179-
// Add spawner checker if enabled and any functions are marked
180-
if enableSpawner && spawners.Len() > 0 {
181-
callCheckers = append(callCheckers, spawner.New(spawners))
182-
}
190+
// Run remaining checkers that aren't migrated yet (spawner, gotask)
191+
runLegacyCheckers(pass, insp, ignoreMaps, carriers, spawners, skipFiles)
192+
}
183193

184-
// gotask checker requires goroutine-deriver to be set
185-
if goroutineDeriver != "" && enableGotask {
186-
callCheckers = append(callCheckers, gotask.New(goroutineDeriver))
194+
// runLegacyCheckers runs checkers that haven't been migrated to the unified checker yet.
195+
func runLegacyCheckers(
196+
pass *analysis.Pass,
197+
insp *inspector.Inspector,
198+
ignoreMaps map[string]ignore.Map,
199+
carriers []carrier.Carrier,
200+
spawners *spawnerdir.Map,
201+
skipFiles map[string]bool,
202+
) {
203+
// Only spawner and gotask need the legacy path
204+
spawnerEnabled := enableSpawner && spawners.Len() > 0
205+
gotaskEnabled := goroutineDeriver != "" && enableGotask
206+
if !spawnerEnabled && !gotaskEnabled {
207+
return
187208
}
188209

210+
// Build context scopes for functions with context parameters
211+
funcScopes := buildFuncScopes(pass, insp, carriers)
212+
189213
// Node types we're interested in
190214
nodeFilter := []ast.Node{
191215
(*ast.FuncDecl)(nil),
192216
(*ast.FuncLit)(nil),
193-
(*ast.GoStmt)(nil),
194217
(*ast.CallExpr)(nil),
195218
}
196219

@@ -217,14 +240,14 @@ func runASTChecks(
217240
Carriers: carriers,
218241
}
219242

220-
switch node := n.(type) {
221-
case *ast.GoStmt:
222-
for _, checker := range goStmtCheckers {
223-
checker.CheckGoStmt(cctx, node)
243+
if call, ok := n.(*ast.CallExpr); ok {
244+
// Spawner checker
245+
if enableSpawner && spawners.Len() > 0 {
246+
spawner.New(spawners).CheckCall(cctx, call)
224247
}
225-
case *ast.CallExpr:
226-
for _, checker := range callCheckers {
227-
checker.CheckCall(cctx, node)
248+
// Gotask checker
249+
if goroutineDeriver != "" && enableGotask {
250+
gotask.New(goroutineDeriver).CheckCall(cctx, call)
228251
}
229252
}
230253

internal/checker/checker.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ func (c *Checker) Run(pass *analysis.Pass, insp *inspector.Inspector) {
8080
}
8181

8282
cctx := &patterns.CheckContext{
83-
Pass: pass,
84-
Tracer: c.tracer,
85-
SSAProg: c.ssaProg,
83+
Pass: pass,
84+
Tracer: c.tracer,
85+
SSAProg: c.ssaProg,
86+
CtxNames: scope.ctxNames,
87+
Carriers: c.carriers,
8688
}
8789

8890
switch node := n.(type) {

0 commit comments

Comments
 (0)