whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,648 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.20
package main
import (
"bytes"
_ "embed"
"encoding/json"
"flag"
"fmt"
"go/ast"
"go/token"
"go/types"
"io"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
"sort"
"strings"
"text/template"
"golang.org/x/telemetry"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/callgraph/rta"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
"golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var doc string
// flags
var (
testFlag = flag.Bool("test", false, "include implicit test packages and executables")
tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)")
filterFlag = flag.String("filter", "<module>", "report only packages matching this regular expression (default: module of first package)")
generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files")
whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function")
formatFlag = flag.String("f", "", "format output records using template")
jsonFlag = flag.Bool("json", false, "output JSON records")
cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file")
memProfile = flag.String("memprofile", "", "write memory profile to this file")
)
func usage() {
// Extract the content of the /* ... */ comment in doc.go.
_, after, _ := strings.Cut(doc, "/*\n")
doc, _, _ := strings.Cut(after, "*/")
io.WriteString(flag.CommandLine.Output(), doc+`
Flags:
`)
flag.PrintDefaults()
}
func main() {
telemetry.Start(telemetry.Config{ReportCrashes: true})
log.SetPrefix("deadcode: ")
log.SetFlags(0) // no time prefix
flag.Usage = usage
flag.Parse()
if len(flag.Args()) == 0 {
usage()
os.Exit(2)
}
if *cpuProfile != "" {
f, err := os.Create(*cpuProfile)
if err != nil {
log.Fatal(err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}
// NB: profile won't be written in case of error.
defer pprof.StopCPUProfile()
}
if *memProfile != "" {
f, err := os.Create(*memProfile)
if err != nil {
log.Fatal(err)
}
// NB: profile won't be written in case of error.
defer func() {
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatalf("Writing memory profile: %v", err)
}
f.Close()
}()
}
// Reject bad output options early.
if *formatFlag != "" {
if *jsonFlag {
log.Fatalf("you cannot specify both -f=template and -json")
}
if _, err := template.New("deadcode").Parse(*formatFlag); err != nil {
log.Fatalf("invalid -f: %v", err)
}
}
// Load, parse, and type-check the complete program(s).
cfg := &packages.Config{
BuildFlags: []string{"-tags=" + *tagsFlag},
Mode: packages.LoadAllSyntax | packages.NeedModule,
Tests: *testFlag,
}
initial, err := packages.Load(cfg, flag.Args()...)
if err != nil {
log.Fatalf("Load: %v", err)
}
if len(initial) == 0 {
log.Fatalf("no packages")
}
if packages.PrintErrors(initial) > 0 {
log.Fatalf("packages contain errors")
}
// If -filter is unset, use first module (if available).
if *filterFlag == "<module>" {
if mod := initial[0].Module; mod != nil && mod.Path != "" {
*filterFlag = "^" + regexp.QuoteMeta(mod.Path) + "\\b"
} else {
*filterFlag = "" // match any
}
}
filter, err := regexp.Compile(*filterFlag)
if err != nil {
log.Fatalf("-filter: %v", err)
}
// Create SSA-form program representation
// and find main packages.
prog, pkgs := ssautil.AllPackages(initial, ssa.InstantiateGenerics)
prog.Build()
mains := ssautil.MainPackages(pkgs)
if len(mains) == 0 {
log.Fatalf("no main packages")
}
var roots []*ssa.Function
for _, main := range mains {
roots = append(roots, main.Func("init"), main.Func("main"))
}
// Gather all source-level functions,
// as the user interface is expressed in terms of them.
//
// We ignore synthetic wrappers, and nested functions. Literal
// functions passed as arguments to other functions are of
// course address-taken and there exists a dynamic call of
// that signature, so when they are unreachable, it is
// invariably because the parent is unreachable.
var sourceFuncs []*ssa.Function
generated := make(map[string]bool)
packages.Visit(initial, nil, func(p *packages.Package) {
for _, file := range p.Syntax {
for _, decl := range file.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok {
obj := p.TypesInfo.Defs[decl.Name].(*types.Func)
fn := prog.FuncValue(obj)
sourceFuncs = append(sourceFuncs, fn)
}
}
if isGenerated(file) {
generated[p.Fset.File(file.Pos()).Name()] = true
}
}
})
// Compute the reachabilty from main.
// (Build a call graph only for -whylive.)
res := rta.Analyze(roots, *whyLiveFlag != "")
// Subtle: the -test flag causes us to analyze test variants
// such as "package p as compiled for p.test" or even "for q.test".
// This leads to multiple distinct ssa.Function instances that
// represent the same source declaration, and it is essentially
// impossible to discover this from the SSA representation
// (since it has lost the connection to go/packages.Package.ID).
//
// So, we de-duplicate such variants by position:
// if any one of them is live, we consider all of them live.
// (We use Position not Pos to avoid assuming that files common
// to packages "p" and "p [p.test]" were parsed only once.)
reachablePosn := make(map[token.Position]bool)
for fn := range res.Reachable {
if fn.Pos().IsValid() || fn.Name() == "init" {
reachablePosn[prog.Fset.Position(fn.Pos())] = true
}
}
// The -whylive=fn flag causes deadcode to explain why a function
// is not dead, by showing a path to it from some root.
if *whyLiveFlag != "" {
targets := make(map[*ssa.Function]bool)
for _, fn := range sourceFuncs {
if prettyName(fn, true) == *whyLiveFlag {
targets[fn] = true
}
}
if len(targets) == 0 {
// Function is not part of the program.
//
// TODO(adonovan): improve the UX here in case
// of spelling or syntax mistakes. Some ideas:
// - a cmd/callgraph command to enumerate
// available functions.
// - a deadcode -live flag to compute the complement.
// - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method
// - report the element of AllFunctions with the smallest
// Levenshtein distance from *whyLiveFlag.
// - permit -whylive=regexp. But beware of spurious
// matches (e.g. fmt.Print matches fmt.Println)
// and the annoyance of having to quote parens (*T).f.
log.Fatalf("function %q not found in program", *whyLiveFlag)
}
// Opt: remove the unreachable ones.
for fn := range targets {
if !reachablePosn[prog.Fset.Position(fn.Pos())] {
delete(targets, fn)
}
}
if len(targets) == 0 {
log.Fatalf("function %s is dead code", *whyLiveFlag)
}
res.CallGraph.DeleteSyntheticNodes() // inline synthetic wrappers (except inits)
root, path := pathSearch(roots, res, targets)
if root == nil {
// RTA doesn't add callgraph edges for reflective calls.
log.Fatalf("%s is reachable only through reflection", *whyLiveFlag)
}
if len(path) == 0 {
// No edges => one of the targets is a root.
// Rather than (confusingly) print nothing, make this an error.
log.Fatalf("%s is a root", root.Func)
}
// Build a list of jsonEdge records
// to print as -json or -f=template.
var edges []any
for _, edge := range path {
edges = append(edges, jsonEdge{
Initial: cond(len(edges) == 0, prettyName(edge.Caller.Func, true), ""),
Kind: cond(isStaticCall(edge), "static", "dynamic"),
Position: toJSONPosition(prog.Fset.Position(edge.Pos())),
Callee: prettyName(edge.Callee.Func, true),
})
}
format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Position.Line .Callee}}`
if *formatFlag != "" {
format = *formatFlag
}
printObjects(format, edges)
return
}
// Group unreachable functions by package path.
byPkgPath := make(map[string]map[*ssa.Function]bool)
for _, fn := range sourceFuncs {
posn := prog.Fset.Position(fn.Pos())
if !reachablePosn[posn] {
reachablePosn[posn] = true // suppress dups with same pos
pkgpath := fn.Pkg.Pkg.Path()
m, ok := byPkgPath[pkgpath]
if !ok {
m = make(map[*ssa.Function]bool)
byPkgPath[pkgpath] = m
}
m[fn] = true
}
}
// Build array of jsonPackage objects.
var packages []any
pkgpaths := keys(byPkgPath)
sort.Strings(pkgpaths)
for _, pkgpath := range pkgpaths {
if !filter.MatchString(pkgpath) {
continue
}
m := byPkgPath[pkgpath]
// Print functions that appear within the same file in
// declaration order. This tends to keep related
// methods such as (T).Marshal and (*T).Unmarshal
// together better than sorting.
fns := keys(m)
sort.Slice(fns, func(i, j int) bool {
xposn := prog.Fset.Position(fns[i].Pos())
yposn := prog.Fset.Position(fns[j].Pos())
if xposn.Filename != yposn.Filename {
return xposn.Filename < yposn.Filename
}
return xposn.Line < yposn.Line
})
var functions []jsonFunction
for _, fn := range fns {
posn := prog.Fset.Position(fn.Pos())
// Without -generated, skip functions declared in
// generated Go files.
// (Functions called by them may still be reported.)
gen := generated[posn.Filename]
if gen && !*generatedFlag {
continue
}
functions = append(functions, jsonFunction{
Name: prettyName(fn, false),
Position: toJSONPosition(posn),
Generated: gen,
})
}
if len(functions) > 0 {
packages = append(packages, jsonPackage{
Name: fns[0].Pkg.Pkg.Name(),
Path: pkgpath,
Funcs: functions,
})
}
}
// Default line-oriented format: "a/b/c.go:1:2: unreachable func: T.f"
format := `{{range .Funcs}}{{printf "%s: unreachable func: %s\n" .Position .Name}}{{end}}`
if *formatFlag != "" {
format = *formatFlag
}
printObjects(format, packages)
}
// prettyName is a fork of Function.String designed to reduce
// go/ssa's fussy punctuation symbols, e.g. "(*pkg.T).F" -> "pkg.T.F".
//
// It only works for functions that remain after
// callgraph.Graph.DeleteSyntheticNodes: source-level named functions
// and methods, their anonymous functions, and synthetic package
// initializers.
func prettyName(fn *ssa.Function, qualified bool) string {
var buf strings.Builder
// optional package qualifier
if qualified && fn.Pkg != nil {
fmt.Fprintf(&buf, "%s.", fn.Pkg.Pkg.Path())
}
var format func(*ssa.Function)
format = func(fn *ssa.Function) {
// anonymous?
if fn.Parent() != nil {
format(fn.Parent())
i := index(fn.Parent().AnonFuncs, fn)
fmt.Fprintf(&buf, "$%d", i+1)
return
}
// method receiver?
if recv := fn.Signature.Recv(); recv != nil {
_, named := typesinternal.ReceiverNamed(recv)
buf.WriteString(named.Obj().Name())
buf.WriteByte('.')
}
// function/method name
buf.WriteString(fn.Name())
}
format(fn)
return buf.String()
}
// printObjects formats an array of objects, either as JSON or using a
// template, following the manner of 'go list (-json|-f=template)'.
func printObjects(format string, objects []any) {
if *jsonFlag {
out, err := json.MarshalIndent(objects, "", "\t")
if err != nil {
log.Fatalf("internal error: %v", err)
}
os.Stdout.Write(out)
return
}
// -f=template. Parse can't fail: we checked it earlier.
tmpl := template.Must(template.New("deadcode").Parse(format))
for _, object := range objects {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, object); err != nil {
log.Fatal(err)
}
if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' {
buf.WriteByte('\n')
}
os.Stdout.Write(buf.Bytes())
}
}
// TODO(adonovan): use go1.21's ast.IsGenerated.
// isGenerated reports whether the file was generated by a program,
// not handwritten, by detecting the special comment described
// at https://go.dev/s/generatedcode.
//
// The syntax tree must have been parsed with the ParseComments flag.
// Example:
//
// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
// if err != nil { ... }
// gen := ast.IsGenerated(f)
func isGenerated(file *ast.File) bool {
_, ok := generator(file)
return ok
}
func generator(file *ast.File) (string, bool) {
for _, group := range file.Comments {
for _, comment := range group.List {
if comment.Pos() > file.Package {
break // after package declaration
}
// opt: check Contains first to avoid unnecessary array allocation in Split.
const prefix = "// Code generated "
if strings.Contains(comment.Text, prefix) {
for _, line := range strings.Split(comment.Text, "\n") {
if rest, ok := strings.CutPrefix(line, prefix); ok {
if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok {
return gen, true
}
}
}
}
}
}
return "", false
}
// pathSearch returns the shortest path from one of the roots to one
// of the targets (along with the root itself), or zero if no path was found.
func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
// Search breadth-first (for shortest path) from the root.
//
// We don't use the virtual CallGraph.Root node as we wish to
// choose the order in which we search entrypoints:
// non-test packages before test packages,
// main functions before init functions.
// Sort roots into preferred order.
importsTesting := func(fn *ssa.Function) bool {
isTesting := func(p *types.Package) bool { return p.Path() == "testing" }
return containsFunc(fn.Pkg.Pkg.Imports(), isTesting)
}
sort.Slice(roots, func(i, j int) bool {
x, y := roots[i], roots[j]
xtest := importsTesting(x)
ytest := importsTesting(y)
if xtest != ytest {
return !xtest // non-tests before tests
}
xinit := x.Name() == "init"
yinit := y.Name() == "init"
if xinit != yinit {
return !xinit // mains before inits
}
return false
})
search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) {
// seen maps each encountered node to its predecessor on the
// path to a root node, or to nil for root itself.
seen := make(map[*callgraph.Node]*callgraph.Edge)
bfs := func(root *callgraph.Node) []*callgraph.Edge {
queue := []*callgraph.Node{root}
seen[root] = nil
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
// found a path?
if targets[node.Func] {
path := []*callgraph.Edge{} // non-nil in case len(path)=0
for {
edge := seen[node]
if edge == nil {
reverse(path)
return path
}
path = append(path, edge)
node = edge.Caller
}
}
for _, edge := range node.Out {
if allowDynamic || isStaticCall(edge) {
if _, ok := seen[edge.Callee]; !ok {
seen[edge.Callee] = edge
queue = append(queue, edge.Callee)
}
}
}
}
return nil
}
for _, rootFn := range roots {
root := res.CallGraph.Nodes[rootFn]
if root == nil {
// Missing call graph node for root.
// TODO(adonovan): seems like a bug in rta.
continue
}
if path := bfs(root); path != nil {
return root, path
}
}
return nil, nil
}
for _, allowDynamic := range []bool{false, true} {
if root, path := search(allowDynamic); path != nil {
return root, path
}
}
return nil, nil
}
// -- utilities --
func isStaticCall(edge *callgraph.Edge) bool {
return edge.Site != nil && edge.Site.Common().StaticCallee() != nil
}
var cwd, _ = os.Getwd()
func toJSONPosition(posn token.Position) jsonPosition {
// Use cwd-relative filename if possible.
filename := posn.Filename
if rel, err := filepath.Rel(cwd, filename); err == nil && !strings.HasPrefix(rel, "..") {
filename = rel
}
return jsonPosition{filename, posn.Line, posn.Column}
}
func cond[T any](cond bool, t, f T) T {
if cond {
return t
} else {
return f
}
}
// -- output protocol (for JSON or text/template) --
// Keep in sync with doc comment!
type jsonFunction struct {
Name string // name (sans package qualifier)
Position jsonPosition // file/line/column of declaration
Generated bool // function is declared in a generated .go file
}
func (f jsonFunction) String() string { return f.Name }
type jsonPackage struct {
Name string // declared name
Path string // full import path
Funcs []jsonFunction // non-empty list of package's dead functions
}
func (p jsonPackage) String() string { return p.Path }
// The Initial and Callee names are package-qualified.
type jsonEdge struct {
Initial string `json:",omitempty"` // initial entrypoint (main or init); first edge only
Kind string // = static | dynamic
Position jsonPosition
Callee string
}
type jsonPosition struct {
File string
Line, Col int
}
func (p jsonPosition) String() string {
return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col)
}
// -- from the future --
// TODO(adonovan): use go1.22's slices and maps packages.
func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
return indexFunc(s, f) >= 0
}
func indexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
}
}
return -1
}
func index[S ~[]E, E comparable](s S, v E) int {
for i := range s {
if v == s[i] {
return i
}
}
return -1
}
func reverse[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func keys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
@@ -0,0 +1,184 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.20
package main_test
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/txtar"
)
// Test runs the deadcode command on each scenario
// described by a testdata/*.txtar file.
func Test(t *testing.T) {
testenv.NeedsTool(t, "go")
if runtime.GOOS == "android" {
t.Skipf("the dependencies are not available on android")
}
exe := buildDeadcode(t)
matches, err := filepath.Glob("testdata/*.txtar")
if err != nil {
t.Fatal(err)
}
for _, filename := range matches {
filename := filename
t.Run(filename, func(t *testing.T) {
t.Parallel()
ar, err := txtar.ParseFile(filename)
if err != nil {
t.Fatal(err)
}
// Write the archive files to the temp directory.
tmpdir := t.TempDir()
for _, f := range ar.Files {
filename := filepath.Join(tmpdir, f.Name)
if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filename, f.Data, 0666); err != nil {
t.Fatal(err)
}
}
// Parse archive comment as directives of these forms:
//
// [!]deadcode args... command-line arguments
// [!]want arg expected/unwanted string in output (or stderr)
//
// Args may be Go-quoted strings.
type testcase struct {
linenum int
args []string
wantErr bool
want map[string]bool // string -> sense
}
var cases []*testcase
var current *testcase
for i, line := range strings.Split(string(ar.Comment), "\n") {
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue // skip blanks and comments
}
words, err := words(line)
if err != nil {
t.Fatalf("cannot break line into words: %v (%s)", err, line)
}
switch kind := words[0]; kind {
case "deadcode", "!deadcode":
current = &testcase{
linenum: i + 1,
want: make(map[string]bool),
args: words[1:],
wantErr: kind[0] == '!',
}
cases = append(cases, current)
case "want", "!want":
if current == nil {
t.Fatalf("'want' directive must be after 'deadcode'")
}
if len(words) != 2 {
t.Fatalf("'want' directive needs argument <<%s>>", line)
}
current.want[words[1]] = kind[0] != '!'
default:
t.Fatalf("%s: invalid directive %q", filename, kind)
}
}
for _, tc := range cases {
t.Run(fmt.Sprintf("L%d", tc.linenum), func(t *testing.T) {
// Run the command.
cmd := exec.Command(exe, tc.args...)
cmd.Stdout = new(bytes.Buffer)
cmd.Stderr = new(bytes.Buffer)
cmd.Dir = tmpdir
cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
var got string
if err := cmd.Run(); err != nil {
if !tc.wantErr {
t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
}
got = fmt.Sprint(cmd.Stderr)
} else {
if tc.wantErr {
t.Fatalf("deadcode succeeded unexpectedly (stdout=%s)", cmd.Stdout)
}
got = fmt.Sprint(cmd.Stdout)
}
// Check each want directive.
for str, sense := range tc.want {
ok := true
if strings.Contains(got, str) != sense {
if sense {
t.Errorf("missing %q", str)
} else {
t.Errorf("unwanted %q", str)
}
ok = false
}
if !ok {
t.Errorf("got: <<%s>>", got)
}
}
})
}
})
}
}
// buildDeadcode builds the deadcode executable.
// It returns its path, and a cleanup function.
func buildDeadcode(t *testing.T) string {
bin := filepath.Join(t.TempDir(), "deadcode")
if runtime.GOOS == "windows" {
bin += ".exe"
}
cmd := exec.Command("go", "build", "-o", bin)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Building deadcode: %v\n%s", err, out)
}
return bin
}
// words breaks a string into words, respecting
// Go string quotations around words with spaces.
func words(s string) ([]string, error) {
var words []string
for s != "" {
s = strings.TrimSpace(s)
var word string
if s[0] == '"' || s[0] == '`' {
prefix, err := strconv.QuotedPrefix(s)
if err != nil {
return nil, err
}
s = s[len(prefix):]
word, _ = strconv.Unquote(prefix)
} else {
prefix, rest, _ := strings.Cut(s, " ")
s = rest
word = prefix
}
words = append(words, word)
}
return words, nil
}
@@ -0,0 +1,138 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
The deadcode command reports unreachable functions in Go programs.
Usage: deadcode [flags] package...
The deadcode command loads a Go program from source then uses Rapid
Type Analysis (RTA) to build a call graph of all the functions
reachable from the program's main function. Any functions that are not
reachable are reported as dead code, grouped by package.
Packages are expressed in the notation of 'go list' (or other
underlying build system if you are using an alternative
golang.org/x/go/packages driver). Only executable (main) packages are
considered starting points for the analysis.
The -test flag causes it to analyze test executables too. Tests
sometimes make use of functions that would otherwise appear to be dead
code, and public API functions reported as dead with -test indicate
possible gaps in your test coverage. Bear in mind that an Example test
function without an "Output:" comment is merely documentation:
it is dead code, and does not contribute coverage.
The -filter flag restricts results to packages that match the provided
regular expression; its default value is the module name of the first
package. Use -filter= to display all results.
Example: show all dead code within the gopls module:
$ deadcode -test golang.org/x/tools/gopls/...
The analysis can soundly analyze dynamic calls though func values,
interface methods, and reflection. However, it does not currently
understand the aliasing created by //go:linkname directives, so it
will fail to recognize that calls to a linkname-annotated function
with no body in fact dispatch to the function named in the annotation.
This may result in the latter function being spuriously reported as dead.
By default, the tool does not report dead functions in generated files,
as determined by the special comment described in
https://go.dev/s/generatedcode. Use the -generated flag to include them.
In any case, just because a function is reported as dead does not mean
it is unconditionally safe to delete it. For example, a dead function
may be referenced by another dead function, and a dead method may be
required to satisfy an interface that is never called.
Some judgement is required.
The analysis is valid only for a single GOOS/GOARCH/-tags configuration,
so a function reported as dead may be live in a different configuration.
Consider running the tool once for each configuration of interest.
Consider using a line-oriented output format (see below) to make it
easier to compute the intersection of results across all runs.
# Output
The command supports three output formats.
With no flags, the command prints the name and location of each dead
function in the form of a typical compiler diagnostic, for example:
$ deadcode -f='{{range .Funcs}}{{println .Position}}{{end}}' -test ./gopls/...
gopls/internal/protocol/command.go:1206:6: unreachable func: openClientEditor
gopls/internal/template/parse.go:414:18: unreachable func: Parsed.WriteNode
gopls/internal/template/parse.go:419:18: unreachable func: wrNode.writeNode
With the -json flag, the command prints an array of Package
objects, as defined by the JSON schema (see below).
With the -f=template flag, the command executes the specified template
on each Package record. So, this template shows dead functions grouped
by package:
$ deadcode -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .Name}}{{end}}{{println}}' -test ./gopls/...
golang.org/x/tools/gopls/internal/lsp
openClientEditor
golang.org/x/tools/gopls/internal/template
Parsed.WriteNode
wrNode.writeNode
# Why is a function not dead?
The -whylive=function flag explain why the named function is not dead
by showing an arbitrary shortest path to it from one of the main functions.
(To enumerate the functions in a program, or for more sophisticated
call graph queries, use golang.org/x/tools/cmd/callgraph.)
Fully static call paths are preferred over paths involving dynamic
edges, even if longer. Paths starting from a non-test package are
preferred over those from tests. Paths from main functions are
preferred over paths from init functions.
The result is a list of Edge objects (see JSON schema below).
Again, the -json and -f=template flags may be used to control
the formatting of the list of Edge objects.
The default format shows, for each edge in the path, whether the call
is static or dynamic, and its source line number. For example:
$ deadcode -whylive=bytes.Buffer.String -test ./cmd/deadcode/...
golang.org/x/tools/cmd/deadcode.main
static@L0117 --> golang.org/x/tools/go/packages.Load
static@L0262 --> golang.org/x/tools/go/packages.defaultDriver
static@L0305 --> golang.org/x/tools/go/packages.goListDriver
static@L0153 --> golang.org/x/tools/go/packages.goListDriver$1
static@L0154 --> golang.org/x/tools/go/internal/packagesdriver.GetSizesForArgsGolist
static@L0044 --> bytes.Buffer.String
# JSON schema
type Package struct {
Name string // declared name
Path string // full import path
Funcs []Function // list of dead functions within it
}
type Function struct {
Name string // name (sans package qualifier)
Position Position // file/line/column of function declaration
Generated bool // function is declared in a generated .go file
}
type Edge struct {
Initial string // initial entrypoint (main or init); first edge only
Kind string // = static | dynamic
Position Position // file/line/column of call site
Callee string // target of the call
}
type Position struct {
File string // name of file
Line, Col int // line and byte index, both 1-based
}
*/
package main
@@ -0,0 +1,38 @@
# Test of basic functionality.
deadcode -filter= example.com
want "T.Goodbye"
want "T.Goodbye2"
want "T.Goodbye3"
!want "T.Hello"
want "unreferenced"
want "Scanf"
want "Printf"
!want "Println"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
import "fmt"
type T int
func main() {
var x T
x.Hello()
}
func (T) Hello() { fmt.Println("hello") }
func (T) Goodbye() { fmt.Println("goodbye") }
func (*T) Goodbye2() { fmt.Println("goodbye2") }
func (*A) Goodbye3() { fmt.Println("goodbye3") }
type A = T
func unreferenced() {}
@@ -0,0 +1,39 @@
# Test of -filter flag.
deadcode -filter=other.net example.com
want `other.net`
want `Dead`
!want `Live`
!want `example.com`
!want `unreferenced`
-- go.work --
use example.com
use other.net
-- example.com/go.mod --
module example.com
go 1.18
-- example.com/main.go --
package main
import "other.net"
func main() {
other.Live()
}
func unreferenced() {}
-- other.net/go.mod --
module other.net
go 1.18
-- other.net/other.go --
package other
func Live() {}
func Dead() {}
@@ -0,0 +1,28 @@
# Test of -generated flag output.
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" example.com
!want "main.main"
want "main.Dead1"
!want "main.Dead2"
deadcode "-f={{range .Funcs}}{{$.Name}}.{{.Name}}{{end}}" -generated example.com
!want "main.main"
want "main.Dead1"
want "main.Dead2"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
func main() {}
func Dead1() {}
-- gen.go --
// Code generated by hand. DO NOT EDIT.
package main
func Dead2() {}
@@ -0,0 +1,44 @@
# Regression test for issue 65915: the enumeration of source-level
# functions used the flawed ssautil.AllFunctions, causing it to
# miss some unexported ones.
deadcode -filter= example.com
want "unreachable func: example.UnUsed"
want "unreachable func: example.unUsed"
want "unreachable func: PublicExample.UnUsed"
want "unreachable func: PublicExample.unUsed"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
type example struct{}
func (e example) UnUsed() {}
func (e example) Used() {}
func (e example) unUsed() {}
func (e example) used() {}
type PublicExample struct{}
func (p PublicExample) UnUsed() {}
func (p PublicExample) Used() {}
func (p PublicExample) unUsed() {}
func (p PublicExample) used() {}
func main() {
example{}.Used()
example{}.used()
PublicExample{}.Used()
PublicExample{}.used()
}
@@ -0,0 +1,37 @@
# Test of -whylive with reflective call
# (regression test for golang/go#67915).
# The live function is reached via reflection:
deadcode example.com
want "unreachable func: dead"
!want "unreachable func: live"
# Reflective calls have Edge.Site=nil, which formerly led to a crash
# when -whylive would compute its position. Now it has NoPos.
deadcode -whylive=example.com.live example.com
want " example.com.main"
want " static@L0006 --> reflect.Value.Call"
want "dynamic@L0000 --> example.com.live"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
import "reflect"
func main() {
reflect.ValueOf(live).Call(nil)
}
func live() {
println("hello")
}
func dead() {
println("goodbye")
}
@@ -0,0 +1,23 @@
# Very minimal test of -json flag.
deadcode -json example.com/p
want `"Path": "example.com/p",`
want `"Name": "DeadFunc",`
want `"Generated": false`
want `"Line": 5,`
want `"Col": 6`
-- go.mod --
module example.com
go 1.18
-- p/p.go --
package main
func main() {}
func DeadFunc() {}
type T int
func (*T) DeadMethod() {}
@@ -0,0 +1,32 @@
# Test of line-oriented output.
deadcode `-f={{range .Funcs}}{{printf "%s: %s.%s\n" .Position $.Path .Name}}{{end}}` -filter= example.com
want "main.go:13:10: example.com.T.Goodbye"
!want "example.com.T.Hello"
want "main.go:15:6: example.com.unreferenced"
want "fmt.Scanf"
want "fmt.Printf"
!want "fmt.Println"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
import "fmt"
type T int
func main() {
var x T
x.Hello()
}
func (T) Hello() { fmt.Println("hello") }
func (T) Goodbye() { fmt.Println("goodbye") }
func unreferenced() {}
@@ -0,0 +1,42 @@
# Test of -test flag.
deadcode -test -filter=example.com example.com/p
want "Dead"
!want "Live1"
!want "Live2"
want "ExampleDead"
!want "ExampleLive"
-- go.mod --
module example.com
go 1.18
-- p/p.go --
package p
func Live1() {}
func Live2() {}
func Dead() {}
-- p/p_test.go --
package p_test
import "example.com/p"
import "testing"
func Test(t *testing.T) {
p.Live1()
}
func ExampleLive() {
p.Live2()
// Output:
}
// A test Example function without an "Output:" comment is never executed.
func ExampleDead() {
p.Dead()
}
@@ -0,0 +1,133 @@
# Test of -whylive flag.
# The -whylive argument must be live.
!deadcode -whylive=example.com.d example.com
want "function example.com.d is dead code"
# A fully static path is preferred, even if longer.
deadcode -whylive=example.com.c example.com
want " example.com.main"
want " static@L0004 --> example.com.a"
want " static@L0009 --> example.com.b"
want " static@L0012 --> example.com.c"
# Dynamic edges are followed if necessary.
# (Note that main is preferred over init.)
deadcode -whylive=example.com.f example.com
want " example.com.main"
want "dynamic@L0006 --> example.com.e"
want " static@L0017 --> example.com.f"
# Degenerate case where target is itself a root.
!deadcode -whylive=example.com.main example.com
want "example.com.main is a root"
# Test of path through (*T).m method wrapper.
deadcode -whylive=example.com/p.live example.com/p
want " example.com/p.main"
want "static@L0006 --> example.com/p.E.Error"
want "static@L0010 --> example.com/p.live"
# Test of path through (I).m interface method wrapper (thunk).
deadcode -whylive=example.com/q.live example.com/q
want " example.com/q.main"
want "static@L0006 --> example.com/q.E.Error"
want "static@L0010 --> example.com/q.live"
# Test of path through synthetic package initializer,
# a declared package initializer, and its anonymous function.
deadcode -whylive=example.com/q.live2 example.com/q
want " example.com/q.init"
want "static@L0000 --> example.com/q.init#1"
want "static@L0016 --> example.com/q.init#1$1"
want "static@L0015 --> example.com/q.live2"
# Test of path through synthetic package initializer,
# and a global var initializer.
deadcode -whylive=example.com/r.live example.com/r
want " example.com/r.init"
want "static@L0007 --> example.com/r.init$1"
want "static@L0006 --> example.com/r.live"
-- go.mod --
module example.com
go 1.18
-- main.go --
package main
func main() {
a()
println(c, e) // c, e are address-taken
(func ())(nil)() // potential dynamic call to c, e
}
func a() {
b()
}
func b() {
c()
}
func c()
func d()
func e() {
f()
}
func f()
func init() {
(func ())(nil)() // potential dynamic call to c, e
}
-- p/p.go --
package main
func main() {
f := (*E).Error
var e E
f(&e)
}
type E int
func (E) Error() string { return live() }
func live() string
-- q/q.go --
package main
func main() {
f := error.Error
var e E
f(e)
}
type E int
func (E) Error() string { return live() }
func live() string
func init() {
f := func() { live2() }
f()
}
func live2()
-- r/r.go --
package main
func main() {}
var x = func() int {
return live()
}()
func live() int