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
+}