Skip to content

Detect missing recovery in goroutines launched through local closures - #71897

Merged
pelikhan merged 3 commits into
mainfrom
copilot/goroutinemissingrecover-fix-linter-issue
Oct 25, 2026
Merged

pelikhan merged 3 commits into
mainfrom
copilot/goroutinemissingrecover-fix-linter-issue

Conversation

Copilot AI commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

goroutinemissingrecover missed go fn() when fn was a local closure, allowing extraction of an inline literal to silently bypass panic-safety checks.

  • Detection: Resolve local := and var literal initializers by type-object identity, then apply the existing top-level defer/recover check.
  • Scope: Preserve named-function exclusions and suppression directives. Exclude package variables, reassigned variables, and address-taken variables to avoid inspecting stale initializers.
  • Coverage: Add regression cases for protected and unprotected closures, parentheses, shadowing, suppression, and mutable targets.
fn := func() {
    panic("oops")
}
go fn() // Reports missing top-level defer/recover.
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix linter for local closure in goroutines Oct 25, 2026
Copilot AI requested a review from pelikhan October 25, 2026 13:17
@pelikhan
pelikhan marked this pull request as ready for review October 25, 2026 13:17
Copilot AI max review requested due to automatic review settings October 25, 2026 13:17
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Commit pushed: 6cc911f

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Ponytail Reviewer

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Ponytail Reviewer · gpt56 · ~7 AIC · ⌖ ~6 AIC · ⊞ 14.8K · ◷

Generated by Ponytail Reviewer for #71897

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Test Quality Sentinel · sonnet50 · ~18 AIC · ⌖ ~16 AIC · ◇ ~7 AIC · ⊞ 12.7K · ◷

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (163 new lines under pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/71897-resolve-local-closures-in-goroutine-recover-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch. It was generated from the PR diff and records the decision to resolve go fn() to local, immutable function-literal initializers by *types.Var identity. Reassigned, range-bound, address-taken, and package-level variables are excluded.
  2. Complete the missing sections by filling in the [TODO: verify] markers (deciders, and whether SSA/dataflow resolution was considered) and refining the rationale.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-71897: Resolve Immutable Local Closures in the goroutinemissingrecover Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · opus55 · ~24 AIC · ⌖ ~41 AIC · ⊞ 1.9K · ◷
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: no blocking findings

The change correctly resolves local function-literal variables by type-object identity and conservatively excludes reassigned/address-taken targets. I found no actionable correctness or security issue on the changed lines. Static review only; tests were not run.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt50 · ~3 AIC · ⌖ ~1 AIC · ⊞ 1.6K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — the fix is well-scoped (mutable/address-taken exclusions, suppression, named-function exclusion are all correctly preserved), but one reassignment pattern still slips through the new detection, undercutting the PR's core claim of closing the local-closure loophole.

📋 Key Themes & Highlights

Key Finding

  • Declare-then-assign variables (var fn func(); fn = func(){...}) are never tracked. The record helper's mutable-detection treats any non-:=/ValueSpec write recorded via Uses as disqualifying, even when it's the variable's only write. I verified this with a standalone analysistest probe: go fn() after var fn func(); fn = func(){ panic("oops") } is silently not flagged. See inline comment for repro and suggested direction.

Positive Highlights

  • ✅ Good defensive exclusions for reassigned, range-assigned, and address-taken variables — each backed by a dedicated regression test (reassignedLocalClosureGoroutine, rangeAssignedLocalClosureGoroutine, addressTakenLocalClosureGoroutine).
  • ✅ Shadowing test (shadowedLocalClosureGoroutine) correctly validates object-identity resolution rather than name matching.
  • ✅ Doc comments at the package and function level were updated in lockstep with the behavior change.

Suggested Follow-up

  • Add a regression test for the var + later = pattern (single assignment, no intervening declaration-time initializer) and adjust indexLocalFuncLiterals to only mark a variable mutable when it has more than one write, rather than any non-:= write.

No other blocking issues found in the diff — the rest of the index/resolve logic is sound and the test matrix is thorough for the cases it does cover.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · ~53 AIC · ⌖ ~14 AIC · ⊞ 10.2K
Comment /matt to run again

return
}
variable, ok := pass.TypesInfo.Defs[ident].(*types.Var)
if !ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] False negative: a var fn func() declared then assigned (fn = func(){...}) is never tracked as a literal — the first write via = lands in the Uses branch and gets marked mutable, so the only initializer is discarded even though go fn() runs after it and never writes again.

💡 Repro and suggested fix
func twoStepVarAssignGoroutine() {
	var fn func()
	fn = func() {
		panic("oops")
	}
	go fn() // currently NOT flagged, should be
}

Confirmed via a standalone analysistest probe against this exact pattern — no diagnostic is reported. This is a common idiom (declare-then-assign, e.g. for recursive closures or to satisfy a pre-declared signature) and currently bypasses the check entirely, which undercuts the PR's stated goal of closing the local-closure loophole.

Consider tracking assignment count per variable instead of binary mutable/defs-only: if a *types.Var is written to exactly once via = (and never via :=/ValueSpec with a separate literal, never range-assigned, never address-taken), that single write should still be eligible as the literal initializer. The current logic conflates "reassigned after first literal" with "first and only assignment," both landing in the same mutable bucket.

@copilot please address this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Confirmed false-positive diagnostics remain for protected goroutines.

2 open issues
What changed in this PR

Extends goroutinemissingrecover so extracting a goroutine’s inline function into a local closure variable does not bypass panic-safety checks.

Changes:

  • Resolve local closure initializers by variable identity while excluding reassigned and address-taken variables.
  • Add regression coverage for recovery guards, parentheses, shadowing, suppression, and mutable targets.
File Description
pkg/​linters/​goroutinemissingrecover/​testdata/​src/​a/​a.go Adds closure-detection and exclusion regression cases.
pkg/​linters/​goroutinemissingrecover/​goroutinemissingrecover.go Indexes local closures and checks their recovery guards.

Review effort: Max

Comment on lines +61 to +62
if variable, ok := pass.TypesInfo.Uses[fun].(*types.Var); ok {
call = localLiterals[variable]
Comment on lines +127 to +130
case *ast.UnaryExpr:
if stmt.Op == token.AND {
record(stmt.X, nil)
}
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran pr-finisher on 6cc911f. Local make fmt, make lint, make agent-report-progress, and the targeted linter test passed. Current CI checks show no failures, and the PR is open, non-draft, with clean mergeability. Not merge-ready: three review threads remain unresolved, and the draft ADR needs completion and linking in the PR body. No files changed or commits pushed; older feedback was left unchanged under this task’s comment-scoping rules.

@pelikhan
pelikhan merged commit 1e4f4e0 into main Oct 25, 2026
36 checks passed
@pelikhan
pelikhan deleted the copilot/goroutinemissingrecover-fix-linter-issue branch October 25, 2026 14:06
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.93.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants