Support aliases and reexporting ts_libraries for ordinary imports.
Instead of querying directly for rules with a given source file in their srcs, load all the rules in the package, and check for aliases and reexporting ts_libraries.
PiperOrigin-RevId: 238708950
diff --git a/ts_auto_deps/analyze/analyze.go b/ts_auto_deps/analyze/analyze.go
index 475ab3e..a80a554 100644
--- a/ts_auto_deps/analyze/analyze.go
+++ b/ts_auto_deps/analyze/analyze.go
@@ -371,26 +371,6 @@
var ambientModuleDeclRE = regexp.MustCompile("(?m)^\\s*declare\\s+module\\s+['\"]([^'\"]+)['\"]\\s+\\{")
-// pathStartsWith checks if path starts with prefix, checking each path segment,
-// so that @angular/core starts with @angular/core, but @angular/core-bananas
-// does not
-func pathStartsWith(path, prefix string) bool {
- pathParts := strings.Split(path, string(os.PathSeparator))
- prefixParts := strings.Split(prefix, string(os.PathSeparator))
-
- if len(prefixParts) > len(pathParts) {
- return false
- }
-
- for i, prefixPart := range prefixParts {
- if prefixPart != pathParts[i] {
- return false
- }
- }
-
- return true
-}
-
// findExistingDepProvidingImport looks through a map of the existing deps to
// see if any of them provide the import in a way that can't be queried
// for. E.g. if the build rule has a "module_name" attribute or if one
@@ -405,28 +385,10 @@
// check if any of the existing deps declare a module_name that matches the import
for _, r := range rt.dependencies {
- moduleName := stringAttribute(r, "module_name")
- if moduleName == "" {
+ resolvedImportPath := resolveAgainstModuleRoot(r, i.importPath)
+ if resolvedImportPath == i.importPath {
continue
}
- if !pathStartsWith(i.importPath, moduleName) {
- continue
- }
- // fmt.Printf("checking if %s is provided by %s\n", i.importPath, moduleName)
- // if module root is a file, remove the file extension, since it'll be added
- // by possibleFilepaths below
- moduleRoot := stripTSExtension(stringAttribute(r, "module_root"))
- _, pkg, _ := edit.ParseLabel(r.GetName())
-
- // resolve the import path against the module name and module root, ie if
- // the import path is @foo/bar and there's a moduleName of @foo the resolved
- // import path is location/of/foo/bar, or if there's also a moduleRoot of
- // baz, the resolved import path is location/of/foo/baz/bar
- //
- // using strings.TrimPrefix for trimming the path is ok, since
- // pathStartsWith already checked that moduleName is a proper prefix of
- // i.importPath
- resolvedImportPath := filepath.Join(pkg, moduleRoot, strings.TrimPrefix(i.importPath, moduleName))
// enumerate all the possible filepaths for the resolved import path, and
// compare against all the srcs
diff --git a/ts_auto_deps/analyze/loader.go b/ts_auto_deps/analyze/loader.go
index d522017..ff9e9eb 100644
--- a/ts_auto_deps/analyze/loader.go
+++ b/ts_auto_deps/analyze/loader.go
@@ -158,30 +158,42 @@
results := make(map[string]*appb.Rule)
addedPaths := make(map[string]bool)
- var possiblePaths []string
+ var possibleFilePaths []string
+ possiblePathToPath := make(map[string]string)
+ // for all the normalized typescript import paths, generate all the possible
+ // corresponding file paths
for _, path := range paths {
if strings.HasPrefix(path, "goog:") {
// 'goog:' imports are resolved using an sstable.
results[path] = nil
continue
}
- if !strings.HasPrefix(path, "@") {
- if _, ok := addedPaths[path]; !ok {
- addedPaths[path] = true
+ if strings.HasPrefix(path, "@") {
+ continue
+ }
- // there isn't a one to one mapping from ts import paths to file
- // paths, so look for all the possible file paths
- possiblePaths = append(possiblePaths, possibleFilepaths(path)...)
+ if _, ok := addedPaths[path]; !ok {
+ addedPaths[path] = true
+
+ // there isn't a one to one mapping from ts import paths to file
+ // paths, so look for all the possible file paths
+ pfs := possibleFilepaths(path)
+ possibleFilePaths = append(possibleFilePaths, pfs...)
+ for _, pf := range pfs {
+ possiblePathToPath[pf] = path
}
}
}
- r, err := q.batchQuery(possiblePaths)
+ // query for all the possible filepaths, to determine which ones are real
+ r, err := q.batchQuery(possibleFilePaths)
if err != nil {
return nil, err
}
- var fileLabels, generators []string
- generatorsToFiles := make(map[string][]*appb.GeneratedFile)
+ var fileLabels, packages []string
+ fileToGeneratorLabel := make(map[string]string)
+ pathToLabels := make(map[string][]string)
+ // get the labels for all the files which exist
for _, target := range r.GetTarget() {
label, err := q.fileLabel(target)
if err != nil {
@@ -191,50 +203,64 @@
case appb.Target_GENERATED_FILE:
file := target.GetGeneratedFile()
generator := file.GetGeneratingRule()
+ label = file.GetName()
+ fileLabels = append(fileLabels, label)
+ _, pkg, _ := edit.ParseLabel(label)
+ packages = append(packages, pkg)
// a generated file can be included as a source by referencing the label
// of the generated file, or the label of the generating rule, so check
// for both
- fileLabels = append(fileLabels, file.GetName())
- generators = append(generators, generator)
- generatorsToFiles[generator] = append(generatorsToFiles[generator], file)
+ fileToGeneratorLabel[labelToPath(label)] = labelToPath(generator)
+ pathToLabels[possiblePathToPath[labelToPath(label)]] = append(pathToLabels[possiblePathToPath[labelToPath(label)]], label)
case appb.Target_SOURCE_FILE:
fileLabels = append(fileLabels, label)
+ _, pkg, _ := edit.ParseLabel(label)
+ packages = append(packages, pkg)
+ pathToLabels[possiblePathToPath[labelToPath(label)]] = append(pathToLabels[possiblePathToPath[labelToPath(label)]], label)
}
}
- filepathToRules := make(map[string][]*appb.Rule)
-
- // load all the rules with file srcs (either literal or generated)
- sourceLabelToRules, err := q.loadRulesWithSources(workspaceRoot, fileLabels)
+ // load all the rules in all the packages files were found in, so we can look
+ // for aliases and reexporting libraries in the same package
+ pkgToAllRules, pkgToActualToAlias, err := q.loadAllRulesInPackages("", packages)
if err != nil {
return nil, err
}
- for label, rules := range sourceLabelToRules {
- for _, rule := range rules {
- filepathToRules[labelToPath(label)] = append(filepathToRules[labelToPath(label)], rule)
- }
- }
-
- // load all the rules with generator rule srcs
- generatorLabelToRules, err := q.loadRulesWithSources(workspaceRoot, generators)
- if err != nil {
- return nil, err
- }
- for label, rules := range generatorLabelToRules {
- for _, rule := range rules {
- for _, generated := range generatorsToFiles[label] {
- filepathToRules[labelToPath(generated.GetName())] = append(filepathToRules[labelToPath(generated.GetName())], rule)
- }
- }
- }
for _, path := range paths {
- // check all the possible file paths for the import path
- for _, fp := range possibleFilepaths(path) {
- if rules, ok := filepathToRules[fp]; ok {
- rule := chooseCanonicalRule(rules)
- results[path] = rule
+ // look up the corresponding file label(s) for the normalized typescript
+ // import path
+ for _, label := range pathToLabels[path] {
+ _, pkg, _ := edit.ParseLabel(label)
+ // get the file path that corresponds to the normalized typescript import
+ // path
+ filePath := labelToPath(label)
+ var matchingDeps []*appb.Rule
+ allRules := pkgToAllRules[pkg]
+ actualToAlias := pkgToActualToAlias[pkg]
+ for _, candidate := range typeScriptRules(allRules) {
+ // check if the rule has the file or the generator of the file in its
+ // srcs
+ possibleSources := []string{filePath}
+ if gl, ok := fileToGeneratorLabel[filePath]; ok {
+ possibleSources = append(possibleSources, gl)
+ }
+ provides, err := q.ruleProvidesImports(candidate, srcsContainsAnyFilePath(possibleSources))
+ if err != nil {
+ return nil, err
+ }
+ if !provides {
+ continue
+ }
+
+ if alias, ok := actualToAlias[candidate.GetName()]; ok {
+ candidate = alias
+ }
+ matchingDeps = append(matchingDeps, candidate)
+ }
+ if len(matchingDeps) > 0 {
+ results[path] = chooseCanonicalRule(matchingDeps)
}
}
}
@@ -243,14 +269,12 @@
}
// chooseCanonicalRule chooses between rules which includes the imported file as
-// a source. It applies heuristics, such as prefering ts_library to other rule
-// types to narrow down the choices. After narrowing, it chooses the first
-// rule. If no rules are left after narrowing, it returns the first rule from
-// the original list.
+// a source.
func chooseCanonicalRule(rules []*appb.Rule) *appb.Rule {
- // filter down to only ts_library rules
+ // if any of the rules is a reexporting lib assume that the reexport was
+ // created for a reason
for _, r := range rules {
- if r.GetRuleClass() == "ts_library" {
+ if isReexportingLib(r) {
return r
}
}
@@ -300,44 +324,6 @@
}
}
-// loadRulesWithSources loads all rules which include the labels in sources as
-// srcs attributes. Returns a map from source label to a list of rules which
-// include it. A source label can be the label of a source file or a generated
-// file or a generating rule.
-func (q *QueryBasedTargetLoader) loadRulesWithSources(workspaceRoot string, sources []string) (map[string][]*appb.Rule, error) {
- pkgToLabels := make(map[string][]string)
- queries := make([]string, 0, len(sources))
- for _, label := range sources {
- _, pkg, file := edit.ParseLabel(label)
- pkgToLabels[pkg] = append(pkgToLabels[pkg], label)
- // Query for all targets in the package which use file.
- queries = append(queries, fmt.Sprintf("attr('srcs', %s, //%s:*)", file, pkg))
- }
- r, err := q.batchQuery(queries)
- if err != nil {
- return nil, err
- }
- labelToRules := make(map[string][]*appb.Rule)
- for _, target := range r.GetTarget() {
- label, err := q.ruleLabel(target)
- if err != nil {
- return nil, err
- }
- rule := target.GetRule()
- _, pkg, _ := edit.ParseLabel(label)
- labels := pkgToLabels[pkg]
- for _, src := range listAttribute(rule, "srcs") {
- for _, l := range labels {
- if src == l {
- labelToRules[l] = append(labelToRules[l], rule)
- break
- }
- }
- }
- }
- return labelToRules, nil
-}
-
// batchQuery runs a set of queries with a single call to Bazel query and the
// '--keep_going' flag.
func (q *QueryBasedTargetLoader) batchQuery(queries []string) (*appb.QueryResult, error) {
@@ -388,6 +374,180 @@
return &result, nil
}
+// ruleProvidesImports checks if the rule directly provides the import, or if
+// it's a reexporting lib, if one of its deps does.
+func (q *QueryBasedTargetLoader) ruleProvidesImports(rule *appb.Rule, srcMatcher func(rule *appb.Rule) bool) (bool, error) {
+ if srcMatcher(rule) {
+ return true, nil
+ }
+
+ if !isReexportingLib(rule) {
+ return false, nil
+ }
+
+ // if the rule is a reexporting library, load all the rules that the rule
+ // reexports, and check if they provide the imported paths. This only handles
+ // one level of reexport.
+ _, pkg, _ := edit.ParseLabel(rule.GetName())
+ // TODO(alexeagle): Batch calls to LoadLabels. Batching calls to ruleProvidesImport
+ // would also be required.
+ exportedRules, err := q.LoadRules(pkg, exportedLabels(rule))
+ if err != nil {
+ return false, err
+ }
+ for _, exportedRule := range exportedRules {
+ if srcMatcher(exportedRule) {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
+
+// exportedLabels returns the labels exported by rule. Exported labels are the
+// deps of a rule if the rule is an alias.
+func exportedLabels(rule *appb.Rule) []string {
+ var exported []string
+ if isReexportingLib(rule) {
+ exported = append(exported, listAttribute(rule, "deps")...)
+ }
+ return exported
+}
+
+// isReexportingLib checks if a library has no sources, which the TS rules use a
+// way to mark a library as an alias.
+func isReexportingLib(rule *appb.Rule) bool {
+ return len(listAttribute(rule, "srcs")) == 0
+}
+
+// srcsContainsPath returns a function, which takes a rule, which returns true
+// if the rule has a src which matches one of the possible filepaths for the
+// provided typescript import path.
+func srcsContainsPath(path string) func(rule *appb.Rule) bool {
+ return func(rule *appb.Rule) bool {
+ resolvedImportPath := resolveAgainstModuleRoot(rule, path)
+
+ // enumerate all the possible filepaths for the resolved import path, and
+ // compare against all the srcs
+ possibleImportPaths := possibleFilepaths(resolvedImportPath)
+ for _, src := range listAttribute(rule, "srcs") {
+ for _, mi := range possibleImportPaths {
+ if mi == labelToPath(src) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+}
+
+// srcsContainsFilePath returns a function which takes a rule, which returns
+// true if the rule has a src which, if pathified, equals one of the filePaths.
+func srcsContainsAnyFilePath(filePaths []string) func(rule *appb.Rule) bool {
+ return func(rule *appb.Rule) bool {
+ for _, filePath := range filePaths {
+ for _, src := range listAttribute(rule, "srcs") {
+ if filePath == labelToPath(src) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+}
+
+// loadAllRulesInPackages loads all rules in all packages.
+//
+// If an alias or aliases are present in the package, the rules for each alias'
+// 'actual' attribute are loaded and a map from each 'actual' rule to its alias
+// rule is constructed.
+//
+// loadAllRulesInPackages returns two maps. The first map is a map from a package
+// label to all of the rules in the package. The second map is a map from a
+// package to the map of 'actual' rules to alias rules for that package.
+func (q *QueryBasedTargetLoader) loadAllRulesInPackages(currentPkg string, packages []string) (map[string][]*appb.Rule, map[string]map[string]*appb.Rule, error) {
+ var missingPackages []string
+ for _, pkg := range packages {
+ if _, ok := q.pkgCache[pkgCacheKey(currentPkg, pkg)]; !ok {
+ missingPackages = append(missingPackages, pkg)
+ }
+ }
+ if len(missingPackages) > 0 {
+ // Load any packages not already available in the cache.
+ var queries []string
+ pkgToRules := make(map[string][]*appb.Rule)
+ pkgToAliasToRule := make(map[string]map[string]*appb.Rule)
+ for _, pkg := range missingPackages {
+ if currentPkg != "" {
+ queries = append(queries, fmt.Sprintf("visible(%s:*, %s:*)", currentPkg, pkg))
+ } else {
+ queries = append(queries, fmt.Sprintf("%s:*", pkg))
+ }
+ pkgToAliasToRule[pkg] = make(map[string]*appb.Rule)
+ }
+ r, err := q.batchQuery(queries)
+ if err != nil {
+ return nil, nil, err
+ }
+ actualToAlias := make(map[string]*appb.Rule)
+ pkgToActuals := make(map[string][]string)
+ for _, target := range r.GetTarget() {
+ if target.GetType() == appb.Target_RULE {
+ rule := target.GetRule()
+ _, pkg, _ := edit.ParseLabel(rule.GetName())
+ if rule.GetRuleClass() == "alias" {
+ // if the package contains an alias, derefence it (but only one layer
+ // of aliases)
+ actual := stringAttribute(rule, "actual")
+ if actual == "" {
+ return nil, nil, fmt.Errorf(`alias %q missing "actual" attribute`, rule.GetName())
+ }
+ actualToAlias[actual] = rule
+ pkgToActuals[pkg] = append(pkgToActuals[pkg], actual)
+ } else {
+ pkgToRules[pkg] = append(pkgToRules[pkg], rule)
+ }
+ }
+ }
+ for pkg, actuals := range pkgToActuals {
+ // Load all the aliased rules, checking if they're visible from the
+ // package where they're aliased from
+ resolvedActuals, err := q.LoadRules(pkg, actuals)
+ if err != nil {
+ return nil, nil, err
+ }
+ for actual, rule := range resolvedActuals {
+ alias := actualToAlias[actual]
+ _, pkg, _ := edit.ParseLabel(alias.GetName())
+ pkgToAliasToRule[pkg][rule.GetName()] = alias
+ pkgToRules[pkg] = append(pkgToRules[pkg], rule)
+ }
+ }
+ for _, pkg := range missingPackages {
+ q.pkgCache[pkgCacheKey(currentPkg, pkg)] = &pkgCacheEntry{
+ rules: pkgToRules[pkg],
+ aliases: pkgToAliasToRule[pkg],
+ }
+ }
+ }
+
+ pkgToRules := make(map[string][]*appb.Rule)
+ pkgToRuleToAlias := make(map[string]map[string]*appb.Rule)
+ for _, pkg := range packages {
+ cacheEntry := q.pkgCache[pkgCacheKey(currentPkg, pkg)]
+ pkgToRules[pkg] = cacheEntry.rules
+ pkgToRuleToAlias[pkg] = cacheEntry.aliases
+ }
+
+ return pkgToRules, pkgToRuleToAlias, nil
+}
+
+func pkgCacheKey(currentPkg, pkg string) string {
+ return currentPkg + "|" + pkg
+}
+
// dedupeLabels returns a new set of labels with no duplicates.
func dedupeLabels(labels []string) []string {
addedLabels := make(map[string]bool)
@@ -420,16 +580,48 @@
}
// resolveAgainstModuleRoot resolves imported against moduleRoot and moduleName.
-func resolveAgainstModuleRoot(label, moduleRoot, moduleName, imported string) string {
- if moduleRoot == "" && moduleName == "" {
+func resolveAgainstModuleRoot(rule *appb.Rule, imported string) string {
+ moduleName := stringAttribute(rule, "module_name")
+ if moduleName == "" {
return imported
}
- trim := strings.TrimPrefix(imported, moduleName)
- if trim == imported {
+ if !pathStartsWith(imported, moduleName) {
return imported
}
- _, pkg, _ := edit.ParseLabel(label)
- return platform.Normalize(filepath.Join(pkg, moduleRoot, trim))
+ // if module root is a file, remove the file extension, since it'll be added
+ // by possibleFilepaths below
+ moduleRoot := stripTSExtension(stringAttribute(rule, "module_root"))
+ _, pkg, _ := edit.ParseLabel(rule.GetName())
+
+ // resolve the import path against the module name and module root, ie if
+ // the import path is @foo/bar and there's a moduleName of @foo the resolved
+ // import path is location/of/foo/bar, or if there's also a moduleRoot of
+ // baz, the resolved import path is location/of/foo/baz/bar
+ //
+ // using strings.TrimPrefix for trimming the path is ok, since
+ // pathStartsWith already checked that moduleName is a proper prefix of
+ // i.importPath
+ return platform.Normalize(filepath.Join(pkg, moduleRoot, strings.TrimPrefix(imported, moduleName)))
+}
+
+// pathStartsWith checks if path starts with prefix, checking each path segment,
+// so that @angular/core starts with @angular/core, but @angular/core-bananas
+// does not
+func pathStartsWith(path, prefix string) bool {
+ pathParts := strings.Split(path, "/")
+ prefixParts := strings.Split(prefix, "/")
+
+ if len(prefixParts) > len(pathParts) {
+ return false
+ }
+
+ for i, prefixPart := range prefixParts {
+ if prefixPart != pathParts[i] {
+ return false
+ }
+ }
+
+ return true
}
// parsePackageName parses and returns the scope and package of imported. For
diff --git a/ts_auto_deps/analyze/loader_test.go b/ts_auto_deps/analyze/loader_test.go
index ddb460c..5765129 100644
--- a/ts_auto_deps/analyze/loader_test.go
+++ b/ts_auto_deps/analyze/loader_test.go
@@ -10,17 +10,63 @@
func TestResolveAgainstModuleRoot(t *testing.T) {
tests := []struct {
- label, moduleRoot, moduleName, imported string
- expectedResolution string
+ ruleLiteral string
+ imported string
+ expected string
}{
- {"//a", "", "", "foo", "foo"},
- {"//b", "", "foo", "bar", "bar"},
- {"//c", "", "foo", "foo/bar", "c/bar"},
- {"//actual/loc:target", "mod/root", "foo/bar", "foo/bar/baz/bam", "actual/loc/mod/root/baz/bam"},
+ {
+ ruleLiteral: `name: "//a"
+ rule_class: "ts_library"`,
+ imported: "foo",
+ expected: "foo",
+ },
+ {
+ ruleLiteral: `name: "//b"
+ rule_class: "ts_library"
+ attribute: <
+ type: 4
+ name: "module_name"
+ string_value: "foo"
+ >`,
+ imported: "bar",
+ expected: "bar",
+ },
+ {
+ ruleLiteral: `name: "//c"
+ rule_class: "ts_library"
+ attribute: <
+ type: 4
+ name: "module_name"
+ string_value: "foo"
+ >`,
+ imported: "foo/bar",
+ expected: "c/bar",
+ },
+ {
+ ruleLiteral: `name: "//actual/loc:target"
+ rule_class: "ts_library"
+ attribute: <
+ type: 4
+ name: "module_name"
+ string_value: "foo/bar"
+ >
+ attribute: <
+ type: 4
+ name: "module_root"
+ string_value: "mod/root"
+ >`,
+ imported: "foo/bar/baz/bam",
+ expected: "actual/loc/mod/root/baz/bam",
+ },
}
for _, test := range tests {
- if resolution := resolveAgainstModuleRoot(test.label, test.moduleRoot, test.moduleName, test.imported); resolution != test.expectedResolution {
- t.Errorf("resolveAgainstModuleRoot(%q): got %q, want %q", test.label, resolution, test.expectedResolution)
+ rule, err := parseRuleLiteral(test.ruleLiteral)
+ if err != nil {
+ t.Errorf("isRuleAnAlias(%q): failed to parse literal: %s", test.ruleLiteral, err)
+ continue
+ }
+ if actual := resolveAgainstModuleRoot(rule, test.imported); actual != test.expected {
+ t.Errorf("resolveAgainstModuleRoot(%q): got %q, want %q", rule.GetName(), actual, test.expected)
}
}
}