tool/listpkgs: add --has-go-generate filter flag too

For use in parallelizing go:generate up-to-date checks.

Updates tailscale/corp#28679

Change-Id: Ifc31c56de4225ba2e0fc048b0f18974dc2f2fc82
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2026-04-22 00:42:20 +00:00 committed by Brad Fitzpatrick
parent d7916d4369
commit 12813dee02

View File

@ -31,6 +31,7 @@ var (
shard = flag.String("shard", "", "if non-empty, a string of the form 'N/M' to only print packages in shard N of M (e.g. '1/3', '2/3', '3/3/' for different thirds of the list)")
affectedByTag = flag.String("affected-by-tag", "", "if non-empty, only list packages whose test binary would be affected by the presence or absence of this build tag")
hasRootTests = flag.Bool("has-root-tests", false, "list packages (as ./relative/path) containing _test.go files that call tstest.RequireRoot")
hasGoGenerate = flag.Bool("has-go-generate", false, "only list packages that contain at least one //go:generate directive")
)
func main() {
@ -121,6 +122,9 @@ Pkg:
continue Pkg
}
}
if *hasGoGenerate && !pkgHasGoGenerate(pkg) {
continue Pkg
}
matches++
if *shard != "" {
@ -291,6 +295,65 @@ func fileMentionsTag(filename, tag string) (bool, error) {
return tags[tag], nil
}
// pkgHasGoGenerate reports whether any source file in pkg contains a
// //go:generate directive.
func pkgHasGoGenerate(pkg *packages.Package) bool {
// Include IgnoredFiles so directives behind build constraints are still
// found; the caller can narrow by tag via -with-tags-all/-without-tags-any
// if they care.
all := slices.Concat(pkg.CompiledGoFiles, pkg.OtherFiles, pkg.IgnoredFiles)
for _, name := range all {
ok, err := fileHasGoGenerate(name)
if err != nil {
log.Printf("reading %s: %v", name, err)
continue
}
if ok {
return true
}
}
return false
}
var (
goGenerateMu sync.Mutex
goGenerate = map[string]bool{} // abs path -> whether file has //go:generate
)
func fileHasGoGenerate(filename string) (bool, error) {
goGenerateMu.Lock()
v, ok := goGenerate[filename]
goGenerateMu.Unlock()
if ok {
return v, nil
}
f, err := os.Open(filename)
if err != nil {
return false, err
}
defer f.Close()
has := false
s := bufio.NewScanner(f)
for s.Scan() {
// go:generate directives must start at column 1 (no leading
// whitespace) to be recognized by the go tool.
if strings.HasPrefix(s.Text(), "//go:generate") {
has = true
break
}
}
if err := s.Err(); err != nil {
return false, fmt.Errorf("reading %s: %w", filename, err)
}
goGenerateMu.Lock()
goGenerate[filename] = has
goGenerateMu.Unlock()
return has, nil
}
// printRootTestPkgs walks the current directory tree looking for _test.go
// files that contain "tstest.RequireRoot" and prints the unique package
// directories as ./relative/path.