golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/cmd/deadcode/deadcode.go (about) 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 //go:build go1.20 6 7 package main 8 9 import ( 10 "bytes" 11 _ "embed" 12 "encoding/json" 13 "flag" 14 "fmt" 15 "go/ast" 16 "go/token" 17 "go/types" 18 "io" 19 "log" 20 "os" 21 "path/filepath" 22 "regexp" 23 "runtime" 24 "runtime/pprof" 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 if mod := initial[0].Module; mod != nil && mod.Path != "" { 136 *filterFlag = "^" + regexp.QuoteMeta(mod.Path) + "\\b" 137 } else { 138 *filterFlag = "" // match any 139 } 140 } 141 filter, err := regexp.Compile(*filterFlag) 142 if err != nil { 143 log.Fatalf("-filter: %v", err) 144 } 145 146 // Create SSA-form program representation 147 // and find main packages. 148 prog, pkgs := ssautil.AllPackages(initial, ssa.InstantiateGenerics) 149 prog.Build() 150 151 mains := ssautil.MainPackages(pkgs) 152 if len(mains) == 0 { 153 log.Fatalf("no main packages") 154 } 155 var roots []*ssa.Function 156 for _, main := range mains { 157 roots = append(roots, main.Func("init"), main.Func("main")) 158 } 159 160 // Gather all source-level functions, 161 // as the user interface is expressed in terms of them. 162 // 163 // We ignore synthetic wrappers, and nested functions. Literal 164 // functions passed as arguments to other functions are of 165 // course address-taken and there exists a dynamic call of 166 // that signature, so when they are unreachable, it is 167 // invariably because the parent is unreachable. 168 var sourceFuncs []*ssa.Function 169 generated := make(map[string]bool) 170 packages.Visit(initial, nil, func(p *packages.Package) { 171 for _, file := range p.Syntax { 172 for _, decl := range file.Decls { 173 if decl, ok := decl.(*ast.FuncDecl); ok { 174 obj := p.TypesInfo.Defs[decl.Name].(*types.Func) 175 fn := prog.FuncValue(obj) 176 sourceFuncs = append(sourceFuncs, fn) 177 } 178 } 179 180 if isGenerated(file) { 181 generated[p.Fset.File(file.Pos()).Name()] = true 182 } 183 } 184 }) 185 186 // Compute the reachabilty from main. 187 // (Build a call graph only for -whylive.) 188 res := rta.Analyze(roots, *whyLiveFlag != "") 189 190 // Subtle: the -test flag causes us to analyze test variants 191 // such as "package p as compiled for p.test" or even "for q.test". 192 // This leads to multiple distinct ssa.Function instances that 193 // represent the same source declaration, and it is essentially 194 // impossible to discover this from the SSA representation 195 // (since it has lost the connection to go/packages.Package.ID). 196 // 197 // So, we de-duplicate such variants by position: 198 // if any one of them is live, we consider all of them live. 199 // (We use Position not Pos to avoid assuming that files common 200 // to packages "p" and "p [p.test]" were parsed only once.) 201 reachablePosn := make(map[token.Position]bool) 202 for fn := range res.Reachable { 203 if fn.Pos().IsValid() || fn.Name() == "init" { 204 reachablePosn[prog.Fset.Position(fn.Pos())] = true 205 } 206 } 207 208 // The -whylive=fn flag causes deadcode to explain why a function 209 // is not dead, by showing a path to it from some root. 210 if *whyLiveFlag != "" { 211 targets := make(map[*ssa.Function]bool) 212 for _, fn := range sourceFuncs { 213 if prettyName(fn, true) == *whyLiveFlag { 214 targets[fn] = true 215 } 216 } 217 if len(targets) == 0 { 218 // Function is not part of the program. 219 // 220 // TODO(adonovan): improve the UX here in case 221 // of spelling or syntax mistakes. Some ideas: 222 // - a cmd/callgraph command to enumerate 223 // available functions. 224 // - a deadcode -live flag to compute the complement. 225 // - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method 226 // - report the element of AllFunctions with the smallest 227 // Levenshtein distance from *whyLiveFlag. 228 // - permit -whylive=regexp. But beware of spurious 229 // matches (e.g. fmt.Print matches fmt.Println) 230 // and the annoyance of having to quote parens (*T).f. 231 log.Fatalf("function %q not found in program", *whyLiveFlag) 232 } 233 234 // Opt: remove the unreachable ones. 235 for fn := range targets { 236 if !reachablePosn[prog.Fset.Position(fn.Pos())] { 237 delete(targets, fn) 238 } 239 } 240 if len(targets) == 0 { 241 log.Fatalf("function %s is dead code", *whyLiveFlag) 242 } 243 244 res.CallGraph.DeleteSyntheticNodes() // inline synthetic wrappers (except inits) 245 root, path := pathSearch(roots, res, targets) 246 if root == nil { 247 // RTA doesn't add callgraph edges for reflective calls. 248 log.Fatalf("%s is reachable only through reflection", *whyLiveFlag) 249 } 250 if len(path) == 0 { 251 // No edges => one of the targets is a root. 252 // Rather than (confusingly) print nothing, make this an error. 253 log.Fatalf("%s is a root", root.Func) 254 } 255 256 // Build a list of jsonEdge records 257 // to print as -json or -f=template. 258 var edges []any 259 for _, edge := range path { 260 edges = append(edges, jsonEdge{ 261 Initial: cond(len(edges) == 0, prettyName(edge.Caller.Func, true), ""), 262 Kind: cond(isStaticCall(edge), "static", "dynamic"), 263 Position: toJSONPosition(prog.Fset.Position(edge.Site.Pos())), 264 Callee: prettyName(edge.Callee.Func, true), 265 }) 266 } 267 format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Position.Line .Callee}}` 268 if *formatFlag != "" { 269 format = *formatFlag 270 } 271 printObjects(format, edges) 272 return 273 } 274 275 // Group unreachable functions by package path. 276 byPkgPath := make(map[string]map[*ssa.Function]bool) 277 for _, fn := range sourceFuncs { 278 posn := prog.Fset.Position(fn.Pos()) 279 280 if !reachablePosn[posn] { 281 reachablePosn[posn] = true // suppress dups with same pos 282 283 pkgpath := fn.Pkg.Pkg.Path() 284 m, ok := byPkgPath[pkgpath] 285 if !ok { 286 m = make(map[*ssa.Function]bool) 287 byPkgPath[pkgpath] = m 288 } 289 m[fn] = true 290 } 291 } 292 293 // Build array of jsonPackage objects. 294 var packages []any 295 pkgpaths := keys(byPkgPath) 296 sort.Strings(pkgpaths) 297 for _, pkgpath := range pkgpaths { 298 if !filter.MatchString(pkgpath) { 299 continue 300 } 301 302 m := byPkgPath[pkgpath] 303 304 // Print functions that appear within the same file in 305 // declaration order. This tends to keep related 306 // methods such as (T).Marshal and (*T).Unmarshal 307 // together better than sorting. 308 fns := keys(m) 309 sort.Slice(fns, func(i, j int) bool { 310 xposn := prog.Fset.Position(fns[i].Pos()) 311 yposn := prog.Fset.Position(fns[j].Pos()) 312 if xposn.Filename != yposn.Filename { 313 return xposn.Filename < yposn.Filename 314 } 315 return xposn.Line < yposn.Line 316 }) 317 318 var functions []jsonFunction 319 for _, fn := range fns { 320 posn := prog.Fset.Position(fn.Pos()) 321 322 // Without -generated, skip functions declared in 323 // generated Go files. 324 // (Functions called by them may still be reported.) 325 gen := generated[posn.Filename] 326 if gen && !*generatedFlag { 327 continue 328 } 329 330 functions = append(functions, jsonFunction{ 331 Name: prettyName(fn, false), 332 Position: toJSONPosition(posn), 333 Generated: gen, 334 }) 335 } 336 if len(functions) > 0 { 337 packages = append(packages, jsonPackage{ 338 Name: fns[0].Pkg.Pkg.Name(), 339 Path: pkgpath, 340 Funcs: functions, 341 }) 342 } 343 } 344 345 // Default line-oriented format: "a/b/c.go:1:2: unreachable func: T.f" 346 format := `{{range .Funcs}}{{printf "%s: unreachable func: %s\n" .Position .Name}}{{end}}` 347 if *formatFlag != "" { 348 format = *formatFlag 349 } 350 printObjects(format, packages) 351 } 352 353 // prettyName is a fork of Function.String designed to reduce 354 // go/ssa's fussy punctuation symbols, e.g. "(*pkg.T).F" -> "pkg.T.F". 355 // 356 // It only works for functions that remain after 357 // callgraph.Graph.DeleteSyntheticNodes: source-level named functions 358 // and methods, their anonymous functions, and synthetic package 359 // initializers. 360 func prettyName(fn *ssa.Function, qualified bool) string { 361 var buf strings.Builder 362 363 // optional package qualifier 364 if qualified && fn.Pkg != nil { 365 fmt.Fprintf(&buf, "%s.", fn.Pkg.Pkg.Path()) 366 } 367 368 var format func(*ssa.Function) 369 format = func(fn *ssa.Function) { 370 // anonymous? 371 if fn.Parent() != nil { 372 format(fn.Parent()) 373 i := index(fn.Parent().AnonFuncs, fn) 374 fmt.Fprintf(&buf, "$%d", i+1) 375 return 376 } 377 378 // method receiver? 379 if recv := fn.Signature.Recv(); recv != nil { 380 _, named := typesinternal.ReceiverNamed(recv) 381 buf.WriteString(named.Obj().Name()) 382 buf.WriteByte('.') 383 } 384 385 // function/method name 386 buf.WriteString(fn.Name()) 387 } 388 format(fn) 389 390 return buf.String() 391 } 392 393 // printObjects formats an array of objects, either as JSON or using a 394 // template, following the manner of 'go list (-json|-f=template)'. 395 func printObjects(format string, objects []any) { 396 if *jsonFlag { 397 out, err := json.MarshalIndent(objects, "", "\t") 398 if err != nil { 399 log.Fatalf("internal error: %v", err) 400 } 401 os.Stdout.Write(out) 402 return 403 } 404 405 // -f=template. Parse can't fail: we checked it earlier. 406 tmpl := template.Must(template.New("deadcode").Parse(format)) 407 for _, object := range objects { 408 var buf bytes.Buffer 409 if err := tmpl.Execute(&buf, object); err != nil { 410 log.Fatal(err) 411 } 412 if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' { 413 buf.WriteByte('\n') 414 } 415 os.Stdout.Write(buf.Bytes()) 416 } 417 } 418 419 // TODO(adonovan): use go1.21's ast.IsGenerated. 420 421 // isGenerated reports whether the file was generated by a program, 422 // not handwritten, by detecting the special comment described 423 // at https://go.dev/s/generatedcode. 424 // 425 // The syntax tree must have been parsed with the ParseComments flag. 426 // Example: 427 // 428 // f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly) 429 // if err != nil { ... } 430 // gen := ast.IsGenerated(f) 431 func isGenerated(file *ast.File) bool { 432 _, ok := generator(file) 433 return ok 434 } 435 436 func generator(file *ast.File) (string, bool) { 437 for _, group := range file.Comments { 438 for _, comment := range group.List { 439 if comment.Pos() > file.Package { 440 break // after package declaration 441 } 442 // opt: check Contains first to avoid unnecessary array allocation in Split. 443 const prefix = "// Code generated " 444 if strings.Contains(comment.Text, prefix) { 445 for _, line := range strings.Split(comment.Text, "\n") { 446 if rest, ok := strings.CutPrefix(line, prefix); ok { 447 if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok { 448 return gen, true 449 } 450 } 451 } 452 } 453 } 454 } 455 return "", false 456 } 457 458 // pathSearch returns the shortest path from one of the roots to one 459 // of the targets (along with the root itself), or zero if no path was found. 460 func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) { 461 // Search breadth-first (for shortest path) from the root. 462 // 463 // We don't use the virtual CallGraph.Root node as we wish to 464 // choose the order in which we search entrypoints: 465 // non-test packages before test packages, 466 // main functions before init functions. 467 468 // Sort roots into preferred order. 469 importsTesting := func(fn *ssa.Function) bool { 470 isTesting := func(p *types.Package) bool { return p.Path() == "testing" } 471 return containsFunc(fn.Pkg.Pkg.Imports(), isTesting) 472 } 473 sort.Slice(roots, func(i, j int) bool { 474 x, y := roots[i], roots[j] 475 xtest := importsTesting(x) 476 ytest := importsTesting(y) 477 if xtest != ytest { 478 return !xtest // non-tests before tests 479 } 480 xinit := x.Name() == "init" 481 yinit := y.Name() == "init" 482 if xinit != yinit { 483 return !xinit // mains before inits 484 } 485 return false 486 }) 487 488 search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) { 489 // seen maps each encountered node to its predecessor on the 490 // path to a root node, or to nil for root itself. 491 seen := make(map[*callgraph.Node]*callgraph.Edge) 492 bfs := func(root *callgraph.Node) []*callgraph.Edge { 493 queue := []*callgraph.Node{root} 494 seen[root] = nil 495 for len(queue) > 0 { 496 node := queue[0] 497 queue = queue[1:] 498 499 // found a path? 500 if targets[node.Func] { 501 path := []*callgraph.Edge{} // non-nil in case len(path)=0 502 for { 503 edge := seen[node] 504 if edge == nil { 505 reverse(path) 506 return path 507 } 508 path = append(path, edge) 509 node = edge.Caller 510 } 511 } 512 513 for _, edge := range node.Out { 514 if allowDynamic || isStaticCall(edge) { 515 if _, ok := seen[edge.Callee]; !ok { 516 seen[edge.Callee] = edge 517 queue = append(queue, edge.Callee) 518 } 519 } 520 } 521 } 522 return nil 523 } 524 for _, rootFn := range roots { 525 root := res.CallGraph.Nodes[rootFn] 526 if root == nil { 527 // Missing call graph node for root. 528 // TODO(adonovan): seems like a bug in rta. 529 continue 530 } 531 if path := bfs(root); path != nil { 532 return root, path 533 } 534 } 535 return nil, nil 536 } 537 538 for _, allowDynamic := range []bool{false, true} { 539 if root, path := search(allowDynamic); path != nil { 540 return root, path 541 } 542 } 543 544 return nil, nil 545 } 546 547 // -- utilities -- 548 549 func isStaticCall(edge *callgraph.Edge) bool { 550 return edge.Site != nil && edge.Site.Common().StaticCallee() != nil 551 } 552 553 var cwd, _ = os.Getwd() 554 555 func toJSONPosition(posn token.Position) jsonPosition { 556 // Use cwd-relative filename if possible. 557 filename := posn.Filename 558 if rel, err := filepath.Rel(cwd, filename); err == nil && !strings.HasPrefix(rel, "..") { 559 filename = rel 560 } 561 562 return jsonPosition{filename, posn.Line, posn.Column} 563 } 564 565 func cond[T any](cond bool, t, f T) T { 566 if cond { 567 return t 568 } else { 569 return f 570 } 571 } 572 573 // -- output protocol (for JSON or text/template) -- 574 575 // Keep in sync with doc comment! 576 577 type jsonFunction struct { 578 Name string // name (sans package qualifier) 579 Position jsonPosition // file/line/column of declaration 580 Generated bool // function is declared in a generated .go file 581 } 582 583 func (f jsonFunction) String() string { return f.Name } 584 585 type jsonPackage struct { 586 Name string // declared name 587 Path string // full import path 588 Funcs []jsonFunction // non-empty list of package's dead functions 589 } 590 591 func (p jsonPackage) String() string { return p.Path } 592 593 // The Initial and Callee names are package-qualified. 594 type jsonEdge struct { 595 Initial string `json:",omitempty"` // initial entrypoint (main or init); first edge only 596 Kind string // = static | dynamic 597 Position jsonPosition 598 Callee string 599 } 600 601 type jsonPosition struct { 602 File string 603 Line, Col int 604 } 605 606 func (p jsonPosition) String() string { 607 return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col) 608 } 609 610 // -- from the future -- 611 612 // TODO(adonovan): use go1.22's slices and maps packages. 613 614 func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool { 615 return indexFunc(s, f) >= 0 616 } 617 618 func indexFunc[S ~[]E, E any](s S, f func(E) bool) int { 619 for i := range s { 620 if f(s[i]) { 621 return i 622 } 623 } 624 return -1 625 } 626 627 func index[S ~[]E, E comparable](s S, v E) int { 628 for i := range s { 629 if v == s[i] { 630 return i 631 } 632 } 633 return -1 634 } 635 636 func reverse[S ~[]E, E any](s S) { 637 for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { 638 s[i], s[j] = s[j], s[i] 639 } 640 } 641 642 func keys[M ~map[K]V, K comparable, V any](m M) []K { 643 r := make([]K, 0, len(m)) 644 for k := range m { 645 r = append(r, k) 646 } 647 return r 648 }