#1746·go-tools

SA4023 panic: index out of range — objectpath method indices desync when a type has a generic method

Author: jqueunietCreated Sep 15, 2026Updated Sep 15, 2026

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:

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:

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:

go
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:

  1. Nilness indexes Rets unguarded. 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.
  2. 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.