Repository navigation
Description
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 +0x5a4
Reproducer
go.mod:
module repro
go 1.27.0
dep/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 1
Deleting 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 = G
Object 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.
Activity
jqueuniet commentedon Sep 15, 2026
Sorry for the AI, I'm a bit out of my depth on this one, but figured it was still useful to fill as it does not seem this exact defect is referenced anywhere. Available for any further comment or investigation.
dominikh commentedon Sep 15, 2026
This is golang/go#81188 and will be fixed with the next Go patch release.
dominikh commentedon Oct 9, 2026
Somewhat fixed by Go 1.27.2, but not quite: golang/go#82105
bradfitz commentedon Oct 14, 2026
Should be fixed in Go 1.27.3 (expected ~Tuesday 2026-11-03)