1
2
3
4
5
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
39 var doc string
40
41
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
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)
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
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
97 defer func() {
98 runtime.GC()
99 if err := pprof.WriteHeapProfile(f); err != nil {
100 log.Fatalf("Writing memory profile: %v", err)
101 }
102 f.Close()
103 }()
104 }
105
106
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
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
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 = ""
139 }
140 }
141 filter, err := regexp.Compile(*filterFlag)
142 if err != nil {
143 log.Fatalf("-filter: %v", err)
144 }
145
146
147
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
161
162
163
164
165
166
167
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
187
188 res := rta.Analyze(roots, *whyLiveFlag != "")
189
190
191
192
193
194
195
196
197
198
199
200
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
209
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
219
220
221
222
223
224
225
226
227
228
229
230
231 log.Fatalf("function %q not found in program", *whyLiveFlag)
232 }
233
234
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()
245 root, path := pathSearch(roots, res, targets)
246 if root == nil {
247
248 log.Fatalf("%s is reachable only through reflection", *whyLiveFlag)
249 }
250 if len(path) == 0 {
251
252
253 log.Fatalf("%s is a root", root.Func)
254 }
255
256
257
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
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
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
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
305
306
307
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
323
324
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
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
354
355
356
357
358
359
360 func prettyName(fn *ssa.Function, qualified bool) string {
361 var buf strings.Builder
362
363
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
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
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
386 buf.WriteString(fn.Name())
387 }
388 format(fn)
389
390 return buf.String()
391 }
392
393
394
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
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
420
421
422
423
424
425
426
427
428
429
430
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
441 }
442
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
459
460 func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) {
461
462
463
464
465
466
467
468
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
479 }
480 xinit := x.Name() == "init"
481 yinit := y.Name() == "init"
482 if xinit != yinit {
483 return !xinit
484 }
485 return false
486 })
487
488 search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) {
489
490
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
500 if targets[node.Func] {
501 path := []*callgraph.Edge{}
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
528
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
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
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
574
575
576
577 type jsonFunction struct {
578 Name string
579 Position jsonPosition
580 Generated bool
581 }
582
583 func (f jsonFunction) String() string { return f.Name }
584
585 type jsonPackage struct {
586 Name string
587 Path string
588 Funcs []jsonFunction
589 }
590
591 func (p jsonPackage) String() string { return p.Path }
592
593
594 type jsonEdge struct {
595 Initial string `json:",omitempty"`
596 Kind string
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
611
612
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 }
649
View as plain text