golang/go

x/tools: nilness panics "no concrete method"

joeshaw opened this issue · 19 comments

What version of Go are you using (go version)?

$ go version
go version go1.20 linux/amd64

Does this issue reproduce with the latest release?

Yes, having installed with go install golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest:

go version -m $(which nilness)
/go/bin/nilness: go1.20
	path	golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness
	mod	golang.org/x/tools	v0.5.0	h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
	dep	golang.org/x/mod	v0.7.0	h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
	dep	golang.org/x/sys	v0.4.0	h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
	build	-buildmode=exe
	build	-compiler=gc
	build	CGO_ENABLED=1
	build	CGO_CFLAGS=
	build	CGO_CPPFLAGS=
	build	CGO_CXXFLAGS=
	build	CGO_LDFLAGS=
	build	GOARCH=amd64
	build	GOOS=linux
	build	GOAMD64=v1

I've also tried with @master (golang.org/x/tools v0.5.1-0.20230202234227-811111804389)

What operating system and processor architecture are you using (go env)?

I am on an Intel Mac, but I am running this in a container in a Linux VM. This also happens in a container running on Linux directly.

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/opt/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/opt/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.20"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/tmp/test/go.mod"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build2170370967=/tmp/go-build -gno-record-gcc-switches"

What did you do?

As part of a CI process, we run nilness ./... over a project's sources. For three different applications we hit this panic. In all three cases they are panicking on a method inside of google.golang.org/protobuf but the exact package and method varies. So far we've seen it on:

func (google.golang.org/protobuf/encoding/protowire.Number).IsValid() bool
func (google.golang.org/protobuf/reflect/protoreflect.Cardinality).GoString() string
A full panic
nilness ./...
panic: no concrete method: func (google.golang.org/protobuf/reflect/protoreflect.Cardinality).GoString() string

goroutine 2017 [running]:
golang.org/x/tools/go/ssa.(*Program).declaredFunc(0xc002032000, 0xc00158df80)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:149 +0xf9
golang.org/x/tools/go/ssa.(*Program).originFunc(0xc002549450?, 0xc004828868?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/source.go:193 +0x32
golang.org/x/tools/go/ssa.(*Program).addMethod(0xc002032000, 0xc0044248c0, 0xc002549450, 0x71b0e0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:109 +0x18c
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0012fbf80?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:200 +0x705
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc00037b380?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0012fbf10?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc00037be30?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0012fbea0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c595d8?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f80d8?, 0xc000fb3e40?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c59728?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f80d8?, 0xc000fb3fb0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c59818?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f80d8?, 0xc000e28a10?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c59e78?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f80d8?, 0xc000e28a90?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c59ef0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0012fb260?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8010?, 0xc000c59f50?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:226 +0x30f
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc000c59f68?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0012fb1f0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8178?, 0xc001534ab0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f80d8?, 0xc0041f40c0?}, 0x0, 0x9ad620?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc002032000, {0x7f8088?, 0xc0019b4690?}, 0x0, 0xc0008178c0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:242 +0x458
golang.org/x/tools/go/ssa.(*Program).needMethodsOf(0xc002032000, {0x7f8088?, 0xc0019b4690?}, 0x624c508726b89f64?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/methods.go:172 +0x7f
golang.org/x/tools/go/ssa.(*Package).build(0xc0041e4600)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/builder.go:2501 +0x13f
sync.(*Once).doSlow(0xc002032000?, 0xc0012d7810?)
	/opt/go/src/sync/once.go:74 +0xc2
sync.(*Once).Do(...)
	/opt/go/src/sync/once.go:65
golang.org/x/tools/go/ssa.(*Package).Build(...)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/ssa/builder.go:2477
golang.org/x/tools/go/analysis/passes/buildssa.run(0xc002030000)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/analysis/passes/buildssa/buildssa.go:72 +0x1a8
golang.org/x/tools/go/analysis/internal/checker.(*action).execOnce(0xc0017268c0)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/analysis/internal/checker/checker.go:725 +0x9ec
sync.(*Once).doSlow(0x0?, 0x0?)
	/opt/go/src/sync/once.go:74 +0xc2
sync.(*Once).Do(...)
	/opt/go/src/sync/once.go:65
golang.org/x/tools/go/analysis/internal/checker.(*action).exec(0x0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/analysis/internal/checker/checker.go:641 +0x3d
golang.org/x/tools/go/analysis/internal/checker.execAll.func1(0x0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/analysis/internal/checker/checker.go:629 +0x25
created by golang.org/x/tools/go/analysis/internal/checker.execAll
	/go/pkg/mod/golang.org/x/tools@v0.5.0/go/analysis/internal/checker/checker.go:635 +0x165

I haven't yet been able to reduce it to a sharable test case. A minimal test case is below. Simply using those types above doesn't trigger it.

In the application that panics with func (google.golang.org/protobuf/reflect/protoreflect.Cardinality).GoString() string, in its go.mod is google.golang.org/protobuf v1.26.0 // indirect.

Adding -debug fpstv doesn't yield much more info:

16:45:40.851633 load [./...]
16:45:50.913971 building graph of analysis passes
panic: no concrete method: func (google.golang.org/protobuf/reflect/protoreflect.Cardinality).GoString() string
[...]

Found a minimal test case.

go.mod:

module test

go 1.20

require github.com/prometheus/client_golang v1.12.2

require (
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
	github.com/golang/protobuf v1.5.2 // indirect
	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
	github.com/prometheus/client_model v0.3.0 // indirect
	github.com/prometheus/common v0.37.0 // indirect
	github.com/prometheus/procfs v0.8.0 // indirect
	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
	google.golang.org/protobuf v1.26.0 // indirect
)

main.go:

package main

import "github.com/prometheus/client_golang/prometheus/promhttp"
import "net/http/httptest"

func main() {
	w := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/", nil)

	h := promhttp.Handler()
	h.ServeHTTP(w, req)
}

nilness ./...:

panic: no concrete method: func (*crypto/x509/pkix.CertificateList).HasExpired(now time.Time) bool

goroutine 1146 [running]:
golang.org/x/tools/go/ssa.(*Program).declaredFunc(0xc000334540, 0xc0002ea780)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:149 +0xf9
golang.org/x/tools/go/ssa.(*Program).originFunc(0xc00081db80?, 0xc000290b70?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/source.go:193 +0x32
golang.org/x/tools/go/ssa.(*Program).addMethod(0xc000334540, 0xc00086ca40, 0xc00081db80, 0x7204e0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:109 +0x18c
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe898?, 0xc00005ab80?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:200 +0x705
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe938?, 0xc0004978f0?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe898?, 0xc00005ac10?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe8e8?, 0xc00005ac30?}, 0x0, 0x9b6768?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:223 +0x2d7
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe910?, 0xc0002e0570?}, 0x1, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:255 +0x617
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe848?, 0xc0000c4fc0?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:248 +0x487
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe938?, 0xc000497a58?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe8c0?, 0xc0003745c0?}, 0x0, 0x9b6768?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:236 +0x3bc
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe910?, 0xc0002e0630?}, 0x1, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:255 +0x617
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe848?, 0xc0000c42a0?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:248 +0x487
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe898?, 0xc00005ae80?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:220 +0x29b
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe938?, 0xc000497ae8?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe848?, 0xc0000c4150?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:208 +0x1d0
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe938?, 0xc0000ab1e8?}, 0x0, 0x9b6760?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:260 +0x5ae
golang.org/x/tools/go/ssa.(*Program).needMethods(0xc000334540, {0x7fe898?, 0xc000318450?}, 0x0, 0x1?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:209 +0x1f7
golang.org/x/tools/go/ssa.(*Program).needMethodsOf(0xc000334540, {0x7fe898?, 0xc000318450?}, 0xc0005240f0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/methods.go:172 +0x7f
golang.org/x/tools/go/ssa.(*builder).needsRuntimeTypes(0xc000291a40)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/builder.go:2439 +0x36d
golang.org/x/tools/go/ssa.(*Package).build(0xc00039d080)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/builder.go:2607 +0xc93
sync.(*Once).doSlow(0xc000334540?, 0xc000871180?)
	/opt/go/src/sync/once.go:74 +0xc2
sync.(*Once).Do(...)
	/opt/go/src/sync/once.go:65
golang.org/x/tools/go/ssa.(*Package).Build(...)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/ssa/builder.go:2477
golang.org/x/tools/go/analysis/passes/buildssa.run(0xc0003263c0)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/analysis/passes/buildssa/buildssa.go:72 +0x1a8
golang.org/x/tools/go/analysis/internal/checker.(*action).execOnce(0xc000436320)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/analysis/internal/checker/checker.go:763 +0x9ec
sync.(*Once).doSlow(0xc00063b778?, 0x6bc760?)
	/opt/go/src/sync/once.go:74 +0xc2
sync.(*Once).Do(...)
	/opt/go/src/sync/once.go:65
golang.org/x/tools/go/analysis/internal/checker.(*action).exec(0x43f265?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/analysis/internal/checker/checker.go:679 +0x3d
golang.org/x/tools/go/analysis/internal/checker.execAll.func1(0x0?)
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/analysis/internal/checker/checker.go:667 +0x25
created by golang.org/x/tools/go/analysis/internal/checker.execAll
	/go/pkg/mod/golang.org/x/tools@v0.5.1-0.20230202234227-811111804389/go/analysis/internal/checker/checker.go:673 +0x165

CC @golang/tools-team

Many thanks @joeshaw for taking the time to find a minimal testcase; I confirm that it reproduces the panic for me at tip today.

A brief investigation makes me suspect the bug is in this code in the buildssa analyzer. It tries to construct an SSA package for every package in the transitive closure of the target package:

func run(pass *analysis.Pass) (interface{}, error) {
...
	// Create SSA packages for all imports.
	// Order is not significant.
	created := make(map[*types.Package]bool)
	var createAll func(pkgs []*types.Package)
	createAll = func(pkgs []*types.Package) {
		for _, p := range pkgs {
			if !created[p] {
				created[p] = true
				prog.CreatePackage(p, nil, nil, true) // <--here
				createAll(p.Imports())
			}
		}
	}
	createAll(pass.Pkg.Imports())

but this traversal over the Imports graph is not guaranteed to reach all dependencies if some were loaded from export data. I notice that setting cfg.Mode |= NeedDeps in packages.Load is an effective workaround, since it causes all packages to be loaded from syntax (at great expense).

Thank you for the minimal reproducer. Here is an even more minimal reproducer:

package main

import "net/http"

func main() {
	var h any = http.Header{}
	_ = h
}

with the go.mod

module test

go 1.20

My investigation seems to agree with Alan's. For the types.Package for "crypto/tls", the Imports() is is empty, and no types.Package is importing "crypto/x509". So no types.Package for "crypto/x509/pkix". So no ssa.Package for "crypto/x509/pkix". Hence the panic for the missing method "(*crypto/x509/pkix.CertificateList).HasExpired".

Alan do you recall if something is being more conservative during package loading than it used to?

Alan do you recall if something is being more conservative during package loading than it used to?

No, I suspect there has always been a latent problem here with packages loaded from export data.

I encountered a very similar problem in another context--gopls--this morning, which may explain why I noticed the problem so quickly. In that case the solution was to tabulate the mapping from PackagePath to *types.Package during type checking and to use that map instead of the types.Packages.Imports graph. But there's no way to do that in the analysis framework.

Perhaps the best solution would be to create the non-syntactic ssa.Packages on demand as they are referenced or needed (most aren't). This would avoid the need for the logic in buildssa (it would be harmless). By good fortune, the public API for Program would seem to allow packages to be created at any time (need a mutex of course). It would also reduce memory usage.

So if I change cmd := exec.Command("go", goArgs...) to cmd := exec.Command("go1.19", goArgs...) in x/tools/internal/gocommand , the imported packages seem to include "crypto/x509" and "crypto/x509/pkix" and the panic goes away. The constant "go1.20" also fails [edit: so it looks like the issue is starting in 1.20]. I am not seeing anything relevant in the release notes about go list changing in 1.20.

I suspect that the difference is the unified export data format. CC @mdempsky

Sorry, I only skimmed this issue, and didn't see Tim's comment about go list.

Just because the bug appeared at 1.19 doesn't mean go list is at fault: it could just be that the reference structure of the program evolved and now there is an indirect reference to a package that cannot be reached by a chain of 'import' edges that is preserved by the export data, revealing the latent bug. (I have no evidence either way, just want to make sure we don't point the finger at the wrong component.)

FWIW, Go 1.20 changed how go/types.Package.Imports works, due to the change to unified IR.

Suppose: You import package P from export data. P imports A and B. B imports C. P's exported API references types from A and C, but not B.

Old behavior: P.Imports() would return {A, C}. {A,B,C}.Imports() would all return nil. (That is, P.Imports misses B, but also includes transitive dependency C; and B's import list is empty, as an incomplete package.)

New behavior: P.Imports() returns {A, B, C}. B.Imports() returns {C}. (That is, Imports now returns the complete transitive closure of all of its imports, for both the directly imported package and any incomplete transitively imported packages.)

See also #54096.

Using Tim's reduced testcase, the import graph of interest is

     main
 ⟶ net/http
 ⟶ crypto/tls
 ⟶ crypto/x509
 ⟶ crypto/x509/pkix

I observe that the Packages.Imports list for net/http includes crypto/tls in all cases, but in go1.19 it also includes crypto/x509 and crypto/x509/pkix, whereas in go1.20 these two edges are missing. So if the intent of the new behavior is to include the complete transitive closure, that suggests there is a bug in the go1.20 exporter.

(But is that really the intent? It would seem to scale poorly: the export data for a package that depends on a vast tree of dependencies would have to name them all, even if it consists of nothing more than an init function and no public declarations.)

Even more minimal testcase:

Target program:

xtools$ head $(find minim2 -type f)
==> minim2/minim2.go <==
package minim2

import "golang.org/x/tools/minim2/a"

var h any = new(a.A)

==> minim2/a/a.go <==
package a

import "golang.org/x/tools/minim2/b"

type A struct {
	b b.B
}

==> minim2/b/b.go <==
package b

import "golang.org/x/tools/minim2/c"

type B struct {
	c c.C
}

==> minim2/c/c.go <==
package c

type C int

func (C) f() {
}

SSA loader program: https://go.dev/play/p/CUqfPBiomIs

The import graph is minim2 -> a -> b -> c. As @mdempsky said, the Packages.Import list with go1.20 export data is transitively closed, so a (loaded from export data) has Imports of b and c. Those packages weren't loaded from export data since they are indirect dependencies and we didn't request that go/packages give us type information for them. But it's surprising that it gave us a non-nil Package with nil Package.Scope and Imports. That seems like a possible bug in its redaction of unrequested information. The inconsistency between what types you can reach from a and what types you can get from c's Scope is what causes the SSA builder to panic.

[Update: This analysis is one of many wrong hypotheses I have come up with today.]

I suspect a bug in flattenImports in the go1.20 unified export data reader: it assumes that Packages.Imports of each of its direct dependencies is already set. But consider minim2 -> a -> b -> c where minim2 is loaded from syntax and a is loaded from export data, and b and c are never loaded as complete packages. (They are just receptacles for symbols created on demand.) When loading a, the Imports of b and c have not been set: they are just partial packages, so the only element in a's Imports list will be b, even though a references types defined in c.

By contrast, the go1.19 reader would set a.Imports = {b,c}.

The invariant we need is of course that the transitive closure of the Imports graph of any Package includes at least the types referenced by that Package. Given that we can't rely on b.Imports or c.Imports, I think the fix must necessarily involve computing reachability over a's Package, just as it used to.

@adonovan The list of imported packages is constructed here: https://cs.opensource.google/go/x/tools/+/master:internal/gcimporter/ureader_yes.go;drc=3cba5a847ff8ab8c232faf4e4310cb2939a835b4;l=265

And each returned package should already have SetImports (and flattenImports) called by time r.pkg() returns.

In your scenario, are {a,b,c} all being created/loaded from export data, or have some of them been prepopulated into the import map?

And each returned package should already have SetImports (and flattenImports) called by time r.pkg() returns.
In your scenario, are {a,b,c} all being created/loaded from export data, or have some of them been prepopulated into the import map?

The import map is prepopulated by go/packages for every package except the one of interest, and they may be incomplete. The export data reader is allowed to add symbols to the incomplete packages already in the map (the whole thing is a critical section), but it can't assume that it created them all.

@adonovan Hm, okay. My impression was the import map could be prepopulated by other invocations of the same importer, but not that users could prepopulate it themselves. That would certainly explain the issue then.

So currently the unified importer short circuits if it sees a package present in the import map, and assumes its import list has been setup. We could probably easily change it to only short circuit when the package is present and has a non-empty import list. (This would mean some extra overhead for leaf packages, but that should be negligible.)

Change https://go.dev/cl/465936 mentions this issue: internal/gcimporter: compute imports for unified IR

Change https://go.dev/cl/467896 mentions this issue: go/internal/gcimporter: restore Go 1.19 Package.SetImports behavior