SA4023 panic: index out of range — objectpath method indices desync when a type has a generic method
Version
- staticcheck 2026.2.1 (0.8.1)
- go1.27.1 darwin/arm64
What happened
SA4023 panics with index out of range on code where a type has a generic method (Go 1.27) declared before other methods.
panic: runtime error: index out of range [2] with length 1
goroutine 156 [running]:
honnef.co/go/tools/analysis/facts/nilness.(*Result).Nilness(0x..., 0x..., 0x2)
honnef.co/go/tools/analysis/facts/nilness/nilness.go:77 +0x100
honnef.co/go/tools/staticcheck/sa4023.run(0x...)
honnef.co/go/tools/staticcheck/sa4023/sa4023.go:181 +0x5b0
honnef.co/go/tools/lintcmd/runner.(*analyzerRunner).do(...)
honnef.co/go/tools/lintcmd/runner/runner.go:989 +0x5a4Reproducer
go.mod:
module repro
go 1.27.0dep/dep.go:
package dep
type T struct{}
// Generic method (Go 1.27). Declared FIRST in source.
func (T) G[R any](r R) R { return r }
// ONE result, with an interesting nilness fact (always nil).
func (T) A() *int { return nil }
// THREE results.
func (T) B() (int, *int, error) { return 0, nil, nil }main.go:
package main
import "repro/dep"
func main() {
var t dep.T
_, _, err := t.B()
if err != nil {
println("err")
}
_ = t.A()
_ = t.G(1)
}$ staticcheck -checks=SA4023 ./...
panic: runtime error: index out of range [2] with length 1Deleting G (and its call) and changing nothing else makes it clean. Reproduces with a cold STATICCHECK_CACHE, so it is not cache staleness.
Diagnosis
objectpath addresses methods positionally as Type.M<n>. The ordering of T's methods differs between the package as type-checked from source and the same package reconstructed from export data — the generic method moves to the end:
ORDER-EXPORT T.M0 = G # source order: G, A, B
ORDER-EXPORT T.M1 = A
ORDER-EXPORT T.M2 = B
ORDER-LOAD T.M0 = A # export-data order: A, B, G
ORDER-LOAD T.M1 = B
ORDER-LOAD T.M2 = GObject facts are round-tripped through those paths (objectpath.For at lintcmd/runner/runner.go:940, objectpath.Object at :775), so every method declared after a generic method gets the previous method's fact:
FACT-EXPORT path="T.M1" <- obj=A func() *int | fact=nilness: [{MaybeNil AlwaysNil}]
FACT-LOAD path="T.M1" -> obj=B func() (int,*int,error) | fact=nilness: [{MaybeNil AlwaysNil}]A's one-entry Rets now describes B, which has three results. sa4023.go:181 calls Nilness(obj, 2) and nilness.go:77 indexes r.m[fn][2] without a bounds check:
func (r *Result) Nilness(fn *types.Func, ret int) ValueNilness {
typ := fn.Type().(*types.Signature).Results().At(ret).Type() // ok, signature has 3 results
...
return normalize(r.m[fn][ret], typ) // panics, fact has 1
}Scope
This is not specific to SA4023 or to nilness facts — it corrupts any object fact on a method declared after a generic method. #1743 (SA1019 reporting a deprecation from RunT on the unrelated generic method AT) looks like the same mechanism with a deprecation fact.
It also reproduces through golangci-lint, but only with a warm cache: a cold run type-checks dependencies from source in-process and keeps facts keyed by the real objects, skipping the objectpath round-trip.
Notes on the fix
Two separate things:
NilnessindexesRetsunguarded. A bounds check there turns this class of bug into a wrong-but-not-fatal result. Worth having regardless, since a mismatched fact is always possible.- The actual defect is the positional method addressing desynchronising for generic methods. That needs the two orderings reconciled.
This is adjacent to #1741, which panics on the next line of the same block (irutil.IsTrivial(irpkg.Pkg.Prog.FuncValue(obj))) for what looks like the same underlying cause — an obj the analysing package cannot resolve consistently. PR #1745 adds fn == nil inside isTrivial only, so it will not cover this panic: Nilness is evaluated first in that && chain.
Source: dominikh/go-tools