-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
Go version
go version go1.24.2 linux/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/nicholas/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/nicholas/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3441140769=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/nicholas/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/nicholas/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go-1.24'
GOSUMDB='sum.golang.org'
GOTELEMETRY='on'
GOTELEMETRYDIR='/home/nicholas/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/go-1.24/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.2'
GOWORK='/home/nicholas/reproduction/go.work'
PKG_CONFIG='pkg-config'
What did you do?
Argument order can hide some dead code
$ go run golang.org/x/tools/cmd/deadcode@v0.33.0 ./svc/... ./lib/...
$ go run golang.org/x/tools/cmd/deadcode@v0.33.0 ./lib/... ./svc/...
lib/a/a.go:3:6: unreachable func: A
Real Life example
At $work
we use a private monorepo with several services, along with library packages that are shared across services.
Each service may contain several other packages that are not shared with other services.
In addition we has a "tools" module that doesn't import much from the library.
Each service is a module, tools is a module, and the library is a module.
The root of the repo is a workspace.
Linter
We have an internal linter that runs deadcode
and allows us to ignore certain dead functions (etc) based on //nolint:deadcode
comments.
So we expect some deadcode to be reported as dead, and then our linter can ignore it.
The linter also reports when code with a //nolint:deadcode
comment was not reported as dead (that way we can remove the comment).
This works pretty well, but isn't actually relevant to the issue.
Problem
It was discovered that running deadcode
across all the modules sometimes produces a much smaller report than other times.
This appears to happen only in specific circumstances, but it is reliable to reproduce.
Additionally, we were able to reproduce it with some package names, but not others.
In one specific case, adding tools/a/a.go
with a single package a
line (no added dead code), causes deadcode
to fail to report dead code (from another module).
But moving that same file to tools/x/x.go
does not trigger the bug.
While debugging, we discovered that the order of package arguments to deadcode
reliably causes it to fail.
The difference is remarkable too: Depending on the order of the arguments, deadcode
will report either one or several thousand lines.
Minimal Reproduction
Setup
# create a base directory
mkdir reproduction
cd reproduction
# make library
mkdir lib
cd lib
go mod init lib
# library source
mkdir a
echo 'package a \n\n func A(){}\n' > a/a.go
cd ..
# make service
mkdir svc
cd svc
go mod init svc
# service source
mkdir s
echo 'package main \n\n func main(){ println("main") }\n' > s/main.go
cd ..
# make workspace
go work init
go work use -r .
# build (to confirm correctness)
go build -v ./lib/... ./svc/...
$ tree
.
├── go.work
├── lib
│ ├── a
│ │ └── a.go
│ └── go.mod
└── svc
├── go.mod
└── s
└── main.go
This is an example workspace with a service and library module, which each contain a single package.
The only main()
is in svc/s/main.go
and it does not import lib/a
.
Therefore lib/a.A()
is unused and dead.
But this is only found based on the order of the arguments to deadcode
:
$ go run golang.org/x/tools/cmd/deadcode@v0.33.0 ./svc/... ./lib/...
$ go run golang.org/x/tools/cmd/deadcode@v0.33.0 ./lib/... ./svc/...
lib/a/a.go:3:6: unreachable func: A
What did you see happen?
Based on argument order, in a workspace, deadcode
may fail to report dead code
What did you expect to see?
Reporting of dead code is consistent even with different argument orders