88 _ "embed"
99 "flag"
1010 "fmt"
11+ "go/ast"
1112 "go/token"
1213 "io"
1314 "log"
@@ -32,10 +33,11 @@ var (
3233 testFlag = flag .Bool ("test" , false , "include implicit test packages and executables" )
3334 tagsFlag = flag .String ("tags" , "" , "comma-separated list of extra build tags (see: go help buildconstraint)" )
3435
35- filterFlag = flag .String ("filter" , "<module>" , "report only packages matching this regular expression (default: module of first package)" )
36- lineFlag = flag .Bool ("line" , false , "show output in a line-oriented format" )
37- cpuProfile = flag .String ("cpuprofile" , "" , "write CPU profile to this file" )
38- memProfile = flag .String ("memprofile" , "" , "write memory profile to this file" )
36+ filterFlag = flag .String ("filter" , "<module>" , "report only packages matching this regular expression (default: module of first package)" )
37+ generatedFlag = flag .Bool ("generated" , true , "report dead functions in generated Go files" )
38+ lineFlag = flag .Bool ("line" , false , "show output in a line-oriented format" )
39+ cpuProfile = flag .String ("cpuprofile" , "" , "write CPU profile to this file" )
40+ memProfile = flag .String ("memprofile" , "" , "write memory profile to this file" )
3941)
4042
4143func usage () {
@@ -104,6 +106,18 @@ func main() {
104106 log .Fatalf ("packages contain errors" )
105107 }
106108
109+ // (Optionally) gather names of generated files.
110+ generated := make (map [string ]bool )
111+ if ! * generatedFlag {
112+ packages .Visit (initial , nil , func (p * packages.Package ) {
113+ for _ , file := range p .Syntax {
114+ if isGenerated (file ) {
115+ generated [p .Fset .File (file .Pos ()).Name ()] = true
116+ }
117+ }
118+ })
119+ }
120+
107121 // If -filter is unset, use first module (if available).
108122 if * filterFlag == "<module>" {
109123 if mod := initial [0 ].Module ; mod != nil && mod .Path != "" {
@@ -176,6 +190,13 @@ func main() {
176190 }
177191
178192 posn := prog .Fset .Position (fn .Pos ())
193+
194+ // If -generated=false, skip functions declared in generated Go files.
195+ // (Functions called by them may still be reported as dead.)
196+ if generated [posn .Filename ] {
197+ continue
198+ }
199+
179200 if ! reachablePosn [posn ] {
180201 reachablePosn [posn ] = true // suppress dups with same pos
181202
@@ -220,9 +241,6 @@ func main() {
220241 return xposn .Line < yposn .Line
221242 })
222243
223- // TODO(adonovan): add an option to skip (or indicate)
224- // dead functions in generated files (see ast.IsGenerated).
225-
226244 if * lineFlag {
227245 // line-oriented output
228246 for _ , fn := range fns {
@@ -238,3 +256,42 @@ func main() {
238256 }
239257 }
240258}
259+
260+ // TODO(adonovan): use go1.21's ast.IsGenerated.
261+
262+ // isGenerated reports whether the file was generated by a program,
263+ // not handwritten, by detecting the special comment described
264+ // at https://go.dev/s/generatedcode.
265+ //
266+ // The syntax tree must have been parsed with the ParseComments flag.
267+ // Example:
268+ //
269+ // f, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.PackageClauseOnly)
270+ // if err != nil { ... }
271+ // gen := ast.IsGenerated(f)
272+ func isGenerated (file * ast.File ) bool {
273+ _ , ok := generator (file )
274+ return ok
275+ }
276+
277+ func generator (file * ast.File ) (string , bool ) {
278+ for _ , group := range file .Comments {
279+ for _ , comment := range group .List {
280+ if comment .Pos () > file .Package {
281+ break // after package declaration
282+ }
283+ // opt: check Contains first to avoid unnecessary array allocation in Split.
284+ const prefix = "// Code generated "
285+ if strings .Contains (comment .Text , prefix ) {
286+ for _ , line := range strings .Split (comment .Text , "\n " ) {
287+ if rest , ok := strings .CutPrefix (line , prefix ); ok {
288+ if gen , ok := strings .CutSuffix (rest , " DO NOT EDIT." ); ok {
289+ return gen , true
290+ }
291+ }
292+ }
293+ }
294+ }
295+ }
296+ return "" , false
297+ }
0 commit comments