Alan Donovan has uploaded this change for review.
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It builds uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
TODO:
- New commands need a proposal.
- Tests.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
1 file changed, 173 insertions(+), 0 deletions(-)
diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go
new file mode 100644
index 0000000..8babd48
--- /dev/null
+++ b/cmd/deadcode/deadcode.go
@@ -0,0 +1,173 @@
+// 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.
+
+// The deadcode command reports unreachable functions in Go programs.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "go/token"
+ "log"
+ "os"
+ "regexp"
+ "sort"
+
+ "golang.org/x/exp/maps"
+ "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"
+)
+
+// TODO(adonovan): tests. Especially on generics.
+
+const usage = `deadcode: show unreachable functions in a Go program.
+
+Usage: deadcode [-test] package...
+
+The deadcode program loads a Go program from source then uses Rapid
+Type Analysis (RTA) to build a call graph of all the functions
+reachable from the program's main function. Any functions that are not
+reachable are reported as dead code, grouped by package.
+
+Packages are expressed in the notation of 'go list' (or other
+underlying build system if you are using an alternative
+golang.org/x/go/packages driver). Only executable (main) packages are
+considered starting points for the analysis.
+
+The -test flag causes it to analyze test executables too.
+Tests sometimes make use of functions that would otherwie
+appear to be dead code.
+
+Example: show all dead code within the gopls module:
+
+$ deadcode -filter=golang.org/x/tools/gopls -test ./gopls/...
+
+`
+
+// flags
+var (
+ testFlag = flag.Bool("test", false, "include implicit test packages and executables")
+ filterFlag = flag.String("filter", "", "report only packages matching this regular expression")
+)
+
+func main() {
+ log.SetPrefix("deadcode: ")
+ log.SetFlags(0)
+
+ flag.Parse()
+ if len(flag.Args()) == 0 {
+ fmt.Fprintf(os.Stderr, usage)
+ os.Exit(1)
+ }
+ filter, err := regexp.Compile(*filterFlag)
+ if err != nil {
+ log.Fatalf("-filter: %v", err)
+ }
+
+ // Load, parse, and type-check the complete program(s).
+ cfg := &packages.Config{
+ Mode: packages.LoadAllSyntax,
+ 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")
+ }
+
+ // 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"))
+ }
+
+ // Compute the reachabilty from main.
+ // (We don't actually build a call graph.)
+ res := rta.Analyze(roots, false)
+
+ // Inexplicably, reachable doesn't always include the roots
+ // themselves. Add any that are missing.
+ for _, root := range roots {
+ if v, ok := res.Reachable[root]; !ok {
+ res.Reachable[root] = v
+ }
+ }
+
+ // 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.
+ reachablePos := make(map[token.Pos]bool)
+ for fn := range res.Reachable {
+ if fn.Pos().IsValid() {
+ reachablePos[fn.Pos()] = true
+ }
+ }
+
+ // Group unreachable functions by package path.
+ byPkgPath := make(map[string]map[*ssa.Function]bool)
+ for fn := range ssautil.AllFunctions(prog) {
+ if fn.Synthetic != "" {
+ continue // ignore synthetic wrappers etc
+ }
+ if orig := fn.Origin(); orig != nil {
+ fn = orig
+ }
+ if !reachablePos[fn.Pos()] {
+ reachablePos[fn.Pos()] = 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
+ }
+ }
+
+ // TODO(adonovan): report only the outermost dead function;
+ // there's little point point reporting dead lambdas within
+ // dead functions.
+
+ // Report dead functions grouped by packages.
+ for _, pkgpath := range maps.Keys(byPkgPath) {
+ if !filter.MatchString(pkgpath) {
+ continue // -filter didn't match
+ }
+
+ m := byPkgPath[pkgpath]
+
+ // Print functions in source order.
+ fns := maps.Keys(m)
+ sort.Slice(fns, func(i, j int) bool {
+ return fns[i].Pos() < fns[j].Pos()
+ })
+ fmt.Printf("package %q\n", pkgpath)
+ for _, fn := range fns {
+ fmt.Printf("\tfunc %s\n", fn.RelString(fn.Pkg.Pkg))
+ }
+ fmt.Println()
+ }
+}
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/225b8857-cd76-478a-a7a6-3d0316275b08
Patch set 1:gopls-CI +1
Attention is currently required from: Alan Donovan.
Alan Donovan uploaded patch set #2 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result-1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It builds uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
TODO:
- New commands need a proposal.
- Tests.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
1 file changed, 208 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/470dafe6-abb6-4792-8731-53ff91de3b22
Patch set 2:gopls-CI +1
Attention is currently required from: Alan Donovan.
Alan Donovan uploaded patch set #3 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result-1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It builds uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
TODO:
- A new command needs a proposal.
- Tests.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
1 file changed, 216 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/8530ed2b-917e-47b5-a7a1-297e37444676
Patch set 3:gopls-CI +1
Attention is currently required from: Alan Donovan.
Alan Donovan uploaded patch set #4 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO:
- a new command needs a proposal.
- why does gopackages -test not emit calls to Example functions?
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 426 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/bea448b2-02fc-49c0-9ff0-c5c2c6aa870a
Patch set 4:gopls-CI +1
Attention is currently required from: Alan Donovan.
Alan Donovan uploaded patch set #5 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO:
- a new command needs a proposal.
- why does gopackages -test not emit calls to Example functions?
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 426 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/23826ecb-7431-4728-ba88-efb39cce5314
Patch set 5:gopls-CI +1
Attention is currently required from: Alan Donovan, Robert Findley, Tim King.
Alan Donovan uploaded patch set #6 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO:
- a new command needs a proposal.
- why does gopackages -test not emit calls to Example functions?
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 436 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Robert Findley, Tim King.
Alan Donovan uploaded patch set #7 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 436 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Robert Findley, Tim King.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/79430ea9-37e6-41cf-9028-9db331209ba9
Patch set 7:gopls-CI +1
Attention is currently required from: Alan Donovan, Robert Findley, Tim King.
Alan Donovan uploaded patch set #8 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result-1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 442 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Robert Findley, Tim King.
1 comment:
File cmd/deadcode/deadcode.go:
Patch Set #8, Line 27: testFlag = flag.Bool("test", false, "include implicit test packages and executables")
The flag I'd love is being able to say sets of build tags to check. Maybe a repeatable `--tags` flag where each flag value is a comma-separated list of tags.
So I can say `deadcode --tags=linux,cgo --tags=linux,foo --tags=darwin-bar` and the tool would check all those contexts and find things that are only dead across all contexts.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
1 comment:
File cmd/deadcode/deadcode.go:
Patch Set #8, Line 27: testFlag = flag.Bool("test", false, "include implicit test packages and executables")
The flag I'd love is being able to say sets of build tags to check. […]
Interesting idea. Unfortunately the cost of that feature would be proportional to the number of different configurations. Since there's then no advantage to doing it within the tool, it suggests that we should change the output syntax to be line-oriented so that it's easy to run the tool n times and compute the intersection using UNIX tools, something like this:
$ configs=(linux,cgo linux,foo darwin,bar) for config in ${configs[@]}; do deadcode --tags=$config <whatever>; done | sort | uniq -c | grep " ${#configs[@]} "
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
3 comments:
Patchset:
Note to self: the analysis ignores //go:linkname annotations that might cause a function to become reachable. Deal with them somehow.
File cmd/deadcode/deadcode.go:
Note to self: talk about soundness:
Also discuss what you can/can't assume about dead code: you can't assume that it is safe to simply delete it. A dead function may be referenced (e.g. by another dead function) or a dead method may be required to satisfy an interface method (that is never called).
Patch Set #8, Line 156: map[token.Pos]
Note to self: this is sleazy. Use token.Position.
(Anyway: does go/packages not parse common files in p and p_test twice, which would make this incorrect? Yet it seems to work. Why?)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Robert Findley.
4 comments:
File cmd/deadcode/deadcode.go:
Patch Set #8, Line 27: testFlag = flag.Bool("test", false, "include implicit test packages and executables")
My main concern is that this is a single build. There might be some projects where a function or variable is dead in some build, but not others. This is why test is optional, but this also applies to things like windows/linux. This is mildly academic, but is worth thinking about and considering how to document.
This is moderately similar to what Brad is pointing out.
Patch Set #8, Line 28: filterFlag = flag.String("filter", "", "report only packages matching this regular expression")
A *lot* of library code is going to be dead in larger programs. I think the default filter should probably try to nudge towards something more helpful than everything by default. Maybe the current module? (somehow)
a dead method may be required to satisfy an interface method (that is never called).
+1 this is a concern I had. For methods, you can check the type for which interfaces it satisfies and whether this method is the id of a method name of one of these interfaces. For this case, maybe don't print anything? Or print something extra?
Patch Set #8, Line 156: map[token.Pos]
Note to self: this is sleazy. Use token.Position.
I think token.Position would be slower no obvious gain. Now if we were caching results, token.Position would be a much better alternative.
I don't remember if we invent any interesting functions with token.NoPos. Off the top of my head, I don't think so. Synthetic init functions are not interesting and synthetic functions are removed anyways. So this might be fine.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
2 comments:
File cmd/deadcode/deadcode.go:
> a dead method may be required to satisfy an interface method (that is never called). […]
A dead method is still a dead method, and should be reported as such. I don't think we need to go to any extra trouble. My point here is merely that you can't blindly delete dead code and expect things still to compile.
Patch Set #8, Line 156: map[token.Pos]
I think token.Position would be slower no obvious gain.
My point is that this code should not assume that go/packages ensures that files common to packages "p" and "p [p.test]" are parsed once, and thus have the same pos. (I just empirically verified that they are, currently, but I was actually a little surprised.) Using token.Position would finesse that. The performance is difference is not important here.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
Alan Donovan uploaded patch set #9 to this change.
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/testflag.txtar
4 files changed, 457 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Robert Findley, Tim King.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/00dfef15-2792-4d3d-9071-aecdc6347d0f
Patch set 9:gopls-CI +1
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Robert Findley, Tim King.
5 comments:
File cmd/deadcode/deadcode.go:
Patch Set #8, Line 27: testFlag = flag.Bool("test", false, "include implicit test packages and executables")
Interesting idea. […]
Given the subtlety of specifying a configuration (which consists of at least the triple GOROOT/GOARCH/-tags), and the fact that there is no significant gain in efficiency to doing the loop over configurations within the tool, as opposed to an orchestrating shell script, I don't think we should make this a feature. I have added a -tags flag to set non-GOOS/GOARCh tags, and a -line flag to produce shell-script-friendly line-oriented output, and documented the use case though.
Patch Set #8, Line 27: testFlag = flag.Bool("test", false, "include implicit test packages and executables")
My main concern is that this is a single build. […]
Good point. I don't think the tool can reasonably solve this problem, but it can help users help themselves. I've documented this in the help message.
Patch Set #8, Line 28: filterFlag = flag.String("filter", "", "report only packages matching this regular expression")
A *lot* of library code is going to be dead in larger programs. […]
Good idea. Done.
A dead method is still a dead method, and should be reported as such. […]
Done
Patch Set #8, Line 156: map[token.Pos]
> I think token.Position would be slower no obvious gain. […]
Done
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Robert Findley, Tim King.
Alan Donovan uploaded patch set #10 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/doc.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/lineflag.txtar
A cmd/deadcode/testdata/testflag.txtar
6 files changed, 537 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Tim King.
7 comments:
File cmd/deadcode/deadcode.go:
What about packages that aren't contained in 'internal'? For those, shouldn't any exported symbol be considered an entry point?
Perhaps a future extension?
Patch Set #9, Line 98: profile
cpuProfile? (by analogy with the `NB: memprofile` comment below)
Patch Set #9, Line 98: error
I'm not really sure what this comment is saying, TBH. Do you mean that in case of a fatal error below, the profile will be empty?
Patch Set #9, Line 107: memprofile
memProfile?
Patch Set #9, Line 138: ssautil.MainPackages(pkgs)
Add: `// includes test mains, if testFlag is set`
Patch Set #9, Line 144: main.Func("init")
Perhaps comment that this is a synthetic initializer, comprising all initialization?
// Inexplicably, reachable doesn't always include the roots
// themselves. Add any that are missing.
Shouldn't we just fix that bug? Or is it unfixable at this point?
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Tim King.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/548bac6e-829f-4e22-9eda-297f36e3b82a
Patch set 10:gopls-CI +1
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Tim King.
Alan Donovan uploaded patch set #11 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result-1 by Gopher Robot, gopls-CI+1 by kokoro
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/doc.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/filterflag.txtar
A cmd/deadcode/testdata/lineflag.txtar
A cmd/deadcode/testdata/testflag.txtar
7 files changed, 566 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
Patch set 10:Run-TryBot +1
7 comments:
File cmd/deadcode/deadcode.go:
What about packages that aren't contained in 'internal'? For those, shouldn't any exported symbol be […]
No, that won't work. RTA is a whole program analysis. That means it needs to start from main and inspect all dependencies. Anything less gives incorrect results.
Patch Set #9, Line 98: profile
cpuProfile? (by analogy with the `NB: memprofile` comment below)
Changed both to just "profile".
Patch Set #9, Line 98: error
I'm not really sure what this comment is saying, TBH. […]
Exactly.
Patch Set #9, Line 107: memprofile
memProfile?
Done
Patch Set #9, Line 138: ssautil.MainPackages(pkgs)
Add: `// includes test mains, if testFlag is set`
This doesn't seem like the right place for that comment. Main packages just filters out packages called "main". That some of them are tests arises from the fact the 'initial' was produced by packages.Load, optionally with the Config.Tests option, which is self-evidently connected to the -test flag.
Patch Set #9, Line 144: main.Func("init")
Perhaps comment that this is a synthetic initializer, comprising all initialization?
This snippet is a slightly unfortunate copypasta that appears in every whole-program analysis tool in x/tools. I'd rather not add to it. A better solution would be to make the APIs of the various analysis packages better aligned.
// Inexplicably, reachable doesn't always include the roots
// themselves. Add any that are missing.
Shouldn't we just fix that bug? Or is it unfixable at this point?
Yeah, we should. It's clearly a bug and it's even easier to fix than to work around. Done.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
Alan Donovan uploaded patch set #12 to this change.
cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
TODO: a new command needs a proposal.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A cmd/deadcode/deadcode.go
A cmd/deadcode/deadcode_test.go
A cmd/deadcode/doc.go
A cmd/deadcode/testdata/basic.txtar
A cmd/deadcode/testdata/filterflag.txtar
A cmd/deadcode/testdata/lineflag.txtar
A cmd/deadcode/testdata/testflag.txtar
M go/callgraph/rta/rta.go
8 files changed, 561 insertions(+), 1 deletion(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Tim King.
No, that won't work. RTA is a whole program analysis. […]
I mean that one could start from a package an inspect all dependencies, as a future extension of this tool.
Patch Set #9, Line 138: ssautil.MainPackages(pkgs)
This doesn't seem like the right place for that comment. […]
The non-obvious thing is that go/packages includes the synthesized test mains. Had you not mentioned that to me yesterday, I wouldn't have understood this subtlety.
Patch Set #9, Line 144: main.Func("init")
This snippet is a slightly unfortunate copypasta that appears in every whole-program analysis tool i […]
Ack, ok.
File cmd/deadcode/deadcode.go:
Patch Set #12, Line 107: // If -filter is unset, use first module (if available).
Couldn't we equally well just report top-level packages (those matching the go list pattern)?
Then ./... would do the right thing no matter where it was executed.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Tim King.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/2dd39f8f-3e27-402f-89fd-8f97231573f8
Patch set 12:gopls-CI +1
Attention is currently required from: Brad Fitzpatrick, Robert Findley, Tim King.
Patch Set #12, Line 107: // If -filter is unset, use first module (if available).
Couldn't we equally well just report top-level packages (those matching the go list pattern)? […]
I'm really not sure; I suspect we need some feedback from users. There are two ways I personally want to use this command:
1) Run it on a specific executable and find out what functions are in it but are unreachable, causing the executable to be bloated. Sometimes this indicates encroachment of the test logic on the production code, or unused features.
2) Rn it on an entire module, with tests, and find out what code is never called. This can indicate gaps in test coverage, or orphaned logic, or code intended for use only while debugging.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Robert Findley, Tim King.
1 comment:
Commit Message:
Patch Set #12, Line 12: the Rapid Type Analysis (RTA) algorithm
Out of curiosity - What is the reason that govulncheck uses VTA for reachability analysis while this uses RTA?
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King.
1 comment:
Commit Message:
Patch Set #12, Line 12: the Rapid Type Analysis (RTA) algorithm
Out of curiosity - What is the reason that govulncheck uses VTA for reachability analysis while this […]
I don't know whether VTA is sound w.r.t. reflection, which is a necessity for a deadcode tool. Also, vulncheck prefers VTA because VTA delivers a significantly more precise call graph (fewer edges) and vulncheck wants to report plausible paths through the call graph to the user. By contrast, the deadcode command cares about the set of reachable functions (nodes); it doesn't even materialize the graph edges.
If VTA is sound, and significantly more precise than RTA--and not too slow--then we should consider using it. It would be an interesting experiment. Tim and Zvonimir may know more about its soundness and precision.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley.
1 comment:
Commit Message:
Patch Set #12, Line 12: the Rapid Type Analysis (RTA) algorithm
As Alan said, feasible paths matter in govulncheck as these are user facing and we want users to follow them. This tool only displays a lack of any path.
VTA probably won't be sufficiently sounds w.r.t. reflection for deadcode ATM. (This may be fixable/probably should be addressed eventually.)
It would be an interesting experiment.
+1 I would definitely expect VTA to be more expensive and modestly more precise. It would help to have numbers. FWIW making this configurable would be pretty easy. We only use the ssa.Functions from res.Reachable. It would not be very hard to implement this. (Might take a while to figure out how to interpret the difference.)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley.
Alan Donovan uploaded patch set #13 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
internal/cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A internal/cmd/deadcode/deadcode.go
A internal/cmd/deadcode/deadcode_test.go
A internal/cmd/deadcode/doc.go
A internal/cmd/deadcode/testdata/basic.txtar
A internal/cmd/deadcode/testdata/filterflag.txtar
A internal/cmd/deadcode/testdata/lineflag.txtar
A internal/cmd/deadcode/testdata/testflag.txtar
7 files changed, 562 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King.
Patch set 12:Run-TryBot +1
2 comments:
Commit Message:
Patch Set #12, Line 12: the Rapid Type Analysis (RTA) algorithm
As Alan said, feasible paths matter in govulncheck as these are user facing and we want users to fol […]
Thanks for clarifying, Tim.
Patchset:
I've moved this to internal/cmd/deadcode so that anyone can use it (at their own risk). After some experience with it, we may want to to publish it at cmd/deadcode.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King.
Patch Set #12, Line 107: // If -filter is unset, use first module (if available).
I'm really not sure; I suspect we need some feedback from users. […]
Acknowledged
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King.
Patch set 13:Auto-Submit +1
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King.
Alan Donovan uploaded patch set #14 to this change.
The following approvals got outdated and were removed: Auto-Submit+1 by Alan Donovan, Run-TryBot+1 by Alan Donovan
internal/cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A internal/cmd/deadcode/deadcode.go
A internal/cmd/deadcode/deadcode_test.go
A internal/cmd/deadcode/doc.go
A internal/cmd/deadcode/testdata/basic.txtar
A internal/cmd/deadcode/testdata/filterflag.txtar
A internal/cmd/deadcode/testdata/lineflag.txtar
A internal/cmd/deadcode/testdata/testflag.txtar
7 files changed, 566 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King, triciu...@appspot.gserviceaccount.com.
Patch set 13:Run-TryBot +1Auto-Submit +1
1 comment:
File internal/cmd/deadcode/doc.go:
Done.
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King, triciu...@appspot.gserviceaccount.com.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/16ecbb3a-aed9-42c6-8a6b-33596fd7e804
Patch set 13:gopls-CI +1
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Robert Findley, Tim King, triciu...@appspot.gserviceaccount.com.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/801a606f-5209-48e7-acb3-3f6c156ff772
Patch set 14:gopls-CI +1
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Tim King, triciu...@appspot.gserviceaccount.com.
Patch set 14:Code-Review +2
8 comments:
Patchset:
Nice!
File internal/cmd/deadcode/deadcode.go:
Patch Set #14, Line 54: log.SetFlags(0)
Worth adding a comment why you're doing this, for the naive (me)?
f, err := os.Create(*memProfile)
if err != nil {
log.Fatal(err)
}
Why do we do this outside the defer?
Patch Set #14, Line 201: // -filter didn't match
Nit: this comment is probably unnecessary.
Patch Set #14, Line 216: return fns[i].Pos() < fns[j].Pos()
Nit: This sorting relies on ssautil.AllFunctions visiting functions by package. Otherwise fns[i] and fns[j] could be from different variants, and their positions would not be comparable.
Worth a comment, at least. Perhaps, below line 210:
```
// ssautil.AllFunctions visits functions by package, so if fns[i] and fn[j]
// are in the same file, they will be in same package and therefore
// have comparable positions.
```
File internal/cmd/deadcode/deadcode_test.go:
Patch Set #14, Line 64: // TODO(adonovan): permit quotation of spaces.
Is this actually likely to get done? Doesn't seem worth it, IMO.
Patch Set #14, Line 65: strings.Fields(rest)
Isn't this equivalent to `args = fields[1:]`?
File internal/cmd/deadcode/testdata/filterflag.txtar:
Patch Set #14, Line 6: want `func Dead`
Add `!want Live`?
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Tim King, triciu...@appspot.gserviceaccount.com.
Patch set 14:Run-TryBot +1Auto-Submit +1
7 comments:
File internal/cmd/deadcode/deadcode.go:
Patch Set #14, Line 54: log.SetFlags(0)
Worth adding a comment why you're doing this, for the naive (me)?
Done
f, err := os.Create(*memProfile)
if err != nil {
log.Fatal(err)
}
Why do we do this outside the defer?
We want to fail fast if we can't open the file, rather than waste time running a program for the purpose of a profile that can't be written.
Patch Set #14, Line 201: // -filter didn't match
Nit: this comment is probably unnecessary.
Done
Patch Set #14, Line 216: return fns[i].Pos() < fns[j].Pos()
Nit: This sorting relies on ssautil.AllFunctions visiting functions by package. […]
Good point. (BTW AllFunctions is irrelevant: its the order in which files within a
package are parsed that matters, and the truth is that it's parallel, even within a single package, so this is just wrong. Fixed to use token.Position.
File internal/cmd/deadcode/deadcode_test.go:
Patch Set #14, Line 64: // TODO(adonovan): permit quotation of spaces.
Is this actually likely to get done? Doesn't seem worth it, IMO.
Fair point. Changed to a "lossy wrt spaces" comment.
Patch Set #14, Line 65: strings.Fields(rest)
Isn't this equivalent to `args = fields[1:]`?
Yes, once you're comfortable with not doing the right thing w.r.t quotation.
File internal/cmd/deadcode/testdata/filterflag.txtar:
Patch Set #14, Line 6: want `func Dead`
Add `!want Live`?
Done
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Brad Fitzpatrick, Hyang-Ah Hana Kim, Tim King, triciu...@appspot.gserviceaccount.com.
Alan Donovan uploaded patch set #15 to this change.
The following approvals got outdated and were removed: Run-TryBot+1 by Alan Donovan, TryBot-Result+1 by Gopher Robot, gopls-CI+1 by kokoro
internal/cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
---
A internal/cmd/deadcode/deadcode.go
A internal/cmd/deadcode/deadcode_test.go
A internal/cmd/deadcode/doc.go
A internal/cmd/deadcode/testdata/basic.txtar
A internal/cmd/deadcode/testdata/filterflag.txtar
A internal/cmd/deadcode/testdata/lineflag.txtar
A internal/cmd/deadcode/testdata/testflag.txtar
7 files changed, 572 insertions(+), 0 deletions(-)
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alan Donovan, Brad Fitzpatrick, Hyang-Ah Hana Kim, Tim King, triciu...@appspot.gserviceaccount.com.
Kokoro presubmit build finished with status: SUCCESS
Logs at: https://source.cloud.google.com/results/invocations/81172710-a355-43fa-a1aa-16a565c827c8
Patch set 15:gopls-CI +1
Alan Donovan submitted this change.
14 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: internal/cmd/deadcode/deadcode_test.go
Insertions: 3, Deletions: 5.
@@ -57,13 +57,11 @@
}
fields := strings.Fields(line)
- kind := fields[0]
- rest := line[len(kind):]
- switch kind {
+ switch kind := fields[0]; kind {
case "deadcode":
- // TODO(adonovan): permit quotation of spaces.
- args = strings.Fields(rest)
+ args = fields[1:] // lossy wrt spaces
case "want", "!want":
+ rest := line[len(kind):]
str, err := strconv.Unquote(strings.TrimSpace(rest))
if err != nil {
t.Fatalf("bad %s directive <<%s>>", kind, line)
```
```
The name of the file: internal/cmd/deadcode/testdata/filterflag.txtar
Insertions: 1, Deletions: 0.
@@ -4,6 +4,7 @@
want `package "other.net"`
want `func Dead`
+!want `func Live`
!want `package "example.com"`
!want `func unreferenced`
```
```
The name of the file: internal/cmd/deadcode/deadcode.go
Insertions: 14, Deletions: 7.
@@ -51,7 +51,7 @@
func main() {
log.SetPrefix("deadcode: ")
- log.SetFlags(0)
+ log.SetFlags(0) // no time prefix
flag.Usage = usage
flag.Parse()
@@ -198,24 +198,31 @@
sort.Strings(pkgpaths)
for _, pkgpath := range pkgpaths {
if !filter.MatchString(pkgpath) {
- continue // -filter didn't match
+ continue
}
m := byPkgPath[pkgpath]
- // Print functions in source order.
- //
- // This tends to keep related methods
- // such as (T).Marshal and (*T).Unmarshal
+ // 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 := make([]*ssa.Function, 0, len(m))
for fn := range m {
fns = append(fns, fn)
}
sort.Slice(fns, func(i, j int) bool {
- return fns[i].Pos() < fns[j].Pos()
+ 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
})
+ // TODO(adonovan): add an option to skip (or indicate)
+ // dead functions in generated files (see ast.IsGenerated).
+
if *lineFlag {
// line-oriented output
for _, fn := range fns {
```
internal/cmd/deadcode: a command to report dead code in Go programs
This CL adds a command to report functions that are unreachable
from the main functions of applications and tests.
It uses the Rapid Type Analysis (RTA) algorithm to
compute reachability, and reports all functions referenced
by the SSA representation that were not found to be
reachable, grouped by package and sorted by position.
Also, a basic integration test.
Change-Id: Ide78b4e22d4f4066bf901e2d676e5058ca132827
Reviewed-on: https://go-review.googlesource.com/c/tools/+/507738
TryBot-Result: Gopher Robot <go...@golang.org>
Run-TryBot: Alan Donovan <adon...@google.com>
Reviewed-by: Robert Findley <rfin...@google.com>
gopls-CI: kokoro <noreply...@google.com>
---
A internal/cmd/deadcode/deadcode.go
A internal/cmd/deadcode/deadcode_test.go
A internal/cmd/deadcode/doc.go
A internal/cmd/deadcode/testdata/basic.txtar
A internal/cmd/deadcode/testdata/filterflag.txtar
A internal/cmd/deadcode/testdata/lineflag.txtar
A internal/cmd/deadcode/testdata/testflag.txtar
7 files changed, 572 insertions(+), 0 deletions(-)
diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go
new file mode 100644
index 0000000..60e22cb
--- /dev/null
+++ b/internal/cmd/deadcode/deadcode.go
@@ -0,0 +1,240 @@
+// 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.
+
+package main
+
+import (
+ _ "embed"
+ "flag"
+ "fmt"
+ "go/token"
+ "io"
+ "log"
+ "os"
+ "regexp"
+ "runtime"
+ "runtime/pprof"
+ "sort"
+ "strings"
+
+ "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"
+)
+
+//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", "<module>", "report only packages matching this regular expression (default: module of first package)")
+ lineFlag = flag.Bool("line", false, "show output in a line-oriented format")
+ 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() {
+ 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()
+ }()
+ }
+
+ // 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 == "<module>" {
+ 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"))
+ }
+
+ // Compute the reachabilty from main.
+ // (We don't actually build a call graph.)
+ res := rta.Analyze(roots, false)
+
+ // 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() {
+ reachablePosn[prog.Fset.Position(fn.Pos())] = true
+ }
+ }
+
+ // Group unreachable functions by package path.
+ byPkgPath := make(map[string]map[*ssa.Function]bool)
+ for fn := range ssautil.AllFunctions(prog) {
+ if fn.Synthetic != "" {
+ continue // ignore synthetic wrappers etc
+ }
+
+ // Use generic, as instantiations may not have a Pkg.
+ if orig := fn.Origin(); orig != nil {
+ fn = orig
+ }
+
+ // Ignore unreachable 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.
+ if fn.Parent() != nil {
+ continue
+ }
+
+ 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
+ }
+ }
+
+ // Report dead functions grouped by packages.
+ // TODO(adonovan): use maps.Keys, twice.
+ pkgpaths := make([]string, 0, len(byPkgPath))
+ for pkgpath := range byPkgPath {
+ pkgpaths = append(pkgpaths, pkgpath)
+ }
+ 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 := make([]*ssa.Function, 0, len(m))
+ for fn := range m {
+ fns = append(fns, fn)
+ }
+ 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
+ })
+
+ // TODO(adonovan): add an option to skip (or indicate)
+ // dead functions in generated files (see ast.IsGenerated).
+
+ if *lineFlag {
+ // line-oriented output
+ for _, fn := range fns {
+ fmt.Println(fn)
+ }
+ } else {
+ // functions grouped by package
+ fmt.Printf("package %q\n", pkgpath)
+ for _, fn := range fns {
+ fmt.Printf("\tfunc %s\n", fn.RelString(fn.Pkg.Pkg))
+ }
+ fmt.Println()
+ }
+ }
+}
diff --git a/internal/cmd/deadcode/deadcode_test.go b/internal/cmd/deadcode/deadcode_test.go
new file mode 100644
index 0000000..417b816
--- /dev/null
+++ b/internal/cmd/deadcode/deadcode_test.go
@@ -0,0 +1,129 @@
+// 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.
+
+package main_test
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "testing"
+
+ "golang.org/x/tools/internal/testenv"
+ "golang.org/x/tools/txtar"
+)
+
+// Test runs the deadcode command on each scenario
+// described by a testdata/*.txtar file.
+func Test(t *testing.T) {
+ testenv.NeedsTool(t, "go")
+ if runtime.GOOS == "android" {
+ t.Skipf("the dependencies are not available on android")
+ }
+
+ exe := buildDeadcode(t)
+
+ matches, err := filepath.Glob("testdata/*.txtar")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, filename := range matches {
+ filename := filename
+ t.Run(filename, func(t *testing.T) {
+ t.Parallel()
+
+ ar, err := txtar.ParseFile(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Parse archive comment as directives of these forms:
+ //
+ // deadcode args... command-line arguments
+ // [!]want "quoted" expected/unwanted string in output
+ //
+ var args []string
+ want := make(map[string]bool) // string -> sense
+ for _, line := range strings.Split(string(ar.Comment), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" || line[0] == '#' {
+ continue // skip blanks and comments
+ }
+
+ fields := strings.Fields(line)
+ switch kind := fields[0]; kind {
+ case "deadcode":
+ args = fields[1:] // lossy wrt spaces
+ case "want", "!want":
+ rest := line[len(kind):]
+ str, err := strconv.Unquote(strings.TrimSpace(rest))
+ if err != nil {
+ t.Fatalf("bad %s directive <<%s>>", kind, line)
+ }
+ want[str] = kind[0] != '!'
+ default:
+ t.Fatalf("%s: invalid directive %q", filename, kind)
+ }
+ }
+
+ // Write the archive files to the temp directory.
+ tmpdir := t.TempDir()
+ for _, f := range ar.Files {
+ filename := filepath.Join(tmpdir, f.Name)
+ if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filename, f.Data, 0666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // Run the command.
+ cmd := exec.Command(exe, args...)
+ cmd.Stdout = new(bytes.Buffer)
+ cmd.Stderr = new(bytes.Buffer)
+ cmd.Dir = tmpdir
+ cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on")
+ if err := cmd.Run(); err != nil {
+ t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr)
+ }
+
+ // Check each want directive.
+ got := fmt.Sprint(cmd.Stdout)
+ for str, sense := range want {
+ ok := true
+ if strings.Contains(got, str) != sense {
+ if sense {
+ t.Errorf("missing %q", str)
+ } else {
+ t.Errorf("unwanted %q", str)
+ }
+ ok = false
+ }
+ if !ok {
+ t.Errorf("got: <<%s>>", got)
+ }
+ }
+ })
+ }
+}
+
+// buildDeadcode builds the deadcode executable.
+// It returns its path, and a cleanup function.
+func buildDeadcode(t *testing.T) string {
+ bin := filepath.Join(t.TempDir(), "deadcode")
+ if runtime.GOOS == "windows" {
+ bin += ".exe"
+ }
+ cmd := exec.Command("go", "build", "-o", bin)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("Building deadcode: %v\n%s", err, out)
+ }
+ return bin
+}
diff --git a/internal/cmd/deadcode/doc.go b/internal/cmd/deadcode/doc.go
new file mode 100644
index 0000000..cdd24e9
--- /dev/null
+++ b/internal/cmd/deadcode/doc.go
@@ -0,0 +1,58 @@
+// 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.
+
+/*
+The deadcode command reports unreachable functions in Go programs.
+
+Usage: deadcode [flags] package...
+
+The deadcode command loads a Go program from source then uses Rapid
+Type Analysis (RTA) to build a call graph of all the functions
+reachable from the program's main function. Any functions that are not
+reachable are reported as dead code, grouped by package.
+
+Packages are expressed in the notation of 'go list' (or other
+underlying build system if you are using an alternative
+golang.org/x/go/packages driver). Only executable (main) packages are
+considered starting points for the analysis.
+
+The -test flag causes it to analyze test executables too. Tests
+sometimes make use of functions that would otherwise appear to be dead
+code, and public API functions reported as dead with -test indicate
+possible gaps in your test coverage. Bear in mind that an Example test
+function without an "Output:" comment is merely documentation:
+it is dead code, and does not contribute coverage.
+
+The -filter flag restricts results to packages that match the provided
+regular expression; its default value is the module name of the first
+package. Use -filter= to display all results.
+
+Example: show all dead code within the gopls module:
+
+ $ deadcode -test golang.org/x/tools/gopls/...
+
+The analysis can soundly analyze dynamic calls though func values,
+interface methods, and reflection. However, it does not currently
+understand the aliasing created by //go:linkname directives, so it
+will fail to recognize that calls to a linkname-annotated function
+with no body in fact dispatch to the function named in the annotation.
+This may result in the latter function being spuriously reported as dead.
+
+In any case, just because a function is reported as dead does not mean
+it is unconditionally safe to delete it. For example, a dead function
+may be referenced (by another dead function), and a dead method may be
+required to satisfy an interface (that is never called).
+Some judgement is required.
+
+The analysis is valid only for a single GOOS/GOARCH/-tags configuration,
+so a function reported as dead may be live in a different configuration.
+Consider running the tool once for each configuration of interest.
+Use the -line flag to emit a line-oriented output that makes it
+easier to compute the intersection of results across all runs.
+
+THIS TOOL IS EXPERIMENTAL and its interface may change.
+At some point it may be published at cmd/deadcode.
+In the meantime, please give us feedback at github.com/golang/go/issues.
+*/
+package main
diff --git a/internal/cmd/deadcode/testdata/basic.txtar b/internal/cmd/deadcode/testdata/basic.txtar
new file mode 100644
index 0000000..c31d656
--- /dev/null
+++ b/internal/cmd/deadcode/testdata/basic.txtar
@@ -0,0 +1,32 @@
+# Test of basic functionality.
+
+ deadcode -filter= example.com
+
+ want "func (T).Goodbye"
+!want "func (T).Hello"
+ want "func unreferenced"
+
+ want "func Scanf"
+ want "func Printf"
+!want "func Println"
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- main.go --
+package main
+
+import "fmt"
+
+type T int
+
+func main() {
+ var x T
+ x.Hello()
+}
+
+func (T) Hello() { fmt.Println("hello") }
+func (T) Goodbye() { fmt.Println("goodbye") }
+
+func unreferenced() {}
\ No newline at end of file
diff --git a/internal/cmd/deadcode/testdata/filterflag.txtar b/internal/cmd/deadcode/testdata/filterflag.txtar
new file mode 100644
index 0000000..ca1ec43
--- /dev/null
+++ b/internal/cmd/deadcode/testdata/filterflag.txtar
@@ -0,0 +1,39 @@
+# Test of -filter flag.
+
+ deadcode -filter=other.net example.com
+
+ want `package "other.net"`
+ want `func Dead`
+!want `func Live`
+
+!want `package "example.com"`
+!want `func unreferenced`
+
+-- go.work --
+use example.com
+use other.net
+
+-- example.com/go.mod --
+module example.com
+go 1.18
+
+-- example.com/main.go --
+package main
+
+import "other.net"
+
+func main() {
+ other.Live()
+}
+
+func unreferenced() {}
+
+-- other.net/go.mod --
+module other.net
+go 1.18
+
+-- other.net/other.go --
+package other
+
+func Live() {}
+func Dead() {}
diff --git a/internal/cmd/deadcode/testdata/lineflag.txtar b/internal/cmd/deadcode/testdata/lineflag.txtar
new file mode 100644
index 0000000..b817e4c
--- /dev/null
+++ b/internal/cmd/deadcode/testdata/lineflag.txtar
@@ -0,0 +1,32 @@
+# Test of -line output.
+
+ deadcode -line -filter= example.com
+
+ want "(example.com.T).Goodbye"
+!want "(example.com.T).Hello"
+ want "example.com.unreferenced"
+
+ want "fmt.Scanf"
+ want "fmt.Printf"
+!want "fmt.Println"
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- main.go --
+package main
+
+import "fmt"
+
+type T int
+
+func main() {
+ var x T
+ x.Hello()
+}
+
+func (T) Hello() { fmt.Println("hello") }
+func (T) Goodbye() { fmt.Println("goodbye") }
+
+func unreferenced() {}
\ No newline at end of file
diff --git a/internal/cmd/deadcode/testdata/testflag.txtar b/internal/cmd/deadcode/testdata/testflag.txtar
new file mode 100644
index 0000000..1ebfd14
--- /dev/null
+++ b/internal/cmd/deadcode/testdata/testflag.txtar
@@ -0,0 +1,42 @@
+# Test of -test flag.
+
+deadcode -test -filter=example.com example.com/p
+
+ want "func Dead"
+!want "func Live1"
+!want "func Live2"
+
+ want "func ExampleDead"
+!want "func ExampleLive"
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- p/p.go --
+package p
+
+func Live1() {}
+func Live2() {}
+func Dead() {}
+
+-- p/p_test.go --
+package p_test
+
+import "example.com/p"
+
+import "testing"
+
+func Test(t *testing.T) {
+ p.Live1()
+}
+
+func ExampleLive() {
+ p.Live2()
+ // Output:
+}
+
+// A test Example function without an "Output:" comment is never executed.
+func ExampleDead() {
+ p.Dead()
+}
\ No newline at end of file
To view, visit change 507738. To unsubscribe, or for help writing mail filters, visit settings.