// 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", "", "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 == "" { 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.Site.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 }