Support an alternative collection point for test libraries.

PiperOrigin-RevId: 231291427
diff --git a/ts_auto_deps/main.go b/ts_auto_deps/main.go
index 81542dc..2d1da26 100644
--- a/ts_auto_deps/main.go
+++ b/ts_auto_deps/main.go
@@ -12,8 +12,9 @@
 var (
 	isRoot = flag.Bool("root", false, "the given path is the root of a TypeScript project "+
 		"(generates ts_config and ts_development_sources targets).")
-	recursive = flag.Bool("recursive", false, "recursively update all packages under the given root.")
-	files     = flag.Bool("files", false, "treats arguments as file names. Filters .ts files, then runs on their dirnames.")
+	recursive             = flag.Bool("recursive", false, "recursively update all packages under the given root.")
+	files                 = flag.Bool("files", false, "treats arguments as file names. Filters .ts files, then runs on their dirnames.")
+	allowAllTestLibraries = flag.Bool("allow_all_test_libraries", false, "treats testonly ts_libraries named 'all_tests' as an alternative to ts_config/ts_dev_srcs for registering tests")
 )
 
 func usage() {
@@ -58,7 +59,7 @@
 	}
 
 	host := updater.New(false, false, updater.QueryBasedBazelAnalyze, updater.LocalUpdateFile)
-	if err := updater.Execute(host, paths, *isRoot, *recursive); err != nil {
+	if err := updater.Execute(host, paths, *isRoot, *recursive, *allowAllTestLibraries); err != nil {
 		platform.Error(err)
 	}
 }
diff --git a/ts_auto_deps/updater/test_register.go b/ts_auto_deps/updater/test_register.go
new file mode 100644
index 0000000..794dcec
--- /dev/null
+++ b/ts_auto_deps/updater/test_register.go
@@ -0,0 +1,202 @@
+package updater
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+
+	"github.com/bazelbuild/buildtools/build"
+	"github.com/bazelbuild/rules_typescript/ts_auto_deps/platform"
+)
+
+// isAllTestLibrary identifies testonly ts_libraries named "all_tests".  Taze
+// will register tests with these rules instead of
+// ts_config/ts_development_sources rules to allow users to set up their builds
+// differently.
+func isAllTestLibrary(bld *build.File, r *build.Rule) bool {
+	if !ruleMatches(bld, r, "ts_library", ruleTypeTest) {
+		return false
+	}
+
+	if r.Name() != "all_tests" {
+		return false
+	}
+
+	return true
+}
+
+func getAllTestLibraries(bld *build.File) []*build.Rule {
+	var allTestRules []*build.Rule
+	for _, r := range buildRules(bld, "ts_library") {
+		if isAllTestLibrary(bld, r) {
+			allTestRules = append(allTestRules, r)
+		}
+	}
+	return allTestRules
+}
+
+// RegisterTestRules registers ts_library test targets with the project's
+// ts_config and ts_development_sources rules.  It may also register the tests
+// with a testonly ts_library named "all_tests", which allows users to set up
+// their own BUILD layout.  It's separated from UpdateBUILD since it's non-local,
+// multiple packages may all need to make writes to the same ts_config.
+func (upd *Updater) RegisterTestRules(ctx context.Context, allowAllTestLibrary bool, paths ...string) (bool, error) {
+	reg := &buildRegistry{make(map[string]*build.File), make(map[*build.File]bool)}
+	var g3root string
+	for _, path := range paths {
+		// declare variables manually so that g3root doesn't get overwritten by a :=
+		// declaration
+		var err error
+		var buildPath string
+		g3root, buildPath, err = getBUILDPath(ctx, path)
+		if err != nil {
+			return false, err
+		}
+		bld, err := reg.readBUILD(ctx, g3root, buildPath)
+		if err != nil {
+			return false, err
+		}
+		if tr := getRule(bld, "ts_library", ruleTypeTest); tr != nil {
+			// don't register all_test libraries themselves
+			if isAllTestLibrary(bld, tr) {
+				continue
+			}
+			platform.Infof("Registering test rule in closest ts_config & ts_development_sources")
+			target := AbsoluteBazelTarget(bld, tr.Name())
+			if err := reg.registerTestRule(ctx, bld, tsConfig, g3root, target); err != nil {
+				return false, err
+			}
+			// NodeJS rules should not be added to ts_development_sources automatically, because
+			// they typically do not run in the browser.
+			if tr.AttrString("runtime") != "nodejs" {
+				if err := reg.registerTestRule(ctx, bld, tsDevSrcs, g3root, target); err != nil {
+					return false, err
+				}
+			}
+		}
+	}
+
+	updated := false
+	for b := range reg.filesToUpdate {
+		fmt.Printf("Registered test(s) in %s\n", b.Path)
+		fileChanged, err := upd.maybeWriteBUILD(ctx, filepath.Join(g3root, b.Path), b)
+		if err != nil {
+			return false, err
+		}
+		updated = updated || fileChanged
+	}
+
+	return updated, nil
+}
+
+// buildRegistry buffers reads and writes done while registering ts_libraries
+// with ts_config and ts_development_sources rules, so that registers from
+// multiple packages all get applied at once.
+type buildRegistry struct {
+	bldFiles      map[string]*build.File
+	filesToUpdate map[*build.File]bool
+}
+
+func (reg *buildRegistry) readBUILD(ctx context.Context, workspaceRoot, buildFilePath string) (*build.File, error) {
+	normalizedG3Path, err := getAbsoluteBUILDPath(workspaceRoot, buildFilePath)
+	if err != nil {
+		return nil, err
+	}
+
+	if bld, ok := reg.bldFiles[normalizedG3Path]; ok {
+		return bld, nil
+	}
+
+	bld, err := readBUILD(ctx, workspaceRoot, buildFilePath)
+	if err != nil {
+		return nil, err
+	}
+
+	reg.bldFiles[normalizedG3Path] = bld
+
+	return bld, nil
+}
+
+func (reg *buildRegistry) registerForPossibleUpdate(bld *build.File) {
+	reg.filesToUpdate[bld] = true
+}
+
+type registerTarget int
+
+const (
+	tsConfig registerTarget = iota
+	tsDevSrcs
+)
+
+func (rt registerTarget) kind() string {
+	if rt == tsConfig {
+		return "ts_config"
+	}
+
+	return "ts_development_sources"
+}
+
+func (rt registerTarget) ruleType() ruleType {
+	if rt == tsConfig {
+		return ruleTypeAny
+	}
+
+	return ruleTypeTest
+}
+
+// registerTestRule searches ancestor packages for a rule matching the register
+// target and adds the given target to it. If an all_tests library is found, the
+// rule is registered with it, instead of specified register target. Prints a
+// warning if no rule is found, but only returns an error if adding the
+// dependency fails.
+func (reg *buildRegistry) registerTestRule(ctx context.Context, bld *build.File, rt registerTarget, g3root, target string) error {
+	if buildHasDisableTaze(bld) {
+		return nil
+	}
+
+	var ruleToRegister *build.Rule
+	for _, r := range bld.Rules("") {
+		if isAllTestLibrary(bld, r) {
+			if hasDependency(bld, r, target) {
+				return nil
+			}
+
+			// an all_tests library takes presidence over a registerTarget, and there
+			// can only be one, since there can only be one rule with a given name, so
+			// can just break after finding
+			ruleToRegister = r
+			break
+		}
+		if ruleMatches(bld, r, rt.kind(), rt.ruleType()) {
+			if hasDependency(bld, r, target) {
+				return nil
+			}
+
+			// keep overwriting ruleToRegister so the last match in the BUILD gets
+			// used
+			ruleToRegister = r
+		}
+	}
+
+	if ruleToRegister != nil {
+		addDep(bld, ruleToRegister, target)
+		reg.registerForPossibleUpdate(bld)
+		return nil
+	}
+
+	parentDir := filepath.Dir(filepath.Dir(bld.Path))
+	for parentDir != "." && parentDir != "/" {
+		buildFile := filepath.Join(g3root, parentDir, "BUILD")
+		if _, err := platform.Stat(ctx, buildFile); err == nil {
+			parent, err := reg.readBUILD(ctx, g3root, buildFile)
+			if err != nil {
+				return err
+			}
+			return reg.registerTestRule(ctx, parent, rt, g3root, target)
+		}
+		parentDir = filepath.Dir(parentDir)
+	}
+	fmt.Printf("WARNING: no %s rule in parent packages of %s to register with.\n",
+		rt.kind(), target)
+	return nil
+}
diff --git a/ts_auto_deps/updater/updater.go b/ts_auto_deps/updater/updater.go
index 3f6980b..a45d04c 100644
--- a/ts_auto_deps/updater/updater.go
+++ b/ts_auto_deps/updater/updater.go
@@ -477,16 +477,26 @@
 	return true, nil
 }
 
-func getBUILDPathAndBUILDFile(ctx context.Context, path string) (string, string, *build.File, error) {
+func getBUILDPath(ctx context.Context, path string) (string, string, error) {
 	path = strings.TrimSuffix(path, "/BUILD") // Support both package paths and BUILD files
 	if _, err := platform.Stat(ctx, path); os.IsNotExist(err) {
-		return "", "", nil, err
+		return "", "", err
 	}
 	buildFilePath := filepath.Join(path, "BUILD")
 	g3root, err := workspace.Root(buildFilePath)
 	if err != nil {
+		return "", "", err
+	}
+
+	return g3root, buildFilePath, nil
+}
+
+func getBUILDPathAndBUILDFile(ctx context.Context, path string) (string, string, *build.File, error) {
+	g3root, buildFilePath, err := getBUILDPath(ctx, path)
+	if err != nil {
 		return "", "", nil, err
 	}
+
 	bld, err := readBUILD(ctx, g3root, buildFilePath)
 	if err != nil {
 		platform.Infof("Error reading building file!")
@@ -629,48 +639,6 @@
 	return upd.maybeWriteBUILD(ctx, buildFilePath, bld)
 }
 
-// RegisterTsconfigAndTsDevelopmentSources registers ts_library targets with the project's
-// ts_config and ts_development_sources rules.  It's separated from UpdateBUILD since it's
-// non-local, multiple packages may all need to make writes to the same ts_config.
-func (upd *Updater) RegisterTsconfigAndTsDevelopmentSources(ctx context.Context, paths ...string) (bool, error) {
-	reg := &buildRegistry{make(map[string]*build.File), make(map[*build.File]bool)}
-	var g3root string
-	for _, path := range paths {
-		var bld *build.File
-		var err error
-		g3root, _, bld, err = getBUILDPathAndBUILDFile(ctx, path)
-		if err != nil {
-			return false, err
-		}
-		if tr := getRule(bld, "ts_library", ruleTypeTest); tr != nil {
-			platform.Infof("Registering test rule in closest ts_config & ts_development_sources")
-			target := AbsoluteBazelTarget(bld, tr.Name())
-			if err := reg.registerTestRule(ctx, bld, "ts_config", ruleTypeAny, g3root, target); err != nil {
-				return false, err
-			}
-			// NodeJS rules should not be added to ts_development_sources automatically, because
-			// they typically do not run in the browser.
-			if tr.AttrString("runtime") != "nodejs" {
-				if err := reg.registerTestRule(ctx, bld, "ts_development_sources", ruleTypeTest, g3root, target); err != nil {
-					return false, err
-				}
-			}
-		}
-	}
-
-	updated := false
-	for b := range reg.filesToUpdate {
-		fmt.Printf("Registered test(s) in %s\n", b.Path)
-		fileChanged, err := upd.maybeWriteBUILD(ctx, filepath.Join(g3root, b.Path), b)
-		if err != nil {
-			return false, err
-		}
-		updated = updated || fileChanged
-	}
-
-	return updated, nil
-}
-
 // IsTazeDisabledForDir checks if ts_auto_deps is disabled in the BUILD file in the dir,
 // or if no BUILD file exists, in the closest ancestor BUILD
 func IsTazeDisabledForDir(ctx context.Context, dir string) (bool, error) {
@@ -803,82 +771,6 @@
 	return s, nil, err
 }
 
-// buildRegistry buffers reads and writes done while registering ts_libraries
-// with ts_config and ts_development_sources rules, so that registers from
-// multiple packages all get applied at once.
-type buildRegistry struct {
-	bldFiles      map[string]*build.File
-	filesToUpdate map[*build.File]bool
-}
-
-func (reg *buildRegistry) readBUILD(ctx context.Context, workspaceRoot, buildFilePath string) (*build.File, error) {
-	normalizedG3Path, err := getAbsoluteBUILDPath(workspaceRoot, buildFilePath)
-	if err != nil {
-		return nil, err
-	}
-
-	if bld, ok := reg.bldFiles[normalizedG3Path]; ok {
-		return bld, nil
-	}
-
-	bld, err := readBUILD(ctx, workspaceRoot, buildFilePath)
-	if err != nil {
-		return nil, err
-	}
-
-	reg.bldFiles[normalizedG3Path] = bld
-
-	return bld, nil
-}
-
-func (reg *buildRegistry) registerForPossibleUpdate(bld *build.File) {
-	reg.filesToUpdate[bld] = true
-}
-
-// registerTestRule searches ancestor packages for a rule with the given ruleKind and ruleType
-// and adds the given target to it. Prints a warning if no rule is found, but only returns an error
-// if adding the dependency fails.
-func (reg *buildRegistry) registerTestRule(ctx context.Context, bld *build.File, ruleKind string, rt ruleType, g3root, target string) error {
-	// If the target has already been registered in any of the rule with the given ruleKind and ruleType,
-	// we shouldn't register it again.
-	if targetRegisteredInRule(bld, ruleKind, rt, target) {
-		return nil
-	}
-	if buildHasDisableTaze(bld) {
-		return nil
-	}
-	r := getRule(bld, ruleKind, rt)
-	if r != nil {
-		addDep(bld, r, target)
-		reg.registerForPossibleUpdate(bld)
-		return nil
-	}
-	parentDir := filepath.Dir(filepath.Dir(bld.Path))
-	for parentDir != "." && parentDir != "/" {
-		buildFile := filepath.Join(g3root, parentDir, "BUILD")
-		if _, err := platform.Stat(ctx, buildFile); err == nil {
-			parent, err := reg.readBUILD(ctx, g3root, buildFile)
-			if err != nil {
-				return err
-			}
-			return reg.registerTestRule(ctx, parent, ruleKind, rt, g3root, target)
-		}
-		parentDir = filepath.Dir(parentDir)
-	}
-	ruleTypeStr := ""
-	switch rt {
-	case ruleTypeRegular:
-		ruleTypeStr = "testonly=0"
-	case ruleTypeTest, ruleTypeTestSupport:
-		ruleTypeStr = "testonly=1"
-	default:
-		break
-	}
-	fmt.Printf("WARNING: no %s(%s) rule in parent packages of %s to register with.\n",
-		ruleKind, ruleTypeStr, target)
-	return nil
-}
-
 type ruleType int
 
 const (
@@ -888,6 +780,14 @@
 	ruleTypeTestSupport
 )
 
+// isKind returns true if the rule has given kind.  It also accepts "ng_modules"
+// as "ts_library" kind.
+func isKind(r *build.Rule, kind string) bool {
+	acceptNgModule := kind == "ts_library"
+
+	return r.Kind() == kind || (acceptNgModule && r.Kind() == "ng_module")
+}
+
 func buildRules(bld *build.File, kind string) []*build.Rule {
 	// Find all rules, then filter by kind.
 	// This is nearly the same as just calling bld.Rules(kind), but allows to
@@ -896,9 +796,8 @@
 	// last build rule in the file in case multiple match, regardless of kind.
 	allRules := bld.Rules("")
 	var res []*build.Rule
-	acceptNgModule := kind == "ts_library"
 	for _, r := range allRules {
-		if r.Kind() == kind || (acceptNgModule && r.Kind() == "ng_module") {
+		if isKind(r, kind) {
 			res = append(res, r)
 		}
 	}
@@ -1152,8 +1051,11 @@
 	return r
 }
 
-// ruleMatches return whether a rule matches the specified rt value.
-func ruleMatches(bld *build.File, r *build.Rule, rt ruleType) bool {
+// ruleMatches return whether a rule matches the specified kind and rt value.
+func ruleMatches(bld *build.File, r *build.Rule, kind string, rt ruleType) bool {
+	if !isKind(r, kind) {
+		return false
+	}
 	inTestingDir := determineRuleType(bld.Path, "somefile.ts") == ruleTypeTestSupport
 	hasTestsName := strings.HasSuffix(r.Name(), "_tests")
 	// Accept the rule if it matches the testonly attribute.
@@ -1174,9 +1076,8 @@
 // targetRegisteredInRule returns whether a target has been registered in a rule that
 // matches a specified ruleKind and ruleType in current build file
 func targetRegisteredInRule(bld *build.File, ruleKind string, rt ruleType, target string) bool {
-	rs := buildRules(bld, ruleKind)
-	for _, r := range rs {
-		if ruleMatches(bld, r, rt) && hasDependency(bld, r, target) {
+	for _, r := range bld.Rules("") {
+		if ruleMatches(bld, r, ruleKind, rt) && hasDependency(bld, r, target) {
 			return true
 		}
 	}
@@ -1186,10 +1087,10 @@
 // getRule returns the last rule in bld that has the given ruleKind and matches
 // the specified rt value.
 func getRule(bld *build.File, ruleKind string, rt ruleType) *build.Rule {
-	rs := buildRules(bld, ruleKind)
+	rs := bld.Rules("")
 	for i := len(rs) - 1; i >= 0; i-- {
 		r := rs[i]
-		if ruleMatches(bld, r, rt) {
+		if ruleMatches(bld, r, ruleKind, rt) {
 			return r
 		}
 	}
@@ -1320,7 +1221,7 @@
 }
 
 // Execute runs ts_auto_deps on paths using host.
-func Execute(host *Updater, paths []string, isRoot bool, recursive bool) error {
+func Execute(host *Updater, paths []string, isRoot, recursive, allowAllTestLibraries bool) error {
 	ctx := context.Background()
 	for i, p := range paths {
 		isLastAndRoot := isRoot && i == len(paths)-1
@@ -1339,7 +1240,7 @@
 			}
 		}
 	}
-	host.RegisterTsconfigAndTsDevelopmentSources(ctx, paths...)
+	host.RegisterTestRules(ctx, allowAllTestLibraries, paths...)
 	return nil
 }