Skip to content

timeafterleak precision: a stored time.After channel (timer := time.After(d); <-timer) inside a loop+select escapes detection — [Content truncated due to length] #39181

@github-actions

Description

@github-actions

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

  • New testdata case in pkg/linters/timeafterleak/testdata/...: timer := time.After(d) then case <-timer: in a loop+multicase select → flagged (// want)
  • Negative case: stored timer received in a single-case select or outside any loop → not flagged
  • Negative case: stored timer received across a FuncLit boundary → not flagged (parity with line 152)
  • No new false positives on the existing testdata corpus

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 ·

  • expires on Jun 20, 2026, 9:26 PM UTC-08:00

Metadata

Metadata

Labels

cookieIssue Monster Loves Cookies!sergo

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions