Summary
panicinlibrarycode exempts panics that sit lexically inside init() functions and inside functions whose doc comment documents a panic contract (panics if/on ...). Both exemption checks walk to the nearest enclosing *ast.FuncDecl while skipping *ast.FuncLit boundaries, so a panic() inside a function literal nested in such a function is wrongly suppressed. This is a false negative on exactly the kind of deferred/stored/goroutine closure where a panic is most dangerous.
The linter already knows how to respect FuncLit boundaries — its sibling exemption isInSyncOnceDoFuncLit iterates enclosing *ast.FuncLit nodes correctly. The two FuncDecl-only walks are inconsistent with it.
Evidence
pkg/linters/panic-in-library-code/panic-in-library-code.go
isInInitFunction (lines 181–193):
func isInInitFunction(cur inspector.Cursor) bool {
for encl := range cur.Enclosing((*ast.FuncDecl)(nil)) { // skips FuncLit ancestors
decl, ok := encl.Node().(*ast.FuncDecl)
if !ok { break }
if decl.Recv == nil && decl.Name != nil && decl.Name.Name == "init" {
return true
}
break // only the immediate enclosing FuncDecl
}
return false
}
hasDocumentedPanicContract (lines 195–213) has the identical shape: cur.Enclosing((*ast.FuncDecl)(nil)) filters to FuncDecl only, so any intervening FuncLit is transparent and the walk lands on the outer documented FuncDecl.
Because Enclosing((*ast.FuncDecl)(nil)) yields the innermost matching ancestor and the filter excludes FuncLit, a closure defined inside init() (or inside a documented function) inherits the parent's exemption:
func init() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
panic("runs at request time, NOT init time") // exempted (FN) — init() can't return an error, but this handler can
})
}
// Parse panics if the input is malformed.
func Parse(s string) {
registerCallback(func() {
panic("invoked later, unrelated to Parse's contract") // exempted (FN)
})
}
The stored/registered closure runs in a completely different context from the init() load phase or the documented synchronous contract, yet inherits the exemption.
Why this is the established sergo scope-boundary pattern
Same root cause as the already-accepted findings:
Canonical correct shape in this same repo: deferinloop / execcommandwithoutcontext use cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) and break at the FuncLit boundary. And this very file's isInSyncOnceDoFuncLit (lines 88–108) already iterates cur.Enclosing((*ast.FuncLit)(nil)) correctly — so the fix is consistent with existing in-file precedent.
Testdata gap
pkg/linters/panic-in-library-code/testdata/src/panicinlibrarycode/panicinlibrarycode.go covers a direct panic in init() (line 43) and a direct panic in the documented function documentedPreconditionPanics (line 70), but no case where the panic sits inside a FuncLit nested in either — so the false negative is invisible to the suite.
Recommendation
Make both exemptions stop at the FuncLit boundary, mirroring isInSyncOnceDoFuncLit and the deferinloop precedent. Include *ast.FuncLit in the Enclosing filter and treat an innermost *ast.FuncLit as "not directly in init / not the documented body":
for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
if _, isLit := encl.Node().(*ast.FuncLit); isLit {
return false // panic is inside a closure, not directly in init()/the documented FuncDecl
}
decl, ok := encl.Node().(*ast.FuncDecl)
if !ok { break }
// ... existing init()/doc-contract checks ...
break
}
(Apply the analogous change to hasDocumentedPanicContract.)
Secondary observation (same function)
hasDocumentedPanicContract matches the doc substrings panics if/on, panic if/on anywhere in the comment, so a negated contract such as // Foo never panics on a nil input. also exempts every panic in Foo. Lower priority than the FuncLit boundary, but worth tightening (e.g. anchor the phrase) while touching this function.
Validation checklist
Effort
Small — two functions in one file plus two testdata cases. Pattern and correct reference already exist in-repo.
References: §28218238847
Generated by 🤖 Sergo - Serena Go Expert · 236.2 AIC · ⌖ 13.7 AIC · ⊞ 5.9K · ◷
Summary
panicinlibrarycodeexempts panics that sit lexically insideinit()functions and inside functions whose doc comment documents a panic contract (panics if/on ...). Both exemption checks walk to the nearest enclosing*ast.FuncDeclwhile skipping*ast.FuncLitboundaries, so apanic()inside a function literal nested in such a function is wrongly suppressed. This is a false negative on exactly the kind of deferred/stored/goroutine closure where a panic is most dangerous.The linter already knows how to respect FuncLit boundaries — its sibling exemption
isInSyncOnceDoFuncLititerates enclosing*ast.FuncLitnodes correctly. The twoFuncDecl-only walks are inconsistent with it.Evidence
pkg/linters/panic-in-library-code/panic-in-library-code.goisInInitFunction(lines 181–193):hasDocumentedPanicContract(lines 195–213) has the identical shape:cur.Enclosing((*ast.FuncDecl)(nil))filters toFuncDeclonly, so any interveningFuncLitis transparent and the walk lands on the outer documentedFuncDecl.Because
Enclosing((*ast.FuncDecl)(nil))yields the innermost matching ancestor and the filter excludesFuncLit, a closure defined insideinit()(or inside a documented function) inherits the parent's exemption:The stored/registered closure runs in a completely different context from the
init()load phase or the documented synchronous contract, yet inherits the exemption.Why this is the established sergo scope-boundary pattern
Same root cause as the already-accepted findings:
ctxbackground) — enclosing walk only inspected*ast.FuncDecl, skipping FuncLit.wgdonenotdeferred) — ancestor walk never broke at the FuncLit boundary.Canonical correct shape in this same repo:
deferinloop/execcommandwithoutcontextusecur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))and break at the FuncLit boundary. And this very file'sisInSyncOnceDoFuncLit(lines 88–108) already iteratescur.Enclosing((*ast.FuncLit)(nil))correctly — so the fix is consistent with existing in-file precedent.Testdata gap
pkg/linters/panic-in-library-code/testdata/src/panicinlibrarycode/panicinlibrarycode.gocovers a direct panic ininit()(line 43) and a direct panic in the documented functiondocumentedPreconditionPanics(line 70), but no case where the panic sits inside aFuncLitnested in either — so the false negative is invisible to the suite.Recommendation
Make both exemptions stop at the FuncLit boundary, mirroring
isInSyncOnceDoFuncLitand thedeferinloopprecedent. Include*ast.FuncLitin theEnclosingfilter and treat an innermost*ast.FuncLitas "not directly in init / not the documented body":(Apply the analogous change to
hasDocumentedPanicContract.)Secondary observation (same function)
hasDocumentedPanicContractmatches the doc substringspanics if/on,panic if/onanywhere in the comment, so a negated contract such as// Foo never panics on a nil input.also exempts every panic inFoo. Lower priority than the FuncLit boundary, but worth tightening (e.g. anchor the phrase) while touching this function.Validation checklist
panicinside afunc(){...}closure declared ininit()→ expect a diagnostic.panicinside a closure nested in apanics if ...documented function → expect a diagnostic.init()and direct-in-documented-function panics remain exempt (no regression on lines 43/70 cases).sync.Once.Doclosure panic stays exempt (line 51 case unchanged).Effort
Small — two functions in one file plus two testdata cases. Pattern and correct reference already exist in-repo.
References: §28218238847