Skip to content

panicinlibrarycode: enforce FuncLit boundaries for init/doc panic exemptions#41631

Merged
pelikhan merged 2 commits into
mainfrom
copilot/fix-panic-library-code
Jun 26, 2026
Merged

panicinlibrarycode: enforce FuncLit boundaries for init/doc panic exemptions#41631
pelikhan merged 2 commits into
mainfrom
copilot/fix-panic-library-code

Conversation

Copilot AI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

panicinlibrarycode incorrectly suppressed panics inside nested closures when the enclosing FuncDecl was init() or had a documented panic contract (panics if/on ...). The exemption walk skipped FuncLit boundaries, creating false negatives for deferred/registered callbacks that execute outside the parent function’s contract.

  • Analyzer boundary semantics

    • Updated isInInitFunction to walk Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) and immediately stop exemption when the innermost enclosing node is a *ast.FuncLit.
    • Updated hasDocumentedPanicContract with the same boundary rule so documented-contract exemptions only apply to panics directly in the documented function body.
  • Regression coverage

    • Added testdata for a panic inside a closure nested in init() (now reported).
    • Added testdata for a panic inside a closure nested in a documented panics if ... function (now reported).
    • Existing direct-body exemptions remain covered (direct init() panic, direct documented-contract panic, and sync.Once.Do callback exemption).
for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
	if _, isFuncLit := encl.Node().(*ast.FuncLit); isFuncLit {
		return false
	}
	decl, ok := encl.Node().(*ast.FuncDecl)
	if !ok {
		break
	}
	// existing init/doc-contract checks...
	break
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix panicinlibrarycode false negatives for nested closures panicinlibrarycode: enforce FuncLit boundaries for init/doc panic exemptions Jun 26, 2026
Copilot AI requested a review from pelikhan June 26, 2026 07:52
@github-actions

Copy link
Copy Markdown
Contributor

👋 Nice fix to the panicinlibrarycode analyzer! Enforcing FuncLit boundaries for init and documented-panic-contract exemptions is a well-scoped correction — the false-negative for deferred/registered callbacks executing outside a parent function’s contract is a real footgun.

The PR description is clear, the logic change is tight (8 lines modified in the analyzer), and the testdata additions cover both new failure cases while preserving the existing exemption scenarios. This looks ready for review. ✅

Generated by ✅ Contribution Check · 217.8 AIC · ⌖ 19 AIC · ⊞ 6K ·

@pelikhan pelikhan marked this pull request as ready for review June 26, 2026 11:28
Copilot AI review requested due to automatic review settings June 26, 2026 11:28
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped. The PR modifies only the linter implementation (panic-in-library-code.go) and its testdata corpus (testdata/src/panicinlibrarycode/panicinlibrarycode.go). The existing test file (panic-in-library-code_test.go) was not changed.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

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

No ADR enforcement needed: PR #41631 does not have the 'implementation' label and has only 24 new lines of code in business logic directories (threshold: 100).

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 PR fixes panicinlibrarycode false negatives where panics inside nested closures were incorrectly exempted when the enclosing function was init() or had a documented panic contract. The analyzer now treats FuncLit as an exemption boundary so init()/doc-contract exemptions only apply to panics directly in the function body.

Changes:

  • Update isInInitFunction to stop exemptions when a *ast.FuncLit is encountered while walking enclosing nodes.
  • Update hasDocumentedPanicContract with the same FuncLit boundary rule.
  • Add regression testdata covering panics inside closures nested in init() and in documented “panics if …” functions (now reported).
Show a summary per file
File Description
pkg/linters/panic-in-library-code/panic-in-library-code.go Enforces FuncLit boundaries so init/doc-contract panic exemptions don’t suppress panics inside nested closures.
pkg/linters/panic-in-library-code/testdata/src/panicinlibrarycode/panicinlibrarycode.go Adds test cases to ensure nested-closure panics are reported even when wrapped by init/doc-contract contexts.

Review details

Tip

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

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

Comment on lines 178 to 180
// isInInitFunction reports whether the panic is inside a top-level init()
// function. Only top-level (no receiver) init functions are recognized;
// methods named init are ordinary methods and are not exempt.
@github-actions github-actions Bot mentioned this pull request Jun 26, 2026

@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 /diagnose, /tdd, and /improve-codebase-architecture — approving with two minor suggestions.

📋 Key Themes & Highlights

Positive Highlights

  • ✅ Root cause precisely identified: Enclosing was blind to FuncLit boundaries, so the walk silently passed through closures and matched the outer FuncDecl.
  • ✅ Fix is minimal and correct — adding (*ast.FuncLit)(nil) to the Enclosing call and short-circuiting on the first FuncLit is the right invariant.
  • ✅ Regression tests added for both affected exemption paths (isInInitFunction and hasDocumentedPanicContract).
  • isInSyncOnceDoFuncLit is unaffected — it operates on a separate code path that runs before either exemption.

Minor Suggestions

  • Duplicate guard pattern (inline comment on panic-in-library-code.go L185): the 3-line FuncLit check is copied verbatim into both functions. A shared findEnclosingFuncDecl helper would keep the invariant in one place.
  • Deep nesting test (inline comment on testdata L51): the current tests only cover one closure level. A 2-level test would make the Enclosing traversal order contract explicit.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 51.7 AIC · ⌖ 8.14 AIC · ⊞ 6.5K

for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
if _, isFuncLit := encl.Node().(*ast.FuncLit); isFuncLit {
return false
}

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.

[/improve-codebase-architecture] The identical 3-line FuncLit boundary guard is duplicated verbatim in isInInitFunction (lines 183–185) and hasDocumentedPanicContract (lines 200–202). A third exemption function added later would likely miss this invariant.

💡 Suggested: extract a shared helper
// findEnclosingFuncDecl returns the innermost enclosing *ast.FuncDecl,
// or nil if a FuncLit boundary is crossed first.
func findEnclosingFuncDecl(cur inspector.Cursor) *ast.FuncDecl {
    for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
        if _, ok := encl.Node().(*ast.FuncLit); ok {
            return nil
        }
        if decl, ok := encl.Node().(*ast.FuncDecl); ok {
            return decl
        }
        break
    }
    return nil
}

Both callers become single-expression checks, and the boundary invariant lives in one place.

panic("handler panic outside init flow") // want `avoid panic in library code; return an error instead`
}
_ = handler
}

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] The test covers one level of closure nesting. Consider adding a two-level case to explicitly document that the boundary check handles deep nesting (the algorithm is correct because Enclosing yields innermost-first, but a test makes that contract visible).

💡 Suggested test case
func init() {
    outer := func() {
        inner := func() {
            panic("deeply nested") // want `avoid panic in library code; return an error instead`
        }
        _ = inner
    }
    _ = outer
}

Without this, a future change that accidentally breaks inner-to-outer traversal order would not be caught by these tests.

@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.

The FuncLit-boundary fix is logically sound and the two new test cases cover the primary regression scenarios. Two medium-severity findings worth addressing.

Findings summary

Dead code in both boundary functions (lines 187, 204)

After adding the FuncLit early-return, the decl, ok := ...; if !ok { break } defensive branch is permanently unreachable in both isInInitFunction and hasDocumentedPanicContract. The loop only yields FuncDecl or FuncLit nodes; FuncLit is already handled; ok is therefore always true. The residual defensive code misleads readers and should be replaced with a direct type assertion.

Implicit check-ordering dependency, no test coverage

The fix is correct for sync.Once.Do(func(){ panic(...) }) nested inside init() only because isInSyncOnceDoFuncLit short-circuits before isInInitFunction in shouldSkipPanic. That ordering constraint is undocumented and unprotected by any test. A future reorder of those checks would silently convert this exemption into a false positive.

🔎 Code quality review by PR Code Quality Reviewer · 83 AIC · ⌖ 7.62 AIC · ⊞ 5.2K

return false
}
decl, ok := encl.Node().(*ast.FuncDecl)
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.

Dead code: if !ok { break } is unreachable after the new FuncLit early-return, silently misleading future readers about the invariants.

💡 Details and fix

cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) yields only *ast.FuncDecl and *ast.FuncLit nodes. The new early return false handles every *ast.FuncLit, so any code that reaches this type assertion is guaranteed to hold a *ast.FuncDeclok is always true. The if !ok { break } block is permanently dead.

The same issue exists at line 204 in hasDocumentedPanicContract.

Replace both with a direct assertion that makes the invariant explicit:

decl := encl.Node().(*ast.FuncDecl)

_ = handler
}

// ok: panic inside a sync.Once.Do callback.

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.

Missing test for sync.Once.Do callback inside init() — the exemption of this case now depends silently on check-ordering in shouldSkipPanic, with no regression test to protect it.

💡 Details and suggested test

The FuncLit boundary change makes isInInitFunction return false for any panic inside a closure within init(). The sync.Once.Do callback is still correctly exempt only because isInSyncOnceDoFuncLit short-circuits before isInInitFunction in shouldSkipPanic:

return isInSyncOnceDoFuncLit(pass, cur) || // must fire first
    panicMessageStartsWithBUG(pass, call) ||
    isInInitFunction(cur) ||               // now returns false for any FuncLit boundary
    hasDocumentedPanicContract(cur)

This ordering contract is undocumented. If the checks are ever reordered, the sync.Once.Do-inside-init() shape silently breaks (becomes a false positive). No test covers it today.

Add a companion case here:

func init() {
    once.Do(func() {
        panic("lazy init inside init()") // should not be flagged
    })
}

@pelikhan pelikhan merged commit 35c6c80 into main Jun 26, 2026
79 of 90 checks passed
@pelikhan pelikhan deleted the copilot/fix-panic-library-code branch June 26, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants