Discovering unreachable features with deadcode
Alan Donovan
12 December 2023
Features which are a part of your mission’s supply code however can by no means be
reached in any execution are referred to as “lifeless code”, they usually exert a drag
on codebase upkeep efforts.
Right this moment we’re happy to share a device named deadcode
that can assist you determine them.
$ go set up golang.org/x/instruments/cmd/deadcode@newest
$ deadcode -help
The deadcode command reviews unreachable features in Go packages.
Utilization: deadcode [flags] bundle...
Instance
During the last 12 months or so, we’ve been making quite a lot of adjustments to the
construction of gopls, the
language server for Go that powers VS Code and different editors.
A typical change would possibly rewrite some present operate, taking care to
be certain that its new conduct satisfies the wants of all present callers.
Typically, after placing in all that effort, we’d uncover to our
frustration that one of many callers was by no means truly reached in any
execution, so it may safely have been been deleted.
If we had identified this beforehand our refactoring job would have been
simpler.
The straightforward Go program beneath illustrates the issue:
module instance.com/greet
go 1.21
bundle predominant
import "fmt"
func predominant() {
var g Greeter
g = Helloer{}
g.Greet()
}
sort Greeter interface{ Greet() }
sort Helloer struct{}
sort Goodbyer struct{}
var _ Greeter = Helloer{} // Helloer implements Greeter
var _ Greeter = Goodbyer{} // Goodbyer implements Greeter
func (Helloer) Greet() { hi there() }
func (Goodbyer) Greet() { goodbye() }
func hi there() { fmt.Println("hi there") }
func goodbye() { fmt.Println("goodbye") }
Once we execute it, it says hi there:
$ go run .
hi there
It’s clear from its output that this program executes the hi there
operate however not the goodbye
operate.
What’s much less clear at a look is that the goodbye
operate can
by no means be referred to as.
Nevertheless, we will’t merely delete goodbye
, as a result of it’s required by the
Goodbyer.Greet
methodology, which in flip is required to implement the
Greeter
interface whose Greet
methodology we will see is known as from predominant
.
But when we work forwards from predominant, we will see that no Goodbyer
values
are ever created, so the Greet
name in predominant
can solely attain Helloer.Greet
.
That’s the thought behind the algorithm utilized by the deadcode
device.
Once we run deadcode on this program, the device tells us that the
goodbye
operate and the Goodbyer.Greet
methodology are each unreachable:
$ deadcode .
greet.go:23: unreachable func: goodbye
greet.go:20: unreachable func: Goodbyer.Greet
With this information, we will safely take away each features,
together with the Goodbyer
sort itself.
The device may also clarify why the hi there
operate is stay. It responds
with a series of operate calls that reaches hi there
, ranging from predominant:
$ deadcode -whylive=instance.com/greet.hi there .
instance.com/greet.predominant
dynamic@L0008 --> instance.com/greet.Helloer.Greet
static@L0019 --> instance.com/greet.hi there
The output is designed to be simple to learn on a terminal, however you possibly can
use the -json
or -f=template
flags to specify richer output codecs for
consumption by different instruments.
The way it works
The deadcode
command
loads,
parses,
and type-checks the required packages,
then converts them into an
intermediate representation
just like a typical compiler.
It then makes use of an algorithm referred to as
Rapid Type Analysis (RTA)
to construct up the set of features which are reachable,
which is initially simply the entry factors of every predominant
bundle:
the predominant
operate,
and the bundle initializer operate,
which assigns world variables and calls features named init
.
RTA appears on the statements within the physique of every reachable operate to
collect three sorts of knowledge: the set of features it calls immediately;
the set of dynamic calls it makes via interface strategies;
and the set of varieties it converts to an interface.
Direct operate calls are simple: we simply add the callee to the set of
reachable features, and if it’s the primary time we’ve encountered the
callee, we examine its operate physique the identical approach we did for predominant.
Dynamic calls via interface strategies are trickier, as a result of we don’t
know the set of varieties that implement the interface. We don’t need
to imagine that each attainable methodology in this system whose sort matches
is a attainable goal for the decision, as a result of a few of these varieties could
be instantiated solely from lifeless code! That’s why we collect the set of
varieties transformed to interfaces: the conversion makes every of those
varieties reachable from predominant
, in order that its strategies are actually attainable
targets of dynamic calls.
This results in a chicken-and-egg state of affairs. As we encounter every new
reachable operate, we uncover extra interface methodology calls and extra
conversions of concrete varieties to interface varieties.
However because the cross product of those two units (interface methodology calls ×
concrete varieties) grows ever bigger, we uncover new reachable
features.
This class of issues, referred to as “dynamic programming”, might be solved by
(conceptually) making checkmarks in a big two-dimensional desk,
including rows and columns as we go, till there are not any extra checks to
add. The checkmarks within the closing desk tells us what’s reachable;
the clean cells are the lifeless code.
The
predominant
operate causes Helloer
to beinstantiated, and the
g.Greet
namedispatches to the
Greet
methodology of every sort instantiated to this point.Dynamic calls to (non-method) features are handled just like
interfaces of a single methodology.
And calls made using reflection
are thought of to succeed in any methodology of any sort utilized in an interface
conversion, or any sort derivable from one utilizing the replicate
bundle.
However the precept is similar in all circumstances.
Checks
RTA is a whole-program evaluation. Meaning it all the time begins from a
predominant operate and works ahead: you possibly can’t begin from a library
bundle reminiscent of encoding/json
.
Nevertheless, most library packages have checks, and checks have predominant
features. We don’t see them as a result of they’re generated behind the
scenes of go check
, however we will embrace them within the evaluation utilizing the
-test
flag.
If this reviews {that a} operate in a library bundle is lifeless, that’s
an indication that your check protection could possibly be improved.
For instance, this command lists all of the features in encoding/json
that aren’t reached by any of its checks:
$ deadcode -test -filter=encoding/json encoding/json
encoding/json/decode.go:150:31: unreachable func: UnmarshalFieldError.Error
encoding/json/encode.go:225:28: unreachable func: InvalidUTF8Error.Error
(The -filter
flag restricts the output to packages matching the
common expression. By default, the device reviews all packages within the
preliminary module.)
Soundness
All static evaluation instruments
necessarily
produce imperfect approximations of the attainable dynamic
behaviors of the goal program.
A device’s assumptions and inferences could also be “sound”, that means
conservative however maybe overly cautious, or “unsound”, that means
optimistic however not all the time right.
The deadcode device is not any exception: it should approximate the set of
targets of dynamic calls via operate and interface values or
utilizing reflection.
On this respect, the device is sound. In different phrases, if it reviews a
operate as lifeless code, it means the operate can’t be referred to as even
via these dynamic mechanisms. Nevertheless the device could fail to report
some features that in reality can by no means be executed.
The deadcode device should additionally approximate the set of calls created from
features not written in Go, which it can not see.
On this respect, the device will not be sound.
Its evaluation will not be conscious of features referred to as solely from
meeting code, or of the aliasing of features that arises from
the go:linkname
directive.
Fortuitously each of those options are hardly ever used outdoors the Go runtime.
Attempt it out
We run deadcode
periodically on our tasks, particularly after
refactoring work, to assist determine components of this system which are no
longer wanted.
With the lifeless code laid to relaxation, you possibly can deal with eliminating code
whose time has come to an finish however that stubbornly stays alive,
persevering with to empty your life pressure. We name such undead features
“vampire code”!
Please strive it out:
$ go set up golang.org/x/instruments/cmd/deadcode@newest
We’ve discovered it helpful, and we hope you do too.