internal/cmd/deadcode: add -generated flag
-generated=false will suppress output of unreachable
functions in generated Go source files (as determined by
ast.IsGenerated).
Change-Id: Iab5aa9fbc497a9bcb6a10124e2fe7ab892ad1936
Reviewed-on: https://go-review.googlesource.com/c/tools/+/524946
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Alan Donovan <adonovan@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go
index 60e22cb..f3388aa 100644
--- a/internal/cmd/deadcode/deadcode.go
+++ b/internal/cmd/deadcode/deadcode.go
@@ -8,6 +8,7 @@
_ "embed"
"flag"
"fmt"
+ "go/ast"
"go/token"
"io"
"log"
@@ -32,10 +33,11 @@
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")
+ filterFlag = flag.String("filter", "<module>", "report only packages matching this regular expression (default: module of first package)")
+ generatedFlag = flag.Bool("generated", true, "report dead functions in generated Go files")
+ 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() {
@@ -104,6 +106,18 @@
log.Fatalf("packages contain errors")
}
+ // (Optionally) gather names of generated files.
+ generated := make(map[string]bool)
+ if !*generatedFlag {
+ packages.Visit(initial, nil, func(p *packages.Package) {
+ for _, file := range p.Syntax {
+ if isGenerated(file) {
+ generated[p.Fset.File(file.Pos()).Name()] = true
+ }
+ }
+ })
+ }
+
// If -filter is unset, use first module (if available).
if *filterFlag == "<module>" {
if mod := initial[0].Module; mod != nil && mod.Path != "" {
@@ -176,6 +190,13 @@
}
posn := prog.Fset.Position(fn.Pos())
+
+ // If -generated=false, skip functions declared in generated Go files.
+ // (Functions called by them may still be reported as dead.)
+ if generated[posn.Filename] {
+ continue
+ }
+
if !reachablePosn[posn] {
reachablePosn[posn] = true // suppress dups with same pos
@@ -220,9 +241,6 @@
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 {
@@ -238,3 +256,42 @@
}
}
}
+
+// TODO(adonovan): use go1.21's ast.IsGenerated.
+
+// isGenerated reports whether the file was generated by a program,
+// not handwritten, by detecting the special comment described
+// at https://go.dev/s/generatedcode.
+//
+// The syntax tree must have been parsed with the ParseComments flag.
+// Example:
+//
+// f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
+// if err != nil { ... }
+// gen := ast.IsGenerated(f)
+func isGenerated(file *ast.File) bool {
+ _, ok := generator(file)
+ return ok
+}
+
+func generator(file *ast.File) (string, bool) {
+ for _, group := range file.Comments {
+ for _, comment := range group.List {
+ if comment.Pos() > file.Package {
+ break // after package declaration
+ }
+ // opt: check Contains first to avoid unnecessary array allocation in Split.
+ const prefix = "// Code generated "
+ if strings.Contains(comment.Text, prefix) {
+ for _, line := range strings.Split(comment.Text, "\n") {
+ if rest, ok := strings.CutPrefix(line, prefix); ok {
+ if gen, ok := strings.CutSuffix(rest, " DO NOT EDIT."); ok {
+ return gen, true
+ }
+ }
+ }
+ }
+ }
+ }
+ return "", false
+}