Skip to content

x/tools/cmd/deadcode: Deadcode not reported with different argument order in workspace #73652

@nicholascapo

Description

@nicholascapo

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.ToolProposalIssues describing a requested change to a Go tool or command-line program.ToolsThis label describes issues relating to any tools in the x/tools repository.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions