src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

deadcode.go (17911B)


      1 // Copyright 2023 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package main
      6 
      7 import (
      8 	"bytes"
      9 	_ "embed"
     10 	"encoding/json"
     11 	"flag"
     12 	"fmt"
     13 	"go/ast"
     14 	"go/token"
     15 	"go/types"
     16 	"io"
     17 	"log"
     18 	"maps"
     19 	"os"
     20 	"path/filepath"
     21 	"regexp"
     22 	"runtime"
     23 	"runtime/pprof"
     24 	"slices"
     25 	"sort"
     26 	"strings"
     27 	"text/template"
     28 
     29 	"golang.org/x/telemetry"
     30 	"golang.org/x/tools/go/callgraph"
     31 	"golang.org/x/tools/go/callgraph/rta"
     32 	"golang.org/x/tools/go/packages"
     33 	"golang.org/x/tools/go/ssa"
     34 	"golang.org/x/tools/go/ssa/ssautil"
     35 	"golang.org/x/tools/internal/typesinternal"
     36 )
     37 
     38 //go:embed doc.go
     39 var doc string
     40 
     41 // flags
     42 var (
     43 	testFlag = flag.Bool("test", false, "include implicit test packages and executables")
     44 	tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)")
     45 
     46 	filterFlag    = flag.String("filter", "<module>", "report only packages matching this regular expression (default: module of first package)")
     47 	generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files")
     48 	whyLiveFlag   = flag.String("whylive", "", "show a path from main to the named function")
     49 	formatFlag    = flag.String("f", "", "format output records using template")
     50 	jsonFlag      = flag.Bool("json", false, "output JSON records")
     51 	cpuProfile    = flag.String("cpuprofile", "", "write CPU profile to this file")
     52 	memProfile    = flag.String("memprofile", "", "write memory profile to this file")
     53 )
     54 
     55 func usage() {
     56 	// Extract the content of the /* ... */ comment in doc.go.
     57 	_, after, _ := strings.Cut(doc, "/*\n")
     58 	doc, _, _ := strings.Cut(after, "*/")
     59 	io.WriteString(flag.CommandLine.Output(), doc+`
     60 Flags:
     61 
     62 `)
     63 	flag.PrintDefaults()
     64 }
     65 
     66 func main() {
     67 	telemetry.Start(telemetry.Config{ReportCrashes: true})
     68 
     69 	log.SetPrefix("deadcode: ")
     70 	log.SetFlags(0) // no time prefix
     71 
     72 	flag.Usage = usage
     73 	flag.Parse()
     74 	if len(flag.Args()) == 0 {
     75 		usage()
     76 		os.Exit(2)
     77 	}
     78 
     79 	if *cpuProfile != "" {
     80 		f, err := os.Create(*cpuProfile)
     81 		if err != nil {
     82 			log.Fatal(err)
     83 		}
     84 		if err := pprof.StartCPUProfile(f); err != nil {
     85 			log.Fatal(err)
     86 		}
     87 		// NB: profile won't be written in case of error.
     88 		defer pprof.StopCPUProfile()
     89 	}
     90 
     91 	if *memProfile != "" {
     92 		f, err := os.Create(*memProfile)
     93 		if err != nil {
     94 			log.Fatal(err)
     95 		}
     96 		// NB: profile won't be written in case of error.
     97 		defer func() {
     98 			runtime.GC() // get up-to-date statistics
     99 			if err := pprof.WriteHeapProfile(f); err != nil {
    100 				log.Fatalf("Writing memory profile: %v", err)
    101 			}
    102 			f.Close()
    103 		}()
    104 	}
    105 
    106 	// Reject bad output options early.
    107 	if *formatFlag != "" {
    108 		if *jsonFlag {
    109 			log.Fatalf("you cannot specify both -f=template and -json")
    110 		}
    111 		if _, err := template.New("deadcode").Parse(*formatFlag); err != nil {
    112 			log.Fatalf("invalid -f: %v", err)
    113 		}
    114 	}
    115 
    116 	// Load, parse, and type-check the complete program(s).
    117 	cfg := &packages.Config{
    118 		BuildFlags: []string{"-tags=" + *tagsFlag},
    119 		Mode:       packages.LoadAllSyntax | packages.NeedModule,
    120 		Tests:      *testFlag,
    121 	}
    122 	initial, err := packages.Load(cfg, flag.Args()...)
    123 	if err != nil {
    124 		log.Fatalf("Load: %v", err)
    125 	}
    126 	if len(initial) == 0 {
    127 		log.Fatalf("no packages")
    128 	}
    129 	if packages.PrintErrors(initial) > 0 {
    130 		log.Fatalf("packages contain errors")
    131 	}
    132 
    133 	// If -filter is unset, use first module (if available).
    134 	if *filterFlag == "<module>" {
    135 		seen := make(map[string]bool)
    136 		var patterns []string
    137 		for _, pkg := range initial {
    138 			if pkg.Module != nil && pkg.Module.Path != "" && !seen[pkg.Module.Path] {
    139 				seen[pkg.Module.Path] = true
    140 				patterns = append(patterns, regexp.QuoteMeta(pkg.Module.Path))
    141 			}
    142 		}
    143 
    144 		if patterns != nil {
    145 			*filterFlag = "^(" + strings.Join(patterns, "|") + ")\\b"
    146 		} else {
    147 			*filterFlag = "" // match any
    148 		}
    149 	}
    150 	filter, err := regexp.Compile(*filterFlag)
    151 	if err != nil {
    152 		log.Fatalf("-filter: %v", err)
    153 	}
    154 
    155 	// Create SSA-form program representation
    156 	// and find main packages.
    157 	prog, pkgs := ssautil.AllPackages(initial, ssa.InstantiateGenerics)
    158 	prog.Build()
    159 
    160 	mains := ssautil.MainPackages(pkgs)
    161 	if len(mains) == 0 {
    162 		log.Fatalf("no main packages")
    163 	}
    164 	var roots []*ssa.Function
    165 	for _, main := range mains {
    166 		roots = append(roots, main.Func("init"), main.Func("main"))
    167 	}
    168 
    169 	// Gather all source-level functions,
    170 	// as the user interface is expressed in terms of them.
    171 	//
    172 	// We ignore synthetic wrappers, and nested functions. Literal
    173 	// functions passed as arguments to other functions are of
    174 	// course address-taken and there exists a dynamic call of
    175 	// that signature, so when they are unreachable, it is
    176 	// invariably because the parent is unreachable.
    177 	var (
    178 		sourceFuncs    []*ssa.Function
    179 		generated      = make(map[string]bool)
    180 		interfaceTypes = make(map[*types.Package][]*types.Interface)
    181 	)
    182 	packages.Visit(initial, nil, func(p *packages.Package) {
    183 		// Collect interfaces by package for marker method identification.
    184 		var interfaces []*types.Interface
    185 		scope := p.Types.Scope()
    186 		for _, name := range scope.Names() {
    187 			if typeName, ok := scope.Lookup(name).(*types.TypeName); ok &&
    188 				types.IsInterface(typeName.Type()) {
    189 				interfaces = append(interfaces, typeName.Type().Underlying().(*types.Interface))
    190 			}
    191 		}
    192 		interfaceTypes[p.Types] = interfaces
    193 
    194 		for _, file := range p.Syntax {
    195 			for _, decl := range file.Decls {
    196 				if decl, ok := decl.(*ast.FuncDecl); ok {
    197 					obj := p.TypesInfo.Defs[decl.Name].(*types.Func)
    198 					fn := prog.FuncValue(obj)
    199 					sourceFuncs = append(sourceFuncs, fn)
    200 				}
    201 			}
    202 
    203 			if ast.IsGenerated(file) {
    204 				generated[p.Fset.File(file.Pos()).Name()] = true
    205 			}
    206 		}
    207 	})
    208 
    209 	// Compute the reachabilty from main.
    210 	// (Build a call graph only for -whylive.)
    211 	res := rta.Analyze(roots, *whyLiveFlag != "")
    212 
    213 	// Subtle: the -test flag causes us to analyze test variants
    214 	// such as "package p as compiled for p.test" or even "for q.test".
    215 	// This leads to multiple distinct ssa.Function instances that
    216 	// represent the same source declaration, and it is essentially
    217 	// impossible to discover this from the SSA representation
    218 	// (since it has lost the connection to go/packages.Package.ID).
    219 	//
    220 	// So, we de-duplicate such variants by position:
    221 	// if any one of them is live, we consider all of them live.
    222 	// (We use Position not Pos to avoid assuming that files common
    223 	// to packages "p" and "p [p.test]" were parsed only once.)
    224 	reachablePosn := make(map[token.Position]bool)
    225 	for fn := range res.Reachable {
    226 		if fn.Pos().IsValid() || fn.Name() == "init" {
    227 			reachablePosn[prog.Fset.Position(fn.Pos())] = true
    228 		}
    229 	}
    230 
    231 	// The -whylive=fn flag causes deadcode to explain why a function
    232 	// is not dead, by showing a path to it from some root.
    233 	if *whyLiveFlag != "" {
    234 		targets := make(map[*ssa.Function]bool)
    235 		for _, fn := range sourceFuncs {
    236 			if prettyName(fn, true) == *whyLiveFlag {
    237 				targets[fn] = true
    238 			}
    239 		}
    240 		if len(targets) == 0 {
    241 			// Function is not part of the program.
    242 			//
    243 			// TODO(adonovan): improve the UX here in case
    244 			// of spelling or syntax mistakes. Some ideas:
    245 			// - a cmd/callgraph command to enumerate
    246 			//   available functions.
    247 			// - a deadcode -live flag to compute the complement.
    248 			// - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method
    249 			// - report the element of AllFunctions with the smallest
    250 			//   Levenshtein distance from *whyLiveFlag.
    251 			// - permit -whylive=regexp. But beware of spurious
    252 			//   matches (e.g. fmt.Print matches fmt.Println)
    253 			//   and the annoyance of having to quote parens (*T).f.
    254 			log.Fatalf("function %q not found in program", *whyLiveFlag)
    255 		}
    256 
    257 		// Opt: remove the unreachable ones.
    258 		for fn := range targets {
    259 			if !reachablePosn[prog.Fset.Position(fn.Pos())] {
    260 				delete(targets, fn)
    261 			}
    262 		}
    263 		if len(targets) == 0 {
    264 			log.Fatalf("function %s is dead code", *whyLiveFlag)
    265 		}
    266 
    267 		res.CallGraph.DeleteSyntheticNodes() // inline synthetic wrappers (except inits)
    268 		root, path := pathSearch(roots, res, targets)
    269 		if root == nil {
    270 			// RTA doesn't add callgraph edges for reflective calls.
    271 			log.Fatalf("%s is reachable only through reflection", *whyLiveFlag)
    272 		}
    273 		if len(path) == 0 {
    274 			// No edges => one of the targets is a root.
    275 			// Rather than (confusingly) print nothing, make this an error.
    276 			log.Fatalf("%s is a root", root.Func)
    277 		}
    278 
    279 		// Build a list of jsonEdge records
    280 		// to print as -json or -f=template.
    281 		var edges []any
    282 		for _, edge := range path {
    283 			edges = append(edges, jsonEdge{
    284 				Initial:  cond(len(edges) == 0, prettyName(edge.Caller.Func, true), ""),
    285 				Kind:     cond(isStaticCall(edge), "static", "dynamic"),
    286 				Position: toJSONPosition(prog.Fset.Position(edge.Pos())),
    287 				Callee:   prettyName(edge.Callee.Func, true),
    288 			})
    289 		}
    290 		format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Position.Line .Callee}}`
    291 		if *formatFlag != "" {
    292 			format = *formatFlag
    293 		}
    294 		printObjects(format, edges)
    295 		return
    296 	}
    297 
    298 	// Group unreachable functions by package path.
    299 	byPkgPath := make(map[string]map[*ssa.Function]bool)
    300 	for _, fn := range sourceFuncs {
    301 		posn := prog.Fset.Position(fn.Pos())
    302 
    303 		if !reachablePosn[posn] {
    304 			reachablePosn[posn] = true // suppress dups with same pos
    305 
    306 			pkgpath := fn.Pkg.Pkg.Path()
    307 			m, ok := byPkgPath[pkgpath]
    308 			if !ok {
    309 				m = make(map[*ssa.Function]bool)
    310 				byPkgPath[pkgpath] = m
    311 			}
    312 			m[fn] = true
    313 		}
    314 	}
    315 
    316 	// Build array of jsonPackage objects.
    317 	var packages []any
    318 	for _, pkgpath := range slices.Sorted(maps.Keys(byPkgPath)) {
    319 		if !filter.MatchString(pkgpath) {
    320 			continue
    321 		}
    322 
    323 		m := byPkgPath[pkgpath]
    324 
    325 		// Print functions that appear within the same file in
    326 		// declaration order. This tends to keep related
    327 		// methods such as (T).Marshal and (*T).Unmarshal
    328 		// together better than sorting.
    329 		fns := slices.Collect(maps.Keys(m))
    330 		sort.Slice(fns, func(i, j int) bool {
    331 			xposn := prog.Fset.Position(fns[i].Pos())
    332 			yposn := prog.Fset.Position(fns[j].Pos())
    333 			if xposn.Filename != yposn.Filename {
    334 				return xposn.Filename < yposn.Filename
    335 			}
    336 			return xposn.Line < yposn.Line
    337 		})
    338 
    339 		var functions []jsonFunction
    340 		for _, fn := range fns {
    341 			posn := prog.Fset.Position(fn.Pos())
    342 
    343 			// Without -generated, skip functions declared in
    344 			// generated Go files.
    345 			// (Functions called by them may still be reported.)
    346 			gen := generated[posn.Filename]
    347 			if gen && !*generatedFlag {
    348 				continue
    349 			}
    350 
    351 			// Marker methods should not be reported
    352 			marker := isMarkerMethod(fn, interfaceTypes[fn.Pkg.Pkg])
    353 			if marker {
    354 				continue
    355 			}
    356 
    357 			functions = append(functions, jsonFunction{
    358 				Name:      prettyName(fn, false),
    359 				Position:  toJSONPosition(posn),
    360 				Generated: gen,
    361 				Marker:    marker,
    362 			})
    363 		}
    364 		if len(functions) > 0 {
    365 			packages = append(packages, jsonPackage{
    366 				Name:  fns[0].Pkg.Pkg.Name(),
    367 				Path:  pkgpath,
    368 				Funcs: functions,
    369 			})
    370 		}
    371 	}
    372 
    373 	// Default line-oriented format: "a/b/c.go:1:2: unreachable func: T.f"
    374 	format := `{{range .Funcs}}{{printf "%s: unreachable func: %s\n" .Position .Name}}{{end}}`
    375 	if *formatFlag != "" {
    376 		format = *formatFlag
    377 	}
    378 	printObjects(format, packages)
    379 }
    380 
    381 // prettyName is a fork of Function.String designed to reduce
    382 // go/ssa's fussy punctuation symbols, e.g. "(*pkg.T).F" -> "pkg.T.F".
    383 //
    384 // It only works for functions that remain after
    385 // callgraph.Graph.DeleteSyntheticNodes: source-level named functions
    386 // and methods, their anonymous functions, and synthetic package
    387 // initializers.
    388 func prettyName(fn *ssa.Function, qualified bool) string {
    389 	var buf strings.Builder
    390 
    391 	// optional package qualifier
    392 	if qualified && fn.Pkg != nil {
    393 		fmt.Fprintf(&buf, "%s.", fn.Pkg.Pkg.Path())
    394 	}
    395 
    396 	var format func(*ssa.Function)
    397 	format = func(fn *ssa.Function) {
    398 		// anonymous?
    399 		if fn.Parent() != nil {
    400 			format(fn.Parent())
    401 			i := slices.Index(fn.Parent().AnonFuncs, fn)
    402 			fmt.Fprintf(&buf, "$%d", i+1)
    403 			return
    404 		}
    405 
    406 		// method receiver?
    407 		if recv := fn.Signature.Recv(); recv != nil {
    408 			_, named := typesinternal.ReceiverNamed(recv)
    409 			buf.WriteString(named.Obj().Name())
    410 			buf.WriteByte('.')
    411 		}
    412 
    413 		// function/method name
    414 		buf.WriteString(fn.Name())
    415 	}
    416 	format(fn)
    417 
    418 	return buf.String()
    419 }
    420 
    421 // printObjects formats an array of objects, either as JSON or using a
    422 // template, following the manner of 'go list (-json|-f=template)'.
    423 func printObjects(format string, objects []any) {
    424 	if *jsonFlag {
    425 		out, err := json.MarshalIndent(objects, "", "\t")
    426 		if err != nil {
    427 			log.Fatalf("internal error: %v", err)
    428 		}
    429 		os.Stdout.Write(out)
    430 		return
    431 	}
    432 
    433 	// -f=template. Parse can't fail: we checked it earlier.
    434 	tmpl := template.Must(template.New("deadcode").Parse(format))
    435 	for _, object := range objects {
    436 		var buf bytes.Buffer
    437 		if err := tmpl.Execute(&buf, object); err != nil {
    438 			log.Fatal(err)
    439 		}
    440 		if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' {
    441 			buf.WriteByte('\n')
    442 		}
    443 		os.Stdout.Write(buf.Bytes())
    444 	}
    445 }
    446 
    447 // pathSearch returns the shortest path from one of the roots to one
    448 // of the targets (along with the root itself), or zero if no path was found.
    449 func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
    450 	// Search breadth-first (for shortest path) from the root.
    451 	//
    452 	// We don't use the virtual CallGraph.Root node as we wish to
    453 	// choose the order in which we search entrypoints:
    454 	// non-test packages before test packages,
    455 	// main functions before init functions.
    456 
    457 	// Sort roots into preferred order.
    458 	importsTesting := func(fn *ssa.Function) bool {
    459 		isTesting := func(p *types.Package) bool { return p.Path() == "testing" }
    460 		return slices.ContainsFunc(fn.Pkg.Pkg.Imports(), isTesting)
    461 	}
    462 	sort.Slice(roots, func(i, j int) bool {
    463 		x, y := roots[i], roots[j]
    464 		xtest := importsTesting(x)
    465 		ytest := importsTesting(y)
    466 		if xtest != ytest {
    467 			return !xtest // non-tests before tests
    468 		}
    469 		xinit := x.Name() == "init"
    470 		yinit := y.Name() == "init"
    471 		if xinit != yinit {
    472 			return !xinit // mains before inits
    473 		}
    474 		return false
    475 	})
    476 
    477 	search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) {
    478 		// seen maps each encountered node to its predecessor on the
    479 		// path to a root node, or to nil for root itself.
    480 		seen := make(map[*callgraph.Node]*callgraph.Edge)
    481 		bfs := func(root *callgraph.Node) []*callgraph.Edge {
    482 			queue := []*callgraph.Node{root}
    483 			seen[root] = nil
    484 			for len(queue) > 0 {
    485 				node := queue[0]
    486 				queue = queue[1:]
    487 
    488 				// found a path?
    489 				if targets[node.Func] {
    490 					path := []*callgraph.Edge{} // non-nil in case len(path)=0
    491 					for {
    492 						edge := seen[node]
    493 						if edge == nil {
    494 							slices.Reverse(path)
    495 							return path
    496 						}
    497 						path = append(path, edge)
    498 						node = edge.Caller
    499 					}
    500 				}
    501 
    502 				for _, edge := range node.Out {
    503 					if allowDynamic || isStaticCall(edge) {
    504 						if _, ok := seen[edge.Callee]; !ok {
    505 							seen[edge.Callee] = edge
    506 							queue = append(queue, edge.Callee)
    507 						}
    508 					}
    509 				}
    510 			}
    511 			return nil
    512 		}
    513 		for _, rootFn := range roots {
    514 			root := res.CallGraph.Nodes[rootFn]
    515 			if root == nil {
    516 				// Missing call graph node for root.
    517 				// TODO(adonovan): seems like a bug in rta.
    518 				continue
    519 			}
    520 			if path := bfs(root); path != nil {
    521 				return root, path
    522 			}
    523 		}
    524 		return nil, nil
    525 	}
    526 
    527 	for _, allowDynamic := range []bool{false, true} {
    528 		if root, path := search(allowDynamic); path != nil {
    529 			return root, path
    530 		}
    531 	}
    532 
    533 	return nil, nil
    534 }
    535 
    536 // -- utilities --
    537 
    538 func isStaticCall(edge *callgraph.Edge) bool {
    539 	return edge.Site != nil && edge.Site.Common().StaticCallee() != nil
    540 }
    541 
    542 var cwd, _ = os.Getwd()
    543 
    544 func toJSONPosition(posn token.Position) jsonPosition {
    545 	// Use cwd-relative filename if possible.
    546 	filename := posn.Filename
    547 	if rel, err := filepath.Rel(cwd, filename); err == nil && !strings.HasPrefix(rel, "..") {
    548 		filename = rel
    549 	}
    550 
    551 	return jsonPosition{filename, posn.Line, posn.Column}
    552 }
    553 
    554 func cond[T any](cond bool, t, f T) T {
    555 	if cond {
    556 		return t
    557 	} else {
    558 		return f
    559 	}
    560 }
    561 
    562 // isMarkerMethod reports whether fn is a marker method:
    563 // an unexported, empty-bodied method with no parameters or results
    564 // that implements some named interface type in the same package.
    565 func isMarkerMethod(fn *ssa.Function, interfaceTypes []*types.Interface) bool {
    566 	// Is it an unexported method of no params/results?
    567 	if !(fn.Signature.Recv() != nil &&
    568 		!ast.IsExported(fn.Name()) &&
    569 		fn.Signature.Params() == nil &&
    570 		fn.Signature.Results() == nil) {
    571 		return false
    572 	}
    573 
    574 	// Does the method have an empty body?
    575 	body := fn.Syntax().(*ast.FuncDecl).Body
    576 	if body == nil || len(body.List) > 0 {
    577 		return false
    578 	}
    579 
    580 	// Does it implement some named interface type in this package?
    581 	return slices.ContainsFunc(interfaceTypes, func(iface *types.Interface) bool {
    582 		return types.Implements(fn.Signature.Recv().Type(), iface)
    583 	})
    584 }
    585 
    586 // -- output protocol (for JSON or text/template) --
    587 
    588 // Keep in sync with doc comment!
    589 
    590 type jsonFunction struct {
    591 	Name      string       // name (sans package qualifier)
    592 	Position  jsonPosition // file/line/column of declaration
    593 	Generated bool         // function is declared in a generated .go file
    594 	Marker    bool         // function is a marker interface method
    595 }
    596 
    597 func (f jsonFunction) String() string { return f.Name }
    598 
    599 type jsonPackage struct {
    600 	Name  string         // declared name
    601 	Path  string         // full import path
    602 	Funcs []jsonFunction // non-empty list of package's dead functions
    603 }
    604 
    605 func (p jsonPackage) String() string { return p.Path }
    606 
    607 // The Initial and Callee names are package-qualified.
    608 type jsonEdge struct {
    609 	Initial  string `json:",omitempty"` // initial entrypoint (main or init); first edge only
    610 	Kind     string // = static | dynamic
    611 	Position jsonPosition
    612 	Callee   string
    613 }
    614 
    615 type jsonPosition struct {
    616 	File      string
    617 	Line, Col int
    618 }
    619 
    620 func (p jsonPosition) String() string {
    621 	return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col)
    622 }