Make it an error to run taze on or under a directory with ts_libraries that reference sources in subdirectories.

PiperOrigin-RevId: 225100010
diff --git a/ts_auto_deps/updater/updater.go b/ts_auto_deps/updater/updater.go
index 82eb960..c559236 100644
--- a/ts_auto_deps/updater/updater.go
+++ b/ts_auto_deps/updater/updater.go
@@ -509,6 +509,80 @@
 	return false
 }
 
+// SubdirectorySourcesError is returned when ts_auto_deps detects a BUILD file
+// that references sources in another directory, either in the directory
+// being ts_auto_depsd, or in a super directory.
+type SubdirectorySourcesError struct{}
+
+func (a *SubdirectorySourcesError) Error() string {
+	return "ts_auto_deps doesn't handle referencing sources in another directory " +
+		"- to use ts_auto_deps, migrate to having a BUILD file in every directory. " +
+		"For more details, see go/ts_auto_deps#subdirectory-sources"
+}
+
+// hasSubdirectorySources checks if the BUILD file has ts_libraries that contain
+// source files from subdirectories of the directory with the BUILD. ie foo/BUILD
+// has a src foo/bar/baz.ts, in the subdirectory foo/bar.
+func hasSubdirectorySources(bld *build.File) bool {
+	for _, rule := range buildRules(bld, "ts_library") {
+		srcs := rule.AttrStrings("srcs")
+		if srcs != nil {
+			for _, s := range srcs {
+				if strings.Contains(s, "/") {
+					return true
+				}
+			}
+		} else {
+			// srcs wasn't a list, check for a glob over subdirectory soruces
+			srcExp := rule.Attr("srcs")
+			call, ok := srcExp.(*build.CallExpr)
+			if ok {
+				callName, ok := call.X.(*build.Ident)
+				if ok {
+					if callName.Name == "glob" {
+						for _, arg := range call.List {
+							strArg, ok := arg.(*build.StringExpr)
+							if ok && strings.Contains(strArg.Value, "/") {
+								return true
+							}
+						}
+					}
+				}
+			}
+		}
+		// TODO(b/120783741):
+		// This only handles a lists of files, and a single glob, there are other
+		// cases such as a glob + a list of files that it doesn't handle, but that's
+		// ok since, this is only meant as a caution to the user.
+	}
+
+	return false
+}
+
+// directoryOrAncestorHasSubdirectorySources checks for ts_libraries referencing sources in subdirectories.
+// It checks the current directory's BUILD if it exists, otherwise it checks the nearest
+// ancestor package.
+func directoryOrAncestorHasSubdirectorySources(ctx context.Context, g3root string, buildFilePath string, bld *build.File) (bool, error) {
+	if _, err := platform.Stat(ctx, buildFilePath); err != nil && os.IsNotExist(err) {
+		// Make sure the next closest ancestor package doesn't reference sources in a subdirectory.
+		ancestor, err := FindBUILDFile(ctx, make(map[string]*build.File), g3root, filepath.Dir(bld.Path))
+		if _, ok := err.(*noAncestorBUILDError); ok {
+			// couldn't find an ancestor BUILD, so there aren't an subdirectory sources
+			return false, nil
+		} else if err != nil {
+			return false, err
+		} else if hasSubdirectorySources(ancestor) {
+			return true, nil
+		}
+	}
+
+	if hasSubdirectorySources(bld) {
+		return true, nil
+	}
+
+	return false, nil
+}
+
 func (upd *Updater) addSourcesToBUILD(ctx context.Context, path string, buildFilePath string, bld *build.File) (bool, error) {
 	platform.Infof("Globbing TS sources in %s", path)
 	srcs, err := globSources(ctx, path, []string{"ts", "tsx"})
@@ -628,6 +702,14 @@
 		return false, nil
 	}
 
+	hasSubdirSrcs, err := directoryOrAncestorHasSubdirectorySources(ctx, g3root, buildFilePath, bld)
+	if err != nil {
+		return false, err
+	}
+	if hasSubdirSrcs {
+		return false, &SubdirectorySourcesError{}
+	}
+
 	changed, err := upd.addSourcesToBUILD(ctx, path, buildFilePath, bld)
 	if err != nil {
 		return false, err
@@ -1133,12 +1215,18 @@
 	return nil
 }
 
+type noAncestorBUILDError struct{}
+
+func (nabe *noAncestorBUILDError) Error() string {
+	return "no ancestor BUILD file found"
+}
+
 // FindBUILDFile searches for the closest parent BUILD file above pkg. It
 // returns the parsed BUILD file, or an error if none can be found.
 func FindBUILDFile(ctx context.Context, pkgToBUILD map[string]*build.File,
 	workspaceRoot string, packagePath string) (*build.File, error) {
 	if packagePath == "." || packagePath == "/" {
-		return nil, fmt.Errorf("no ancestor BUILD file found")
+		return nil, &noAncestorBUILDError{}
 	}
 	if bld, ok := pkgToBUILD[packagePath]; ok {
 		return bld, nil
diff --git a/ts_auto_deps/updater/updater_test.go b/ts_auto_deps/updater/updater_test.go
index 1480344..f587baf 100644
--- a/ts_auto_deps/updater/updater_test.go
+++ b/ts_auto_deps/updater/updater_test.go
@@ -526,3 +526,56 @@
 		t.Fatalf("cache contained BUILD file for package")
 	}
 }
+
+func TestHasSubdirectorySources(t *testing.T) {
+	tests := []struct {
+		name      string
+		buildFile string
+		expected  bool
+	}{
+		{
+			name:      "LocalSources",
+			buildFile: `ts_library(name = "lib", srcs = ["foo.ts", "bar.ts"])`,
+			expected:  false,
+		},
+		{
+			name:      "SubdirectorySources",
+			buildFile: `ts_library(name = "lib", srcs = ["subdir/foo.ts", "subdir/bar.ts"])`,
+			expected:  true,
+		},
+		{
+			name:      "LocalNgModuleSources",
+			buildFile: `ng_module(name = "lib", srcs = ["foo.ts", "bar.ts"])`,
+			expected:  false,
+		},
+		{
+			name:      "SubdirectoryNgModuleSources",
+			buildFile: `ng_module(name = "lib", srcs = ["subdir/foo.ts", "subdir/bar.ts"])`,
+			expected:  true,
+		},
+		{
+			name:      "LocalGlob",
+			buildFile: `ts_library(name = "lib", srcs = glob("*.ts"))`,
+			expected:  false,
+		},
+		{
+			name:      "SubdirectoryGlob",
+			buildFile: `ts_library(name = "lib", srcs = glob("**/*.ts"))`,
+			expected:  true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			bld, err := build.ParseBuild("foo/bar/BUILD",
+				[]byte(test.buildFile))
+			if err != nil {
+				t.Fatalf("parse failure: %v", err)
+			}
+
+			actual := hasSubdirectorySources(bld)
+			if actual != test.expected {
+				t.Errorf("got hasSubdirectorySouces() = %v, expected %v", actual, test.expected)
+			}
+		})
+	}
+}