Skip to content

fix(execcommandwithoutcontext): stop enclosing-scope walk at non-go/defer FuncLit boundaries#43692

Merged
pelikhan merged 5 commits into
mainfrom
copilot/fix-execcommandwithoutcontext-issue
Jul 6, 2026
Merged

fix(execcommandwithoutcontext): stop enclosing-scope walk at non-go/defer FuncLit boundaries#43692
pelikhan merged 5 commits into
mainfrom
copilot/fix-execcommandwithoutcontext-issue

Conversation

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

execcommandwithoutcontext unconditionally walked past ctx-less FuncLit nodes during its enclosing-scope search, falsely attributing an outer function's ctx to exec.Command calls inside detached callbacks — producing wrong diagnostics and actively harmful autofixes.

Root cause

The if !hasCtx { continue } in the enclosing walk crossed every FuncLit boundary regardless of whether the closure actually shares the outer context lifetime. timesleepnocontext already fixed this same class of bug (#42901); execcommandwithoutcontext was left behind.

Changes

  • execcommandwithoutcontext.go — port the boundary discrimination from timesleepnocontext:

    • Non-go/defer FuncLit (regular callback) → break (stop walking, suppress diagnostic)
    • go/defer FuncLit (inherits outer ctx lifetime) → continue (keep walking)
    • Add isGoOrDeferClosure helper (direct port; handles parenthesized defer (func(){})() forms)
  • Testdata — add coverage for the previously untested cases:

    • GoodHTTPHandleFuncCallbackInCtxFunchttp.HandleFunc callback inside a ctx func → no diagnostic
    • GoodSyncCallbackInCtxFunc — sync callback inside a ctx func → no diagnostic
    • BadDeferWithCtx / BadDeferParenWithCtx / BadGoParenWithCtx — deferred/goroutine closures still flagged

False-positive example (now suppressed)

func serve(ctx context.Context) {
    http.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) {
        _ = exec.Command("ls") // was incorrectly flagged: "use exec.CommandContext(ctx, ...)"
                               // ctx is server-setup scope, not request scope
    })
}

…uncLit boundaries

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false positives in execcommandwithoutcontext linter fix(execcommandwithoutcontext): stop enclosing-scope walk at non-go/defer FuncLit boundaries Jul 6, 2026
Copilot AI requested a review from pelikhan July 6, 2026 05:51
@pelikhan
pelikhan marked this pull request as ready for review July 6, 2026 05:52
Copilot AI review requested due to automatic review settings July 6, 2026 05:52
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. This PR fixes execcommandwithoutcontext linter by updating production code and test data/golden files only. Test Quality Sentinel skipped.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer failed during the skills-based review.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

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.

Pull request overview

This pull request fixes a correctness issue in the execcommandwithoutcontext analyzer’s enclosing-scope walk so it no longer attributes an outer function’s context.Context to exec.Command calls inside ordinary callback closures (non-go/defer FuncLit), preventing false positives and harmful autofixes.

Changes:

  • Update execcommandwithoutcontext enclosing-scope traversal to stop at non-go/defer FuncLit boundaries without a context.Context parameter.
  • Add isGoOrDeferClosure helper to detect go/defer-launched closures, including parenthesized forms.
  • Extend testdata to cover suppressed callback cases and preserved go/defer cases (including parenthesized variants).
Show a summary per file
File Description
pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go Adjusts enclosing-scope walk to avoid crossing callback FuncLit boundaries unless the closure is launched via go/defer.
pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go Adds new fixtures covering callback suppression and go/defer closure reporting expectations.
pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go.golden Updates golden output to reflect expected suggested-fix output for newly added Bad* fixtures.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment on lines 62 to 66
if !hasCtx {
if _, isFuncLit := funcNode.(*ast.FuncLit); isFuncLit && !isGoOrDeferClosure(encl) {
break
}
continue
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/43692-discriminate-go-defer-closures-in-linter-scope-walk.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
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  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-43692: Discriminate go/defer Closures in Linter Enclosing-Scope Walk

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 (e.g., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 51.1 AIC · ⌖ 12.9 AIC · ⊞ 8.5K ·
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: execcommandwithoutcontext FuncLit boundary fix

This is a correct and well-tested port of the isGoOrDeferClosure boundary-discrimination logic from timesleepnocontext (#42901).

What was checked:

  • isGoOrDeferClosure logic — The AST walk is correct for all four cases:

    • go func(){...}() (non-paren) → GoStmt grandparent → flagged ✓
    • go (func(){...})() (paren) → ParenExpr unwrap → GoStmt grandparent → flagged ✓
    • defer func(){...}()DeferStmt grandparent → flagged ✓
    • defer (func(){...})() (paren) → DeferStmt grandparent → flagged ✓
    • Regular callback (e.g., mux.HandleFunc("/run", func(){...})) → callee != funcLitCur.Node()false → suppressed ✓
  • break vs continue — The break on a non-go/defer FuncLit without ctx correctly stops the enclosing walk, suppressing the false-positive diagnostic. The prior continue was the root cause of the bug.

  • Golden file — Correctly reflects autofix output for the three Bad* cases; Good* cases are unchanged (no autofix applied).

  • Test coverage — All new edge cases are covered: GoodHTTPHandleFuncCallbackInCtxFunc, GoodSyncCallbackInCtxFunc, BadDeferWithCtx, BadDeferParenWithCtx, BadGoParenWithCtx. The pre-existing OuterCtxInnerClosure covers the non-paren go func case.

The implementation is a clean, faithful port with no deviations from the reference in timesleepnocontext. No issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 87 AIC · ⌖ 8.04 AIC · ⊞ 4.9K

@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 /codebase-design and /tdd — commenting (not blocking). The fix is correct and well-motivated; two non-blocking improvement suggestions below.

📋 Key Themes & Highlights

Key Themes

  • Duplicate helper: isGoOrDeferClosure is now identical byte-for-byte in both execcommandwithoutcontext and timesleepnocontext. The PR description explicitly calls it a "direct port", meaning the codebase now has two copies of the exact helper whose absence caused this bug — creating the same class of drift risk for future linters.
  • Missing test case: The new Bad test matrix is BadDeferWithCtx, BadDeferParenWithCtx, BadGoParenWithCtx but omits plain BadGoWithCtx (non-parenthesized goroutine). OuterCtxInnerClosure (pre-existing) covers it implicitly, but not as a newly-named regression case.

Positive Highlights

  • ✅ Root cause correctly identified and documented — the analysis matches timesleepnocontext's fix (#42901).
  • ✅ Clean boundary logic: break for non-go/defer FuncLit, continue otherwise — easy to follow.
  • ✅ Parenthesized-form handling (defer (func(){})()) is properly covered in both implementation and tests.
  • ✅ Good test naming: GoodHTTPHandleFuncCallbackInCtxFunc and GoodSyncCallbackInCtxFunc read as specifications.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 108.4 AIC · ⌖ 7.24 AIC · ⊞ 6.7K
Comment /matt to run again

return nil, nil
}

func isGoOrDeferClosure(funcLitCur inspector.Cursor) bool {

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.

[/codebase-design] isGoOrDeferClosure is a verbatim copy of the same function in timesleepnocontext — the PR description even calls it a "direct port". Duplicating it recreates the exact divergence window that caused this bug: the next linter ported from this pattern will likely be missing the boundary fix again.

💡 Suggested refactor

Extract to pkg/linters/internal/astutil so all linters share one implementation:

// astutil/astutil.go
// IsGoOrDeferClosure reports whether the FuncLit at cur is the direct callee of a
// go or defer statement (handling parenthesized forms like defer (func(){})()).
func IsGoOrDeferClosure(funcLitCur inspector.Cursor) bool { ... }

Then both execcommandwithoutcontext and timesleepnocontext call astutil.IsGoOrDeferClosure(encl). A single fix, once, propagates everywhere.

The bug history is a strong argument for doing this now rather than later — the third linter to carry this pattern will land without the fix, and we'll be here again.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted isGoOrDeferClosure to pkg/linters/internal/astutil as the exported IsGoOrDeferClosure, and updated both execcommandwithoutcontext and timesleepnocontext to call astutil.IsGoOrDeferClosure. The private copies are removed. Commit: refactor(astutil): extract IsGoOrDeferClosure to shared astutil.

@pelikhan

pelikhan commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…BadGoWithCtx test

- Add IsGoOrDeferClosure to pkg/linters/internal/astutil to eliminate duplication
- Remove local isGoOrDeferClosure from execcommandwithoutcontext and timesleepnocontext
- Both linters now call astutil.IsGoOrDeferClosure
- Add BadGoWithCtx test case (plain go func() closure) to complete boundary matrix
- Update golden file to include BadGoWithCtx expected fixed output

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
pelikhan merged commit e12a010 into main Jul 6, 2026
29 checks passed
@pelikhan
pelikhan deleted the copilot/fix-execcommandwithoutcontext-issue branch July 6, 2026 07:17
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.3

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

Labels

None yet

Projects

None yet

3 participants