8000 SA4023 panic: index out of range — objectpath method indices desync when a type has a generic method · Issue #1746 · dominikh/go-tools · GitHub
[go: up one dir, main page]

Skip to content

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

Open

Description

@jqueuniet

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:

  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.

Activity

  1. jqueuniet commented on Sep 15, 2026

    @jqueuniet
    Author

    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.

  2. dominikh commented on Sep 15, 2026

    @dominikh
    Owner

    This is golang/go#81188 and will be fixed with the next Go patch release.

  3. dominikh commented on Oct 9, 2026

    @dominikh
    Owner

    Somewhat fixed by Go 1.27.2, but not quite: golang/go#82105

  4. bradfitz commented on Oct 14, 2026

    @bradfitz
    Contributor

    Should be fixed in Go 1.27.3 (expected ~Tuesday 2026-11-03)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstreamIssues that are caused by a dependency

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0