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 }