Summary
timeafterleak only reports time.After(...) when the call's immediate AST parent is the channel-receive UnaryExpr of the select case. See pkg/linters/timeafterleak/timeafterleak.go:91-97:
func isInsideLoopSelectComm(cur inspector.Cursor) bool {
// The immediate parent of time.After(...) must be a channel-receive UnaryExpr.
recvCur := cur.Parent()
unary, ok := recvCur.Node().(*ast.UnaryExpr)
if !ok || unary.Op != token.ARROW {
return false
}
...
This means the timer-leak only matters when the channel is received directly (case <-time.After(d):). But the identical leak occurs when the channel is first stored in a variable and then received in the loop's select:
for {
timer := time.After(d) // new timer every iteration — leaks identically
select {
case <-timer:
// ...
case <-ctx.Done():
return
}
}
Here time.After's immediate parent is an *ast.AssignStmt, not the receive UnaryExpr, so isInsideLoopSelectComm returns false at line 95 and the leak is silently missed — a false negative.
Impact
Latent today: all 3 current prod violations (see companion enforce-readiness issue) use the direct <-time.After(d) form, so this gap does not hide a live bug. But the intermediate-variable form is idiomatic and would slip past the linter — and past CI once -timeafterleak is enforced — defeating the analyzer's purpose for a common rewrite.
Recommendation
Apply the alias-tracking pattern already used by lenstringzero (#37741) and tolowerequalfold (#37492): when time.After's result is assigned to a single local variable, follow that variable to its receive site and check whether that receive is the Comm of a loop-enclosed multi-case select. Track the variable via pass.TypesInfo / object identity rather than syntactically. Reuse the existing loop/select/FuncLit-boundary checks at lines 99-156 unchanged — only the entry gate at 92-97 needs to also accept the assign-then-receive shape.
Validation checklist
Effort: Small (single-file analyzer + testdata; bounded by existing helper reuse).
Generated by Sergo R36.
Generated by 🤖 Sergo - Serena Go Expert · 237.8 AIC · ⌖ 12.9 AIC · ⊞ 5.3K · ◷
Summary
timeafterleakonly reportstime.After(...)when the call's immediate AST parent is the channel-receiveUnaryExprof theselectcase. Seepkg/linters/timeafterleak/timeafterleak.go:91-97:This means the timer-leak only matters when the channel is received directly (
case <-time.After(d):). But the identical leak occurs when the channel is first stored in a variable and then received in the loop's select:Here
time.After's immediate parent is an*ast.AssignStmt, not the receiveUnaryExpr, soisInsideLoopSelectCommreturnsfalseat line 95 and the leak is silently missed — a false negative.Impact
Latent today: all 3 current prod violations (see companion enforce-readiness issue) use the direct
<-time.After(d)form, so this gap does not hide a live bug. But the intermediate-variable form is idiomatic and would slip past the linter — and past CI once-timeafterleakis enforced — defeating the analyzer's purpose for a common rewrite.Recommendation
Apply the alias-tracking pattern already used by
lenstringzero(#37741) andtolowerequalfold(#37492): whentime.After's result is assigned to a single local variable, follow that variable to its receive site and check whether that receive is theCommof a loop-enclosed multi-caseselect. Track the variable viapass.TypesInfo/ object identity rather than syntactically. Reuse the existing loop/select/FuncLit-boundary checks at lines 99-156 unchanged — only the entry gate at 92-97 needs to also accept the assign-then-receive shape.Validation checklist
pkg/linters/timeafterleak/testdata/...:timer := time.After(d)thencase <-timer:in a loop+multicase select → flagged (// want)Effort: Small (single-file analyzer + testdata; bounded by existing helper reuse).
Generated by Sergo R36.