Unpublish ts_auto_deps. PiperOrigin-RevId: 282807385
diff --git a/BUILD.bazel b/BUILD.bazel index 7f69e55..b0a0051 100644 --- a/BUILD.bazel +++ b/BUILD.bazel
@@ -45,8 +45,5 @@ "//devserver:devserver-windows", "//internal:generated_BUILD", "//internal:tsc_wrapped", - "//ts_auto_deps:ts_auto_deps-darwin", - "//ts_auto_deps:ts_auto_deps-linux", - "//ts_auto_deps:ts_auto_deps-windows", ], )
diff --git a/README.md b/README.md index 41028ad..8b1aceb 100644 --- a/README.md +++ b/README.md
@@ -5,7 +5,6 @@ It contains these utilities: - `ts_devserver`: a Go library and binary that runs a fast local web server which concatenates JavaScript on-the-fly. It requires inputs in a named module format (module ids must be contained in the file, not inferred from the file's path). -- `ts_auto_deps`: a Go library and binary which generates `BUILD.bazel` files from TypeScript sources. - `tsc_wrapped`: a TypeScript program which wraps the TypeScript compiler, hosting it under a Bazel worker. - `tsetse`: a collection of third-party "strictness" checks which we add to the TypeScript compiler. - `internal/common/*.bzl`: some Starlark utility code for running the `ts_library` rule.
diff --git a/package.bzl b/package.bzl index 2e4473d..e1c4bb3 100644 --- a/package.bzl +++ b/package.bzl
@@ -42,7 +42,7 @@ sha256 = "f624fe9ca8d51de192655369ac538c420afb7cde16e1ad052554b582fff09287", ) - # For building ts_devserver and ts_auto_deps binaries + # For building ts_devserver binary # See https://github.com/bazelbuild/rules_go#setup for the latest version. _maybe( http_archive, @@ -77,15 +77,6 @@ sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687", ) - # ts_auto_deps depends on com_github_bazelbuild_buildtools - _maybe( - http_archive, - name = "com_github_bazelbuild_buildtools", - url = "https://github.com/bazelbuild/buildtools/archive/0.19.2.1.zip", - strip_prefix = "buildtools-0.19.2.1", - sha256 = "9176a7df34dbed2cf5171eb56271868824560364e60644348219f852f593ae79", - ) - def _maybe(repo_rule, name, **kwargs): if not native.existing_rule(name): repo_rule(name = name, **kwargs)
diff --git a/ts_auto_deps/BUILD.bazel b/ts_auto_deps/BUILD.bazel deleted file mode 100644 index b71e8a1..0000000 --- a/ts_auto_deps/BUILD.bazel +++ /dev/null
@@ -1,48 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") - -go_library( - name = "go_default_library", - srcs = ["main.go"], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps", - visibility = ["//visibility:private"], - deps = [ - "//ts_auto_deps/platform:go_default_library", - "//ts_auto_deps/updater:go_default_library", - ], -) - -go_binary( - name = "ts_auto_deps_bin", - embed = [":go_default_library"], - visibility = ["//visibility:public"], -) - -go_binary( - name = "ts_auto_deps-darwin", - out = "ts_auto_deps-darwin_x64", - embed = [":go_default_library"], - goarch = "amd64", - goos = "darwin", - pure = "on", - visibility = ["//visibility:public"], -) - -go_binary( - name = "ts_auto_deps-linux", - out = "ts_auto_deps-linux_x64", - embed = [":go_default_library"], - goarch = "amd64", - goos = "linux", - pure = "on", - visibility = ["//visibility:public"], -) - -go_binary( - name = "ts_auto_deps-windows", - out = "ts_auto_deps-win32_x64.exe", - embed = [":go_default_library"], - goarch = "amd64", - goos = "windows", - pure = "on", - visibility = ["//visibility:public"], -)
diff --git a/ts_auto_deps/analyze/BUILD.bazel b/ts_auto_deps/analyze/BUILD.bazel deleted file mode 100644 index 29213ce..0000000 --- a/ts_auto_deps/analyze/BUILD.bazel +++ /dev/null
@@ -1,37 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "go_default_library", - srcs = [ - "analyze.go", - "imports.go", - "loader.go", - ], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/analyze", - visibility = ["//visibility:public"], - deps = [ - "//ts_auto_deps/platform:go_default_library", - "//ts_auto_deps/proto:go_default_library", - "//ts_auto_deps/workspace:go_default_library", - "@com_github_bazelbuild_buildtools//build_proto:go_default_library", - "@com_github_bazelbuild_buildtools//edit:go_default_library", - "@com_github_golang_protobuf//proto:go_default_library", - ], -) - -go_test( - name = "go_default_test", - srcs = [ - "analyze_test.go", - "imports_test.go", - "loader_test.go", - ], - embed = [":go_default_library"], - deps = [ - "//ts_auto_deps/platform:go_default_library", - "//ts_auto_deps/proto:go_default_library", - "@com_github_bazelbuild_buildtools//build_proto:go_default_library", - "@com_github_golang_protobuf//proto:go_default_library", - "@com_github_kylelemons_godebug//pretty:go_default_library", - ], -)
diff --git a/ts_auto_deps/analyze/analyze.go b/ts_auto_deps/analyze/analyze.go deleted file mode 100644 index 9e005cc..0000000 --- a/ts_auto_deps/analyze/analyze.go +++ /dev/null
@@ -1,603 +0,0 @@ -// Package analyze uses bazel query to determine and locate missing imports -// in TypeScript source files. -package analyze - -import ( - "context" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/bazelbuild/buildtools/edit" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace" - "github.com/golang/protobuf/proto" - - appb "github.com/bazelbuild/buildtools/build_proto" - arpb "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto" -) - -var ( - extensions = []string{ - // '.d.ts' must come before '.ts' to completely remove the '.d.ts' - // extension. - ".d.ts", - ".ts", - ".tsx", - } -) - -const ( - // debug enables/disables debug logging. Set to true to have debug statements - // print to stdout, set to false to disable debug statements. - debug = false -) - -// debugf prints a formatted message prefixed with "DEBUG:" if the debug -// flag is enabled. -func debugf(format string, v ...interface{}) { - if debug { - fmt.Printf(fmt.Sprintf("DEBUG: %s\n", format), v...) - } -} - -// TargetLoader provides methods for loading targets from BUILD files. -type TargetLoader interface { - // LoadTargets loads targets from BUILD files associated with labels. A target - // is a rule, source file, generated file, package group or environment group. - // It returns a mapping from labels to targets or an error, if any occurred. - // - // A label must be the absolute label associated with a target. For example, - // '//foo/bar:baz' is acceptable whereas 'bar:baz' or '//foo/bar' will result - // in undefined behavior. TODO(lucassloan): make this an error - // - // Only returns targets visible to currentPkg. If currentPkg is an empty - // string returns all targets regardless of visibility. - LoadTargets(currentPkg string, labels []string) (map[string]*appb.Target, error) - // LoadRules loads rules from BUILD files associated with labels. - // It returns a mapping from labels to rules or an error, if any - // occurred. - // - // A label must be the absolute label associated with a rule. For - // example, '//foo/bar:baz' is acceptable whereas 'bar:baz' or '//foo/bar' - // will result in undefined behavior. - // TODO(lucassloan): make this an error. - // - // Only returns rules visible to currentPkg. If currentPkg is an empty string - // returns all rules regardless of visibility. - LoadRules(currentPkg string, labels []string) (map[string]*appb.Rule, error) - // LoadImportPaths loads targets from BUILD files associated with import - // paths relative to a root directory. It returns a mapping from import - // paths to targets or an error, if any occurred. - // - // An import path is the path present in a TypeScript import statement - // resolved relative to the workspace root. For example, an import - // statement 'import baz from "../baz.ts"' declared in the TypeScript - // source file '//foo/bar.ts' would have the import path of 'baz.ts'. If - // no target is found associated with a provided import path, the import - // path should be excluded from the returned mapping but an error should - // not be returned. - // - // Only returns rules visible to currentPkg. If currentPkg is an empty string - // returns all targets regardless of visibility. - LoadImportPaths(ctx context.Context, targetToAnalyze *appb.Rule, currentPkg, root string, paths []string) (map[string]*appb.Rule, error) -} - -// Analyzer uses a BuildLoader to generate dependency reports. -type Analyzer struct { - loader TargetLoader -} - -// New returns a new Analyzer which can be used to generate dependency reports. -func New(loader TargetLoader) *Analyzer { - return &Analyzer{loader: loader} -} - -// Analyze generates a dependency report for each target label in labels. -// -// dir is the directory that ts_auto_deps should execute in. Must be a sub-directory -// of the workspace root. -func (a *Analyzer) Analyze(ctx context.Context, dir string, labels []string) ([]*arpb.DependencyReport, error) { - if len(labels) == 0 { - return nil, nil - } - _, currentPkg, _ := edit.ParseLabel(labels[0]) - for _, label := range labels { - if _, pkg, _ := edit.ParseLabel(label); pkg != currentPkg { - return nil, fmt.Errorf("can't analyze targets in different packages") - } - } - root, err := workspace.Root(dir) - if err != nil { - return nil, err - } - rules, err := a.loader.LoadRules(currentPkg, labels) - if err != nil { - return nil, err - } - resolved, err := a.resolveImportsForTargets(ctx, currentPkg, root, rules) - if err != nil { - return nil, err - } - return a.generateReports(labels, resolved) -} - -// resolvedTarget represents a Bazel target and all resolved information. -type resolvedTarget struct { - label string - // A map of all existing dependencies on a target at the time of analysis. - // The keys are labels and the values are thes loaded target. - dependencies map[string]*appb.Rule - // A map of source file paths to their imports. - imports map[string][]*ts_auto_depsImport - // rule is the original rule the target was constructed from. - rule *appb.Rule - // missingSources are source files which could not be opened on disk. - // These are added to the dependency reports and MissingSources. - missingSources []string - // A map from the labels in the target's srcs to the Targets those - // labels refer. - sources map[string]*appb.Target - literalSourcePaths []string - generatedSourcePaths []string -} - -// setSources sets the sources on t. It returns an error if one of the srcs of -// t's rule isn't in loadedSrcs. It also sorts the sources into literal and -// generated sources, setting literalSourcePaths and generatedSourcePaths. -// Returns an error if all the sources are generated - ts_auto_deps can't read the -// import statements to determine deps. -func (t *resolvedTarget) setSources(loadedSrcs map[string]*appb.Target) error { - for _, label := range listAttribute(t.rule, "srcs") { - src := loadedSrcs[label] - if src == nil { - return fmt.Errorf("no source found for label %s", label) - } - t.sources[label] = src - if src.GetType() == appb.Target_SOURCE_FILE { - t.literalSourcePaths = append(t.literalSourcePaths, labelToPath(label)) - } else { - t.generatedSourcePaths = append(t.generatedSourcePaths, labelToPath(label)) - } - } - if len(t.literalSourcePaths) == 0 && len(t.generatedSourcePaths) > 0 { - return fmt.Errorf("rule has generated sources - cannot determine dependencies") - } - return nil -} - -// srcs returns the labels of the sources of t. -func (t *resolvedTarget) srcs() ([]string, error) { - srcs := listAttribute(t.rule, "srcs") - if srcs == nil { - return nil, fmt.Errorf("target %q missing \"srcs\" attribute", t.label) - } - - return srcs, nil -} - -// getAllLiteralSrcPaths returns the file paths of all the non-generated sources -// of the targets. -func getAllLiteralSrcPaths(targets map[string]*resolvedTarget) ([]string, error) { - var allLiteralSrcPaths []string - for _, t := range targets { - allLiteralSrcPaths = append(allLiteralSrcPaths, t.literalSourcePaths...) - } - - return allLiteralSrcPaths, nil -} - -func (t *resolvedTarget) deps() []string { - return listAttribute(t.rule, "deps") -} - -// provides returns whether the resolved target can provide the path provided. -func (t *resolvedTarget) provides(path string) bool { - for _, label := range listAttribute(t.rule, "srcs") { - src := t.sources[label] - if src.GetType() == appb.Target_SOURCE_FILE { - // For literal sources, check the path of the source - if labelToPath(label) == path { - return true - } - } else if src.GetType() == appb.Target_RULE { - // For generated souces, check against the paths of rule's - // outputs - for _, genSrc := range src.GetRule().GetRuleOutput() { - if labelToPath(genSrc) == path { - return true - } - } - } - } - return false -} - -// newTarget constructs a new target instance from a loaded rule. -func newResolvedTarget(r *appb.Rule) *resolvedTarget { - return &resolvedTarget{ - label: r.GetName(), - dependencies: make(map[string]*appb.Rule), - imports: make(map[string][]*ts_auto_depsImport), - rule: r, - sources: make(map[string]*appb.Target), - } -} - -// resolveImportsForTargets attempts to resolve the imports in the sources of -// each target in targets. -func (a *Analyzer) resolveImportsForTargets(ctx context.Context, currentPkg, root string, allTargets map[string]*appb.Rule) (map[string]*resolvedTarget, error) { - targets := make(map[string]*resolvedTarget) - var allDeps, allSrcs []string - for _, t := range allTargets { - target := newResolvedTarget(t) - targets[target.label] = target - srcs, err := target.srcs() - if err != nil { - return nil, err - } - allDeps = append(allDeps, target.deps()...) - allSrcs = append(allSrcs, srcs...) - } - deps, err := a.loader.LoadRules(currentPkg, allDeps) - if err != nil { - return nil, err - } - // Associate the loaded existing deps with the target or targets which - // contained them. - for _, t := range targets { - for _, dep := range t.deps() { - t.dependencies[dep] = deps[dep] - } - } - // load all the sources in the targets, so that literal and generated - // targets can be distinguished - srcs, err := a.loader.LoadTargets(currentPkg, allSrcs) - if err != nil { - return nil, err - } - for _, t := range targets { - err := t.setSources(srcs) - if err != nil { - return nil, err - } - } - // only extract the imports out of the literal sources, since ts_auto_deps can't - // see the contents of generated files - allLiteralSrcPaths, err := getAllLiteralSrcPaths(targets) - if err != nil { - return nil, err - } - imports, errs := extractAllImports(root, allLiteralSrcPaths) - for _, err := range errs { - // NotExist errors are caught and added to the generated dependency - // reports as missing source files. Only errors which are not NotExist - // errors should be reported. - if !os.IsNotExist(err) { - return nil, err - } - } - for _, t := range targets { - srcs := t.literalSourcePaths - for _, src := range srcs { - v, ok := imports[src] - if ok { - t.imports[src] = v - } else { - // The source was not found on disk during import extraction. - t.missingSources = append(t.missingSources, relativePathLabel(t.label, src)) - } - } - } - if err := a.resolveImports(ctx, currentPkg, root, targets); err != nil { - return nil, err - } - return targets, nil -} - -// resolveImports finds targets which provide the imported file or library -// for imports without known targets. -func (a *Analyzer) resolveImports(ctx context.Context, currentPkg, root string, targets map[string]*resolvedTarget) error { - for _, target := range targets { - var paths []string - needingResolution := make(map[string][]*ts_auto_depsImport) - for _, imports := range target.imports { - handlingImports: - for _, imp := range imports { - resolvedPath := imp.resolvedPath() - for _, path := range pathWithExtensions(resolvedPath) { - if target.provides(path) { - imp.knownTarget = target.label - continue handlingImports - } - } - d, err := a.findExistingDepProvidingImport(ctx, root, target, imp) - if err != nil { - return err - } - if d == "" { - // A target providing the import was not found on the - // existing dependencies or in a comment. Use other - // heuristics. - paths = append(paths, resolvedPath) - needingResolution[resolvedPath] = append(needingResolution[resolvedPath], imp) - continue - } - imp.knownTarget = d - } - } - if len(needingResolution) == 0 { - continue - } - res, err := a.loader.LoadImportPaths(ctx, target.rule, currentPkg, root, paths) - if err != nil { - return err - } - for path, imports := range needingResolution { - if target, ok := res[path]; ok { - for _, imp := range imports { - imp.knownTarget = redirectedLabel(target) - } - } - } - } - return nil -} - -func pathWithExtensions(basename string) []string { - var paths []string - for _, ext := range extensions { - paths = append(paths, basename+ext) - } - return paths -} - -var ambientModuleDeclRE = regexp.MustCompile("(?m)^\\s*declare\\s+module\\s+['\"]([^'\"]+)['\"]\\s+\\{") - -// 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 -// of the .d.ts sources has an ambient module declaration. -// -// If the import already has a knownTarget, findRuleProvidingImport will -// return the knownTarget. -func (a *Analyzer) findExistingDepProvidingImport(ctx context.Context, root string, rt *resolvedTarget, i *ts_auto_depsImport) (string, error) { - if i.knownTarget != "" { - return i.knownTarget, nil - } - - // check if any of the existing deps declare a module_name that matches the import - for _, r := range rt.dependencies { - resolvedImportPath := resolveAgainstModuleRoot(r, i.importPath) - if resolvedImportPath == i.importPath { - continue - } - - // enumerate all the possible filepaths for the resolved import path, and - // compare against all the srcs - possibleImportPaths := possibleFilepaths(resolvedImportPath) - for _, src := range listAttribute(r, "srcs") { - for _, mi := range possibleImportPaths { - if mi == labelToPath(src) { - return r.GetName(), nil - } - } - } - } - - // check if any of the other sources or the souces of any of the deps are .d.ts - // files which have ambient module declarations - var allRules []*appb.Rule - for _, r := range rt.dependencies { - allRules = append(allRules, r) - } - allRules = append(allRules, rt.rule) - for _, r := range allRules { - for _, src := range listAttribute(r, "srcs") { - fp := filepath.Join(root, labelToPath(src)) - if !strings.HasSuffix(fp, ".d.ts") { - continue - } - - contents, err := platform.ReadFile(ctx, fp) - if err != nil { - return "", fmt.Errorf("error reading file looking for ambient module decls: %s", err) - } - - matches := ambientModuleDeclRE.FindAllStringSubmatch(string(contents), -1) - - // put all the ambient modules into a set - declaredModules := make(map[string]bool) - for _, match := range matches { - declaredModules[match[1]] = true - } - - // remove all the modules that were imported (ie all the modules that - // were being augmented/re-opened) - for _, mi := range parseImports(fp, contents) { - delete(declaredModules, mi.importPath) - } - - if declaredModules[i.importPath] { - debugf("found import %s in ambient module declaration in %s", i.importPath, r.GetName()) - return r.GetName(), nil - } - } - } - return "", nil -} - -// stripTSExtension removes TypeScript extensions from a file path. If no -// TypeScript extensions are present, the filepath is returned unaltered. -func stripTSExtension(path string) string { - for _, ext := range extensions { - if strings.HasSuffix(path, ext) { - return strings.TrimSuffix(path, ext) - } - } - return path -} - -// redirectedLabel looks in the target's tags for a tag starting with -// 'alt_dep=' followed by a label. If such a tag is found, the label is -// returned. Otherwise, the target's own label is returned. -func redirectedLabel(target *appb.Rule) string { - for _, tag := range listAttribute(target, "tags") { - if trimmedTag := strings.TrimPrefix(tag, "alt_dep="); trimmedTag != tag { - return trimmedTag - } - } - // No 'alt_dep=' tag was present on the target so no redirects need to occur. - return target.GetName() -} - -func labelToPath(label string) string { - _, pkg, file := edit.ParseLabel(label) - return platform.Normalize(filepath.Clean(filepath.Join(pkg, file))) -} - -// generateReports generates reports for each label in labels. -func (a *Analyzer) generateReports(labels []string, labelToTarget map[string]*resolvedTarget) ([]*arpb.DependencyReport, error) { - reports := make([]*arpb.DependencyReport, 0, len(labels)) - for _, label := range labels { - target, ok := labelToTarget[label] - if !ok { - // This case should never happen. - platform.Fatalf("target %s no longer loaded", label) - } - report, err := a.generateReport(target) - if err != nil { - return nil, err - } - reports = append(reports, report) - } - return reports, nil -} - -// generateReport generates a dependency report for a target. -// -// It adds imports for which no target could be found to unresolved imports. -// Imports which had locatable targets are added to the necessary dependency -// or missing dependency properties if the import was already present on target -// or the import was not already present respectively. -// -// Missing source files detected during import resolution are added to the -// reports. Dependencies which were present on the initial target but are not -// required are added to the unnecessary dependency array. -func (a *Analyzer) generateReport(target *resolvedTarget) (*arpb.DependencyReport, error) { - usedDeps := make(map[string]bool) - report := &arpb.DependencyReport{ - Rule: proto.String(target.label), - MissingSourceFile: target.missingSources, - } - for _, imports := range target.imports { - handlingImports: - for _, imp := range imports { - if imp.knownTarget == target.label { - continue - } - if imp.knownTarget == "" { - if strings.HasPrefix(imp.importPath, "goog:") { - // This feedback needs to be phrased this way since the - // updater.go relies on parsing the feedback strings to - // determine which 'goog:' imports to add. - report.Feedback = append(report.Feedback, - fmt.Sprintf( - "ERROR: %s:%d:%d: missing comment for 'goog:' import, "+ - "please add a trailing comment to the import. E.g.\n "+ - "import Bar from '%s'; // from //foo:bar", - imp.location.sourcePath, imp.location.line, imp.location.offset, imp.importPath)) - } - report.UnresolvedImport = append(report.UnresolvedImport, imp.resolvedPath()) - continue - } - - for _, dep := range target.deps() { - if edit.LabelsEqual(dep, imp.knownTarget, "") { - // fmt.Printf("%s provides %s\n", dep, imp.importPath) - usedDeps[dep] = true - report.NecessaryDependency = append(report.NecessaryDependency, imp.knownTarget) - continue handlingImports - } - } - report.MissingDependencyGroup = append(report.MissingDependencyGroup, &arpb.DependencyGroup{ - Dependency: []string{edit.ShortenLabel(imp.knownTarget, "")}, - ImportPath: []string{imp.importPath}, - }) - } - } - - var unusedDeps []string - for _, dep := range target.deps() { - if _, ok := usedDeps[dep]; !ok { - unusedDeps = append(unusedDeps, dep) - } - } - labelToRule, err := a.loader.LoadRules("", unusedDeps) - if err != nil { - return nil, err - } - for label, rule := range labelToRule { - switch class := rule.GetRuleClass(); class { - case "ts_declaration": - // TypeScript declarations might declare arbitrary global symbols, so it - // is impossible to detect reliably if the import is being used (without - // compiling, at least). Report that the rule has no explicit import as a - // warning, so that ts_auto_deps can decide to import remove or not based on a - // flag. - warning := fmt.Sprintf("WARNING: %s: keeping possibly used %s '%s'", rule.GetLocation(), class, label) - report.Feedback = append(report.Feedback, warning) - case "css_library": - // Similar to ts_declaration, ts_auto_deps can't reliably detect if css_library - // imports are being used, since ts_auto_deps can't currently parse @requirecss - // annotations. Unlike ts_declaration, there's no flag to remove them, so - // there's no need to report a warning. - default: - // The contents of generated files aren't visible, so ts_auto_deps can't discover - // the import statements/deps that they contain. To be safe, don't remove - // any unused deps, since they might be used by the generated file(s). - if len(target.generatedSourcePaths) == 0 { - report.UnnecessaryDependency = append(report.UnnecessaryDependency, label) - } - } - } - return report, nil -} - -// relativePathLabel converts src to a label for a path relative to the -// provided target. For example, a target '//foo/bar' and a src 'foo/bar/baz.ts' -// would result in a relative path label of '//foo/bar:baz.ts'. -func relativePathLabel(label, src string) string { - _, pkg, _ := edit.ParseLabel(label) - return fmt.Sprintf("//%s:%s", pkg, strings.TrimPrefix(src, pkg+"/")) -} - -// listAttribute retrieves the attribute from target with name. -func listAttribute(target *appb.Rule, name string) []string { - if a := attribute(target, name); a != nil { - return a.GetStringListValue() - } - return nil -} - -func stringAttribute(target *appb.Rule, name string) string { - if a := attribute(target, name); a != nil { - return a.GetStringValue() - } - return "" -} - -func attribute(target *appb.Rule, name string) *appb.Attribute { - for _, a := range target.GetAttribute() { - if a.GetName() == name { - return a - } - } - return nil -} - -func isGenerated(rule *appb.Rule) bool { - return stringAttribute(rule, "generator_name") != "" -}
diff --git a/ts_auto_deps/analyze/analyze_test.go b/ts_auto_deps/analyze/analyze_test.go deleted file mode 100644 index 7a7d90a..0000000 --- a/ts_auto_deps/analyze/analyze_test.go +++ /dev/null
@@ -1,657 +0,0 @@ -package analyze - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/golang/protobuf/proto" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/kylelemons/godebug/pretty" - - appb "github.com/bazelbuild/buildtools/build_proto" - arpb "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto" -) - -var ( - testTmpDir = os.Getenv("TEST_TMPDIR") -) - -func TestMain(m *testing.M) { - if err := createWorkspaceFile(); err != nil { - platform.Fatalf("failed to create WORKSPACE file: %q", err) - } - os.Exit(m.Run()) -} - -const ( - testDirectory = "a" -) - -type fakeTargetLoader struct { - targetsByLabels map[string]string - targetsByImportPaths map[string]string -} - -func newFakeTargetLoader() *fakeTargetLoader { - return &fakeTargetLoader{ - targetsByLabels: make(map[string]string), - targetsByImportPaths: make(map[string]string), - } -} - -func (bl *fakeTargetLoader) LoadRules(_ string, labels []string) (map[string]*appb.Rule, error) { - return bl.loadRules(bl.targetsByLabels, labels) -} - -func (bl *fakeTargetLoader) LoadTargets(_ string, labels []string) (map[string]*appb.Target, error) { - targets := make(map[string]*appb.Target) - for _, l := range labels { - if strings.Contains(l, ".") { - targets[l] = &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()} - } else { - targets[l] = &appb.Target{Type: appb.Target_RULE.Enum()} - } - } - return targets, nil -} - -func (bl *fakeTargetLoader) byLabel(label, value string) { - bl.targetsByLabels[label] = value -} - -func (bl *fakeTargetLoader) LoadImportPaths(_ context.Context, _ *appb.Rule, _, _ string, paths []string) (map[string]*appb.Rule, error) { - return bl.loadRules(bl.targetsByImportPaths, paths) -} - -func (bl *fakeTargetLoader) byImportPath(importPath, value string) { - bl.targetsByImportPaths[importPath] = value -} - -func (bl *fakeTargetLoader) loadRules(source map[string]string, keys []string) (map[string]*appb.Rule, error) { - targets := make(map[string]*appb.Rule) - for _, key := range keys { - value, ok := source[key] - if !ok { - return nil, nil - } - var target appb.Rule - if err := proto.UnmarshalText(strings.Trim(value, " \n\r\t"), &target); err != nil { - return nil, err - } - targets[key] = &target - } - return targets, nil -} - -type file struct { - path string - contents []string -} - -// testTarget represents a target with a label, a proto literal, and any import -// paths that should resolve to the target. -type testTarget struct { - label, protoLiteral string - importPaths []string -} - -func analyzeTargets(t *testing.T, labels []string, targets []*testTarget, files []*file) []*arpb.DependencyReport { - t.Helper() - for _, file := range files { - path := filepath.Join(testDirectory, file.path) - if err := createFile(path, file.contents...); err != nil { - t.Errorf("failed to create file %q: %q", file.path, err) - return nil - } - defer os.Remove(path) - } - for i, label := range labels { - labels[i] = fmt.Sprintf("//%s:%s", testDirectory, label) - } - loader := newFakeTargetLoader() - for _, t := range targets { - label := t.label - if !strings.HasPrefix(label, "//") { - label = fmt.Sprintf("//%s:%s", testDirectory, label) - } - r := fmt.Sprintf("name: %q\n%s", label, t.protoLiteral) - loader.byLabel(label, r) - for _, i := range t.importPaths { - loader.byImportPath(i, r) - } - } - r, err := New(loader).Analyze(context.Background(), testTmpDir, labels) - if err != nil { - t.Errorf("Analyze(%q): failed to generate reports: %q", labels, err) - return nil - } - if len(r) != len(labels) { - t.Errorf("Analyze(%q): got %d reports, wanted %d", labels, len(r), len(labels)) - return nil - } - return r -} - -func TestUnresolvedImports(t *testing.T) { - tests := []struct { - filepath, fileContents string - expectedImports []string - }{ - {"b/importer.ts", "import X from './path';", []string{"a/b/path"}}, - {"b/importer.ts", "import X from 'absolute/path';", []string{"absolute/path"}}, - {"b/importer.ts", "import X from '../../root';", []string{"root"}}, - {"b/importer.ts", "import X from './multi/subpath';", []string{"a/b/multi/subpath"}}, - {"b/importer.ts", "import X from '/rooted';", []string{"/rooted"}}, - {"b/importer.ts", "import X from 'absolute/path';\nimport Y from './path';", []string{"absolute/path", "a/b/path"}}, - {"b/importer.ts", "import X from 'some/path'; // from //target:location", nil}, - {"importer.d.ts", "import y from 'some/thing/missing';", []string{"some/thing/missing"}}, - } - for _, test := range tests { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", fmt.Sprintf(` - rule_class: "ts_library" - attribute: < - name: "srcs" - string_list_value: "//a:%s" - type: 5 - >`, test.filepath), nil}, - }, []*file{{test.filepath, []string{test.fileContents}}}) - if r == nil { - continue - } - if diff := pretty.Compare(r[0].GetUnresolvedImport(), test.expectedImports); diff != "" { - t.Errorf("Analyze(%q): failed to detect unresolved imports: (-got, +want)\n%s", test.fileContents, diff) - } - } -} - -func TestUnnecessaryDependencies(t *testing.T) { - tests := [][]string{ - []string{"/* nothing */"}, - } - for _, test := range tests { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - name: "srcs" - string_list_value: "//a:importer.ts" - type: 5 - > - attribute: < - name: "deps" - string_list_value: "//a:b_lib" - type: 5 - >`, nil}, - {"b_lib", ` - rule_class: "ts_library"`, nil}, - }, []*file{{"t/importer.ts", test}}) - if r == nil { - continue - } - if diff := pretty.Compare(r[0].GetUnnecessaryDependency(), []string{"//a:b_lib"}); diff != "" { - t.Errorf("Analyze(%q): failed to detect unnecessary dependencies: (-got, +want)\n%s", test, diff) - } - } -} - -func TestNecessaryDependencies(t *testing.T) { - tests := [][]string{ - []string{"import x from 'b/target';"}, - []string{"// ts_auto_deps: x from //b:b_lib"}, - []string{"export x from 'b/target';"}, - } - for _, test := range tests { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:importer.ts" - > - attribute: < - type: 5 - name: "deps" - string_list_value: "//b:b_lib" - >`, nil}, - {"//b:b_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//b:target.ts" - >`, []string{"b/target"}}, - }, []*file{{"importer.ts", test}}) - if r == nil { - continue - } - if diff := pretty.Compare(r[0].GetNecessaryDependency(), []string{"//b:b_lib"}); diff != "" { - t.Errorf("Analyze(%q): failed to detect necessary deps: (-got, +want)\n%s", test, diff) - } - } -} - -func TestMissingDependencies(t *testing.T) { - tests := []struct { - fileContents string - missingDeps []string - }{ - {"import x from 'b/c';\nimport y from 'angular';", []string{"//b/c:b_lib", "//third_party/javascript/typings/angular"}}, - {"import * as angular from 'angular';\ndeclare module 'angular' { /* reopen */ }", []string{"//third_party/javascript/typings/angular"}}, - } - for _, test := range tests { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:x.ts" - >`, nil}, - {"//b/c:b_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//b/c:index.ts" - >`, []string{"b/c"}}, - {"//third_party/javascript/typings/angular:angular", ` - rule_class: "ts_declaration" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//third_party/javascript/typings/angular:index.d.ts" - >`, []string{"angular"}}, - }, []*file{{"x.ts", []string{test.fileContents}}}) - if r == nil { - continue - } - if diff := pretty.Compare(missingDeps(r[0]), test.missingDeps); diff != "" { - t.Errorf("Analyze(%q): failed to detect missing dependencies: (-got, +want)\n%s", test.fileContents, diff) - } - } -} - -func TestMissingSourceFile(t *testing.T) { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:f1.ts" - string_list_value: "//a:f2.ts" - string_list_value: "//a:p/f3.ts" - >`, nil}, - }, []*file{{"f1.ts", []string{"/* nothing */"}}}) - if r == nil { - t.FailNow() - } - if diff := pretty.Compare(r[0].GetMissingSourceFile(), []string{"//a:f2.ts", "//a:p/f3.ts"}); diff != "" { - t.Fatalf("Analyze: failed to detect missing source files: (-got, +want)\n%s", diff) - } -} - -func TestMultipleLabels(t *testing.T) { - r := analyzeTargets(t, []string{"a_lib", "b_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:a/importer.ts" - >`, nil}, - {"b_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:b/importer.ts" - >`, nil}, - }, []*file{ - {"a/importer.ts", []string{"import X from './path';"}}, - {"b/importer.ts", []string{"import X from './path';"}}, - }) - if r == nil { - t.FailNow() - } - tests := []struct { - label string - unresolvedImports []string - }{ - {"a_lib", []string{"a/a/path"}}, - {"b_lib", []string{"a/b/path"}}, - } - for i, test := range tests { - report := r[i] - if diff := pretty.Compare(report.GetUnresolvedImport(), test.unresolvedImports); diff != "" { - t.Errorf("Analyze(%q): failed to detect unresolved imports: (-got, +want)\n%s", test.label, diff) - } - } -} - -func TestMultipleSourceFiles(t *testing.T) { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:importer.ts" - string_list_value: "//a:exporter.ts" - >`, nil}, - }, []*file{ - {"importer.ts", []string{"import {x} from 'a/exporter';"}}, - {"exporter.ts", []string{"export let x = 12;"}}, - }) - if r == nil { - t.FailNow() - } - if diff := pretty.Compare(missingDeps(r[0]), []string{}); diff != "" { - t.Fatalf("Analyze: failed to detect missing dependencies: (-got, +want)\n%s", diff) - } -} - -func TestRedirectTag(t *testing.T) { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//a:x.ts" - >`, nil}, - {"dlib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "deps" - string_list_value: "//b:clib" - >`, nil}, - {"clib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "//clib:c.ts" - > - attribute: < - type: 5 - name: "tags" - string_list_value: "alt_dep=//d:dlib" - >`, []string{"b/c"}}, - }, []*file{{"x.ts", []string{"import x from 'b/c';"}}}) - if r == nil { - t.FailNow() - } - if diff := pretty.Compare(missingDeps(r[0]), []string{"//d:dlib"}); diff != "" { - t.Fatalf("Analyze: failed to detect missing dependencies: (-got, +want)\n%s", diff) - } -} - -func TestCircularImport(t *testing.T) { - r := analyzeTargets(t, []string{"a_lib"}, []*testTarget{ - {"a_lib", ` - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "f1.ts" - string_list_value: "f2.ts" - >`, []string{"a/f1", "a/f2"}}, - }, []*file{ - {"f1.ts", []string{"import {x} from 'a/f1';", "export let y = x + 1;"}}, - {"f2.ts", []string{"import {y} from 'a/f2';", "export let x = y + 0;"}}, - }) - if r == nil { - t.FailNow() - } - if diff := pretty.Compare(missingDeps(r[0]), []string{}); diff != "" { - t.Fatalf("Analyze: failed to detect missing dependencies: (-got, +want)\n%s", diff) - } -} - -func TestListAttribute(t *testing.T) { - tests := []struct { - name string - value []string - }{ - {"srcs", []string{"a.ts", "b.ts"}}, - {"deps", []string{":core"}}, - } - result, err := createResult(` - target: < - type: 1 - rule: < - name: "//tmp:tmp" - rule_class: "ts_library" - attribute: < - type: 5 - name: "srcs" - string_list_value: "a.ts" - string_list_value: "b.ts" - > - attribute: < - type: 5 - name: "deps" - string_list_value: ":core" - > - > - >`) - if err != nil { - t.Fatalf("failed to create result: %q", err) - } - for _, test := range tests { - attrValue := listAttribute(result.GetTarget()[0].GetRule(), test.name) - if attrValue == nil { - t.Errorf("listAttribute(%q): failed to find attribute", test.name) - continue - } - if diff := pretty.Compare(attrValue, test.value); diff != "" { - t.Errorf("listAttribute(%q): failed to get correct attribute values: (-got, +want)\n%s", test.name, diff) - } - } -} - -func TestStringAttribute(t *testing.T) { - tests := []struct { - name, value string - }{ - {"module_name", "@angular/core"}, - {"module_root", ""}, - } - result, err := createResult(` - target: < - type: 1 - rule: < - name: "//tmp:tmp" - rule_class: "ts_library" - attribute: < - type: 5 - name: "module_name" - string_value: "@angular/core" - > - > - >`) - if err != nil { - t.Fatalf("failed to create result: %q", err) - } - for _, test := range tests { - attrValue := stringAttribute(result.GetTarget()[0].GetRule(), test.name) - if diff := pretty.Compare(attrValue, test.value); diff != "" { - t.Errorf("stringAttribute(%q): failed to get correct attribute values: (-got, +want)\n%s", test.name, diff) - } - } -} - -func TestSetSources(t *testing.T) { - tests := []struct { - name string - srcs []string - loadedSrcs map[string]*appb.Target - err error - expected map[string]*appb.Target - expectedLiteralPaths []string - expectedGeneratedPaths []string - }{ - { - "NoSources", - nil, - nil, - nil, - nil, - nil, - nil, - }, - { - "OneSource", - []string{"//a:file.ts"}, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - }, - nil, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - }, - []string{"a/file.ts"}, - nil, - }, - { - "ExtraSources", - []string{"//a:file.ts"}, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:generator": &appb.Target{Type: appb.Target_RULE.Enum()}, - "//b:wiz": &appb.Target{Type: appb.Target_RULE.Enum()}, - }, - nil, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - }, - []string{"a/file.ts"}, - nil, - }, - { - "MultipleLiteralSources", - []string{"//a:file.ts", "//b:file.ts"}, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - }, - nil, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - }, - []string{"a/file.ts", "b/file.ts"}, - nil, - }, - { - "MultipleGeneratedSources", - []string{"//b:generator", "//b:wiz"}, - map[string]*appb.Target{ - "//b:generator": &appb.Target{Type: appb.Target_RULE.Enum()}, - "//b:wiz": &appb.Target{Type: appb.Target_RULE.Enum()}, - }, - fmt.Errorf("rule has generated sources - cannot determine dependencies"), - nil, - nil, - nil, - }, - { - "MixedSources", - []string{"//a:file.ts", "//b:file.ts", "//b:generator", "//b:wiz"}, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:generator": &appb.Target{Type: appb.Target_RULE.Enum()}, - "//b:wiz": &appb.Target{Type: appb.Target_RULE.Enum()}, - }, - nil, - map[string]*appb.Target{ - "//a:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:file.ts": &appb.Target{Type: appb.Target_SOURCE_FILE.Enum()}, - "//b:generator": &appb.Target{Type: appb.Target_RULE.Enum()}, - "//b:wiz": &appb.Target{Type: appb.Target_RULE.Enum()}, - }, - []string{"a/file.ts", "b/file.ts"}, - []string{"//b:generator", "//b:wiz"}, - }, - { - "MissingSources", - []string{"//a:file.ts"}, - nil, - fmt.Errorf("no source found for label %s", "//a:file.ts"), - nil, - nil, - nil, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - rt := &resolvedTarget{ - rule: &appb.Rule{ - Attribute: []*appb.Attribute{ - &appb.Attribute{ - Name: proto.String("srcs"), - Type: appb.Attribute_STRING_LIST.Enum(), - StringListValue: test.srcs, - }, - }, - }, - sources: make(map[string]*appb.Target), - } - - err := rt.setSources(test.loadedSrcs) - if !reflect.DeepEqual(err, test.err) { - t.Errorf("got err %q, expected %q", err, test.err) - } - - if diff := cmp.Diff(rt.sources, test.expected, cmpopts.EquateEmpty(), cmp.Comparer(proto.Equal)); err == nil && diff != "" { - t.Errorf("failed to set correct sources: (-got, +want)\n%s", diff) - } - }) - } -} - -func missingDeps(report *arpb.DependencyReport) []string { - var deps []string - for _, group := range report.GetMissingDependencyGroup() { - deps = append(deps, group.GetDependency()...) - } - return deps -} - -func createResult(str string) (*appb.QueryResult, error) { - var result appb.QueryResult - return &result, proto.UnmarshalText(strings.Trim(str, " \n\r\t"), &result) -} - -func createFile(path string, content ...string) error { - if !filepath.IsAbs(path) { - path = filepath.Join(filepath.Dir(testTmpDir), path) - } - if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { - return err - } - return ioutil.WriteFile(path, []byte(strings.Join(content, "\n")), 0666) -} - -// This method creates a WORKSPACE file in the root of the Bazel test -// directory. This allows the tests to resolve the root path of the -// workspace by looking for the WORKSPACE file on disk. -func createWorkspaceFile() error { - path := filepath.Join(filepath.Dir(testTmpDir), "WORKSPACE") - return ioutil.WriteFile(path, []byte("workspace(name = 'foo')"), 0666) -}
diff --git a/ts_auto_deps/analyze/imports.go b/ts_auto_deps/analyze/imports.go deleted file mode 100644 index 28f6501..0000000 --- a/ts_auto_deps/analyze/imports.go +++ /dev/null
@@ -1,150 +0,0 @@ -package analyze - -import ( - "io/ioutil" - "path/filepath" - "regexp" - "strings" - "sync" - - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace" -) - -// ts_auto_depsImport represents a single import in a TypeScript source. -type ts_auto_depsImport struct { - // importPath can be an ES6 path ('./foo/bar'), but also a namespace ('goog:...'). - // This is the import path as it appears in the TypeScript source. - importPath string - // knownTarget is the (fully qualified) bazel target providing importPath. - // It's either found by locateMissingTargets or taken from a ts_auto_deps comment. - knownTarget string - location sourceLocation -} - -// resolvedPath is the path to the import relative to the root of the -// workspace. For example, an import of './foo' in the 'bar/baz' directory -// would have a path from root of 'bar/baz/foo'. -// -// Absolute imports have no resolvedPath since the physical location of -// these imports depends on the dependencies of the target the source -// location is a member of. For example, an import of 'foo/bar' would have -// no resolvedPath. -func (i *ts_auto_depsImport) resolvedPath() string { - if strings.HasPrefix(i.importPath, "./") || strings.HasPrefix(i.importPath, "../") { - // If the import is relative to the source location, use the source - // location to form a "canonical" path from the root. - return platform.Normalize(filepath.Clean(filepath.Join(filepath.Dir(i.location.sourcePath), i.importPath))) - } else if trim := strings.TrimPrefix(i.importPath, workspace.Name()+"/"); trim != i.importPath { - return trim - } - // The import is an absolute import and therefore does not have a definite - // resolved path. - return i.importPath -} - -// sourceLocation points to a position in a source file. -type sourceLocation struct { - // Workspace root relative source path. - sourcePath string - // offset and length are byte offsets, line is the 1-indexed source line (considering only \n as breaks). - offset, length, line int -} - -// extractAllImports extracts the TypeScript imports from paths. -// -// paths should be relative to root. The root will be joined to each path -// to construct a physical path to each file. -func extractAllImports(root string, paths []string) (map[string][]*ts_auto_depsImport, []error) { - debugf("extracting imports from TypeScript files relative to %q: %q", root, paths) - allImports := make(map[string][]*ts_auto_depsImport) - var ( - errors []error - mutex sync.Mutex - group sync.WaitGroup - ) - for _, path := range paths { - group.Add(1) - go func(path string) { - defer group.Done() - imports, err := extractImports(root, path) - // Lock the mutex to prevent concurrent writes. - mutex.Lock() - defer mutex.Unlock() - if err != nil { - errors = append(errors, err) - return - } - allImports[path] = imports - }(path) - } - group.Wait() - return allImports, errors -} - -// extractImports extracts the TypeScript imports from a single file. path -// should be a path from the root to the file. -func extractImports(root, path string) ([]*ts_auto_depsImport, error) { - d, err := ioutil.ReadFile(filepath.Join(root, path)) - if err != nil { - return nil, err - } - return parseImports(path, d), nil -} - -const ( - ts_auto_depsFrom = `^[ \t]*//[ \t]+ts_auto_deps:[^\n]*?from[ \t]+(?P<Target>//\S+)$` - importPreface = `^[ \t]*(?:import|export)\b\s*` - wildcardTerm = `\*(?:\s*as\s+\S+)?` // "as..." is optional to match exports. - identifiersClause = `(?:\{[^}]*\}|\S+|` + wildcardTerm + `)` - symbolsTerm = `(?:` + identifiersClause + `(?:,\s*` + identifiersClause + `)?\s*\bfrom\b\s*)?` - url = `['"](?P<URL>[^'";]+)['"]\s*;?` - namespaceComment = `(?:\s*//[ \t]*from[ \t]+(?P<Target>//\S+)$)?` -) - -var importRE = regexp.MustCompile("(?ms)" + - "(?:" + ts_auto_depsFrom + ")|" + - "(?:" + importPreface + symbolsTerm + url + namespaceComment + ")") - -// parseImports scans contents for imports (ES6 modules, ts_auto_deps comments), and -// returns a list of ts_auto_depsImports. knownTarget is already filled in for imports -// that have ts_auto_deps comments. -func parseImports(sourcePath string, contents []byte) []*ts_auto_depsImport { - var imports []*ts_auto_depsImport - lastOffset := 0 - line := 1 - column := 1 - for _, matchIndices := range importRE.FindAllSubmatchIndex(contents, -1) { - imp := &ts_auto_depsImport{} - imports = append(imports, imp) - // matchIndices[0, 1]: full RE match - imp.location.sourcePath = sourcePath - for lastOffset < matchIndices[1] { - // Iterate to the *end* of the import statement. - // The ts_auto_deps comment must be placed at the end of the "import" statement. - // This offset has to be exactly the end of the import for ts_auto_deps later on - // to insert the '// from' comment in the correct line. - column++ - if contents[lastOffset] == '\n' { - line++ - column = 1 - } - lastOffset++ - } - imp.location.offset = matchIndices[0] - imp.location.length = matchIndices[1] - matchIndices[0] - imp.location.line = line - if matchIndices[2] >= 0 { - // matchIndices[2, 3]: Target for a // ts_auto_deps: ... from ... comment. - imp.knownTarget = string(contents[matchIndices[2]:matchIndices[3]]) - } else { - // matchIndices[4, 5]: URL in import x from 'url'; - imp.importPath = string(contents[matchIndices[4]:matchIndices[5]]) - } - if matchIndices[6] >= 0 { - // matchIndices[6, 7]: Target for a // from comment - imp.knownTarget = string(contents[matchIndices[6]:matchIndices[7]]) - } - } - return imports -}
diff --git a/ts_auto_deps/analyze/imports_test.go b/ts_auto_deps/analyze/imports_test.go deleted file mode 100644 index 029df0e..0000000 --- a/ts_auto_deps/analyze/imports_test.go +++ /dev/null
@@ -1,85 +0,0 @@ -package analyze - -import ( - "fmt" - "testing" - - "github.com/kylelemons/godebug/pretty" -) - -func TestParseImports(t *testing.T) { - tests := []struct{ text, importPath, knownTarget string }{ - // Imports - {"import {a} from 'named';", "named", ""}, - {"code before;\n import {a} from 'before after'; code after", - "before after", ""}, - {"import A from 'default';", "default", ""}, - {"import A$X from 'default';", "default", ""}, - {"import {x as $} from 'default';", "default", ""}, - {"import ü from 'default';", "default", ""}, - {"import * as prefix from 'wildcard prefixed';", "wildcard prefixed", ""}, - {" \t import {A, B as C} from 'renamed';", "renamed", ""}, - {"import 'sideeffect import';", "sideeffect import", ""}, - {"import\n {A\n, B} from 'newlines';", "newlines", ""}, - {"import*as prefix from'no whitespace';", "no whitespace", ""}, - {"import Symbol from 'goog:some.Symbol'; // from //target:location", - "goog:some.Symbol", "//target:location"}, - {"import Symbol from 'goog:some.Symbol';//from //target:location", - "goog:some.Symbol", "//target:location"}, - {"import {a} from 'missing semi'", "missing semi", ""}, - {"import {a} from 'missing semi' // from //target:location", - "missing semi", "//target:location"}, - {"import A, {B, C} from 'mixed';", "mixed", ""}, - {"import A, * as B from 'mixed';", "mixed", ""}, - {"import * as B, A from 'inverted mixed';", "inverted mixed", ""}, - // Exports - {"export * from 'wildcard';", "wildcard", ""}, - {"export {a, b} from 'named';", "named", ""}, - {"export {} from 'empty import';", "empty import", ""}, - {"export {a as b} from 'renamed';", "renamed", ""}, - {"export\n {A\n, B} from 'newlines';", "newlines", ""}, - {"export*from'no whitespace';", "no whitespace", ""}, - {"export{}from'no whitespace';", "no whitespace", ""}, - // Comments - {"x;\n// ts_auto_deps: ng from //some/global:rule\ny;", "", "//some/global:rule"}, - {"// ts_auto_deps: ng from //foo/bar from //some/global:rule", "", "//some/global:rule"}, - } - - for i, tst := range tests { - imports := parseImports(fmt.Sprintf("test%d.ts", i), []byte(tst.text)) - if len(imports) != 1 { - t.Errorf("parseImports(%q): got %d import(s), want 1", tst.text, len(imports)) - continue - } - imp := imports[0] - if imp.importPath != tst.importPath { - t.Errorf("parseImports(%q): got %q, want %q", tst.text, imp.importPath, tst.importPath) - } - if imp.knownTarget != tst.knownTarget { - t.Errorf("parseImports(%q): got %q, want %q", tst.text, imp.knownTarget, tst.knownTarget) - } - } -} - -func TestParseImportsSourceLocation(t *testing.T) { - tests := []struct { - text string - expectedSourceLocation sourceLocation - }{ - {"import {a} from 'named';", sourceLocation{line: 1, offset: 0, length: 24}}, - {"\n\timport {a} from 'named';", sourceLocation{line: 2, offset: 1, length: 25}}, - } - for i, tst := range tests { - sourcePath := fmt.Sprintf("test%d.ts", i) - imports := parseImports(sourcePath, []byte(tst.text)) - if len(imports) != 1 { - t.Errorf("parseImports(%q): got %d import(s), want 1", tst.text, len(imports)) - continue - } - imp := imports[0] - tst.expectedSourceLocation.sourcePath = sourcePath - if diff := pretty.Compare(imp.location, tst.expectedSourceLocation); diff != "" { - t.Errorf("parseImports(%q): expected different source location: (-got, +want)\n%s", tst.text, diff) - } - } -}
diff --git a/ts_auto_deps/analyze/loader.go b/ts_auto_deps/analyze/loader.go deleted file mode 100644 index 4a98600..0000000 --- a/ts_auto_deps/analyze/loader.go +++ /dev/null
@@ -1,737 +0,0 @@ -package analyze - -import ( - "bytes" - "context" - "fmt" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/bazelbuild/buildtools/edit" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace" - "github.com/golang/protobuf/proto" - - appb "github.com/bazelbuild/buildtools/build_proto" -) - -// pkgCacheEntry represents a set of loaded rules and a mapping from alias -// to rules from a package. -type pkgCacheEntry struct { - // rules is all rules in a package. - rules []*appb.Rule - // aliases is a map from an alias label to the actual rule of the alias. - aliases map[string]*appb.Rule -} - -// QueryBasedTargetLoader uses Bazel query to load targets from BUILD files. -type QueryBasedTargetLoader struct { - workdir string - bazelBinary string - - // pkgCache is a mapping from a package to all of the rules in said - // package along with a map from aliases to actual rules. - // - // Keys are of the form of "<visibility>|<package>" where visibility - // is the package that rules in package must be visible to and package - // is the actual package that has been loaded and cached. - // - // Since a new target loader is constructed for each directory being - // analyzed in the "-recursive" case, these caches will be garbage - // collected between directories. - pkgCache map[string]*pkgCacheEntry - // labelCache is a mapping from a label to its loaded target. - labelCache map[string]*appb.Target - - // queryCount is the total number of queries executed by the target loader. - queryCount int -} - -// NewQueryBasedTargetLoader constructs a new QueryBasedTargetLoader rooted -// in workdir. -func NewQueryBasedTargetLoader(workdir, bazelBinary string) *QueryBasedTargetLoader { - return &QueryBasedTargetLoader{ - workdir: workdir, - bazelBinary: bazelBinary, - - pkgCache: make(map[string]*pkgCacheEntry), - labelCache: make(map[string]*appb.Target), - } -} - -// LoadRules uses Bazel query to load rules associated with labels from BUILD -// files. -func (q *QueryBasedTargetLoader) LoadRules(pkg string, labels []string) (map[string]*appb.Rule, error) { - labelToTarget, err := q.LoadTargets(pkg, labels) - if err != nil { - return nil, err - } - - labelToRule := make(map[string]*appb.Rule) - for _, label := range labels { - target := labelToTarget[label] - if target.GetType() == appb.Target_RULE { - labelToRule[label] = target.GetRule() - } else { - return nil, fmt.Errorf("target %s contains object of type %q instead of type %q", label, target.GetType(), appb.Target_RULE) - } - } - return labelToRule, nil -} - -// LoadTargets uses Bazel query to load targets associated with labels from BUILD -// files. -func (q *QueryBasedTargetLoader) LoadTargets(pkg string, labels []string) (map[string]*appb.Target, error) { - var labelCacheMisses []string - for _, label := range labels { - if _, ok := q.labelCache[labelCacheKey(pkg, label)]; !ok { - labelCacheMisses = append(labelCacheMisses, label) - } - } - if len(labelCacheMisses) > 0 { - var queries []string - if pkg == "" { - queries = labelCacheMisses - } else { - for _, label := range labelCacheMisses { - queries = append(queries, fmt.Sprintf("visible(%s:*, %s)", pkg, label)) - } - } - r, err := q.batchQuery(queries) - if err != nil { - return nil, err - } - for _, target := range r.GetTarget() { - label, err := q.targetLabel(target) - if err != nil { - return nil, err - } - q.labelCache[labelCacheKey(pkg, label)] = target - } - for _, label := range labelCacheMisses { - key := labelCacheKey(pkg, label) - if _, ok := q.labelCache[key]; !ok { - // Set to nil so the result exists in the cache and is not - // loaded again. If the nil is not added at the appropriate - // cache key, LoadLabels will attempt to load it again when - // next requested instead of getting a cache hit. - q.labelCache[key] = nil - } - } - } - labelToTarget := make(map[string]*appb.Target) - for _, label := range labels { - labelToTarget[label] = q.labelCache[labelCacheKey(pkg, label)] - } - return labelToTarget, nil -} - -func labelCacheKey(currentPkg, label string) string { - return currentPkg + "^" + label -} - -// possibleFilepaths generates the possible filepaths for the ts import path. -// e.g. google3/foo/bar could be foo/bar.ts or foo/bar.d.ts or foo/bar/index.ts, etc. -// Also handles special angular import paths (.ngfactory and .ngsummary). -func possibleFilepaths(importPath string) []string { - // If the path has a suffix of ".ngfactory" or ".ngsummary", it might - // be an Angular AOT generated file. We can infer the target as we - // infer its corresponding ngmodule target by simply stripping the - // ".ngfactory" / ".ngsummary" suffix - importPath = strings.TrimSuffix(strings.TrimSuffix(importPath, ".ngsummary"), ".ngfactory") - importPath = strings.TrimPrefix(importPath, workspace.Name()+"/") - - var possiblePaths []string - - possiblePaths = append(possiblePaths, pathWithExtensions(importPath)...) - possiblePaths = append(possiblePaths, pathWithExtensions(filepath.Join(importPath, "index"))...) - - return possiblePaths -} - -// LoadImportPaths uses Bazel Query to load targets associated with import -// paths from BUILD files. -func (q *QueryBasedTargetLoader) LoadImportPaths(ctx context.Context, targetToAnalyze *appb.Rule, currentPkg, workspaceRoot string, paths []string) (map[string]*appb.Rule, error) { - debugf("loading imports visible to %q relative to %q: %q", currentPkg, workspaceRoot, paths) - results := make(map[string]*appb.Rule) - - addedPaths := make(map[string]bool) - var possibleFilePaths []string - possiblePathToPaths := 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, "@") { - continue - } - - 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...) - // map the file paths back to the import paths so we can map the file - // labels back to the import paths - for _, pf := range pfs { - possiblePathToPaths[pf] = append(possiblePathToPaths[pf], path) - } - } - } - - // 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, 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 { - return nil, err - } - switch target.GetType() { - 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 - fileToGeneratorLabel[labelToPath(label)] = labelToPath(generator) - // map file label back to the import paths so that they can be looked for - // in the srcs of the rules - for _, path := range possiblePathToPaths[labelToPath(label)] { - pathToLabels[path] = append(pathToLabels[path], label) - } - case appb.Target_SOURCE_FILE: - fileLabels = append(fileLabels, label) - _, pkg, _ := edit.ParseLabel(label) - packages = append(packages, pkg) - // map file label back to the import paths so that they can be looked for - // in the srcs of the rules - for _, path := range possiblePathToPaths[labelToPath(label)] { - pathToLabels[path] = append(pathToLabels[path], label) - } - } - } - - // 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 _, path := range paths { - // 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) - allRules := pkgToAllRules[pkg] - actualToAlias := pkgToActualToAlias[pkg] - var matchingDeps []*appb.Rule - 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 { - canonicalRule, err := q.chooseCanonicalDep(currentPkg, targetToAnalyze, matchingDeps) - if err != nil { - return nil, err - } - results[path] = canonicalRule - } - } - } - - return results, nil -} - -// chooseCanonicalDep chooses between rules which include the imported file as -// a source (ie the rule that includes the file as a src, and any reexporting -// libraries). -// -// It filters the rules in a 3 stage process: -// -// 1. If only one of the rules is visible, choose that one, since the rule -// creator intended it to be imported. -// -// 2. If all or none of the rules are visible, choose the rule that directly -// includes the file as a src, since that reduces the chance of introducing -// circular dependencies. -// -// 3. Choose the rule that is already included as a dep. -func (q *QueryBasedTargetLoader) chooseCanonicalDep(currentPkg string, targetToAnalyze *appb.Rule, deps []*appb.Rule) (*appb.Rule, error) { - // check for visibility - filterForVisibility := func(deps []*appb.Rule) ([]*appb.Rule, error) { - var labels []string - for _, d := range deps { - labels = append(labels, d.GetName()) - } - visibleDepsMap, err := q.LoadRules(currentPkg, labels) - if err != nil { - return nil, err - } - - var visibleDeps []*appb.Rule - for _, d := range visibleDepsMap { - if d != nil { - visibleDeps = append(visibleDeps, d) - } - } - - return visibleDeps, nil - } - - // if there's a visible reexporting lib and a visible lib with the src, favor - // the lib with the src, to reduce the chance of introducing a circular - // dependency - filterForBaseLibs := func(deps []*appb.Rule) ([]*appb.Rule, error) { - var baseDeps []*appb.Rule - for _, d := range deps { - if !isReexportingLib(d) { - baseDeps = append(baseDeps, d) - } - } - - return baseDeps, nil - } - - // favor the dep that's already on the rule - filterForExistingDeps := func(deps []*appb.Rule) ([]*appb.Rule, error) { - var existingDeps []*appb.Rule - for _, d := range deps { - for _, existing := range listAttribute(targetToAnalyze, "deps") { - if d.GetName() == existing { - existingDeps = append(existingDeps, d) - } - } - } - - return existingDeps, nil - } - - filters := []func(deps []*appb.Rule) ([]*appb.Rule, error){ - filterForVisibility, - filterForBaseLibs, - filterForExistingDeps, - } - - // for each filter, return if it returned a single rule, narrow the set of deps if - // it discarded some, but not all, and try the full set with the next filter if it - // discarded them all - for _, filter := range filters { - filteredDeps, err := filter(deps) - if err != nil { - return nil, err - } - if len(filteredDeps) == 1 { - return filteredDeps[0], nil - } else if len(filteredDeps) > 0 { - deps = filteredDeps - } - } - - // no filter got down to a single rule, just return the first - return deps[0], nil -} - -// ruleLabel returns the label for a target which is a rule. Returns an error if -// target is not a rule. -func (q *QueryBasedTargetLoader) ruleLabel(target *appb.Target) (string, error) { - if t := target.GetType(); t != appb.Target_RULE { - return "", fmt.Errorf("target contains object of type %q instead of type %q", t, appb.Target_RULE) - } - return target.GetRule().GetName(), nil -} - -// fileLabel returns the label for a target which is a file. Returns an error if -// target is not a source file or a generated file. -func (q *QueryBasedTargetLoader) fileLabel(target *appb.Target) (string, error) { - switch t := target.GetType(); t { - case appb.Target_GENERATED_FILE: - return target.GetGeneratedFile().GetName(), nil - case appb.Target_SOURCE_FILE: - return target.GetSourceFile().GetName(), nil - default: - return "", fmt.Errorf("target contains object of type %q instead of type %q or %q", t, appb.Target_SOURCE_FILE, appb.Target_GENERATED_FILE) - } -} - -// targetLabel returns the label for a target. Returns an error if target is an -// unknown type. -func (q *QueryBasedTargetLoader) targetLabel(target *appb.Target) (string, error) { - switch t := target.GetType(); t { - case appb.Target_GENERATED_FILE: - return target.GetGeneratedFile().GetName(), nil - case appb.Target_SOURCE_FILE: - return target.GetSourceFile().GetName(), nil - case appb.Target_RULE: - return target.GetRule().GetName(), nil - case appb.Target_PACKAGE_GROUP: - return target.GetPackageGroup().GetName(), nil - case appb.Target_ENVIRONMENT_GROUP: - return target.GetEnvironmentGroup().GetName(), nil - default: - return "", fmt.Errorf("target contains object of unknown type %q", t) - } -} - -// 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) { - // Join all of the queries with a '+' character according to Bazel's - // syntax for running multiple queries. - return q.query("--keep_going", strings.Join(queries, "+")) -} - -func (q *QueryBasedTargetLoader) query(args ...string) (*appb.QueryResult, error) { - n := len(args) - if n < 1 { - return nil, fmt.Errorf("expected at least one argument") - } - query := args[n-1] - if query == "" { - // An empty query was provided so return an empty result without - // making a call to Bazel. - return &appb.QueryResult{}, nil - } - var stdout, stderr bytes.Buffer - args = append([]string{"query", "--output=proto"}, args...) - q.queryCount++ - debugf("executing query #%d in %q: %s %s %q", q.queryCount, q.workdir, q.bazelBinary, strings.Join(args[:len(args)-1], " "), query) - cmd := exec.Command(q.bazelBinary, args...) - cmd.Dir = q.workdir - cmd.Stdout = &stdout - cmd.Stderr = &stderr - startTime := time.Now() - if err := cmd.Run(); err != nil { - // Exit status 3 is a direct result of one or more queries in a set of - // queries not returning a result while running with the '--keep_going' - // flag. Since one query failing to return a result does not hinder the - // other queries from returning a result, ignore these errors. - // - // Herb prints "printing partial results" to indicate the same as bazel's - // exit status 3 - if err.Error() != "exit status 3" && !strings.Contains(stderr.String(), "printing partial results") { - // The error provided as a result is less useful than the contents of - // stderr for debugging. - return nil, fmt.Errorf(stderr.String()) - } - } - debugf("query #%d took %v", q.queryCount, time.Since(startTime)) - var result appb.QueryResult - if err := proto.Unmarshal(stdout.Bytes(), &result); err != nil { - return nil, err - } - 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 == "" { - // probably an alias with a select statement as the value for - // 'actual' - just ignore - platform.Infof(`alias %q has non-string "actual" attribute`, rule.GetName()) - continue - } - 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 targets, checking if they're visible from the - // package where they're aliased from - resolvedActuals, err := q.LoadTargets(pkg, actuals) - if err != nil { - return nil, nil, err - } - for actual, target := range resolvedActuals { - // aliases can be for anything, but deps can only be rules, so ignore - // other aliased targets - if target.GetType() != appb.Target_RULE { - continue - } - - rule := target.GetRule() - 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) - var uniqueLabels []string - for _, label := range labels { - if _, added := addedLabels[label]; !added { - addedLabels[label] = true - uniqueLabels = append(uniqueLabels, label) - } - } - return uniqueLabels -} - -// typeScriptRules returns all TypeScript rules in rules. -func typeScriptRules(rules []*appb.Rule) []*appb.Rule { - var tsRules []*appb.Rule - for _, rule := range rules { - for _, supportedRuleClass := range []string{ - "ts_library", - "ts_declaration", - "ng_module", - } { - if rule.GetRuleClass() == supportedRuleClass { - tsRules = append(tsRules, rule) - break - } - } - } - return tsRules -} - -// resolveAgainstModuleRoot resolves imported against moduleRoot and moduleName. -func resolveAgainstModuleRoot(rule *appb.Rule, imported string) string { - moduleName := stringAttribute(rule, "module_name") - if moduleName == "" { - return imported - } - if !pathStartsWith(imported, moduleName) { - return imported - } - // 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 -// example, "@foo/bar" would have a scope of "@foo" and a package of "bar". -func parsePackageName(imported string) (string, string) { - firstSlash := strings.Index(imported, "/") - if firstSlash == -1 { - return imported, "" - } - afterSlash := imported[firstSlash+1:] - if secondSlash := strings.Index(afterSlash, "/"); secondSlash > -1 { - return imported[:firstSlash], afterSlash[:secondSlash] - } - return imported[:firstSlash], afterSlash -}
diff --git a/ts_auto_deps/analyze/loader_test.go b/ts_auto_deps/analyze/loader_test.go deleted file mode 100644 index 5765129..0000000 --- a/ts_auto_deps/analyze/loader_test.go +++ /dev/null
@@ -1,96 +0,0 @@ -package analyze - -import ( - "testing" - - "github.com/golang/protobuf/proto" - - appb "github.com/bazelbuild/buildtools/build_proto" -) - -func TestResolveAgainstModuleRoot(t *testing.T) { - tests := []struct { - ruleLiteral string - imported string - expected string - }{ - { - 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 { - 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) - } - } -} - -func TestParsePackageName(t *testing.T) { - tests := []struct { - input, scope, pkg string - }{ - {"foo/bar", "foo", "bar"}, - {"foo/bar/baz", "foo", "bar"}, - {"foo", "foo", ""}, - {"", "", ""}, - } - for _, test := range tests { - if scope, pkg := parsePackageName(test.input); scope != test.scope || pkg != test.pkg { - t.Errorf("moduleName(%q): got %q, %q, want %q, %q", test.input, scope, pkg, test.scope, test.pkg) - } - } -} - -func parseRuleLiteral(literal string) (*appb.Rule, error) { - var rule appb.Rule - if err := proto.UnmarshalText(literal, &rule); err != nil { - return nil, err - } - return &rule, nil -}
diff --git a/ts_auto_deps/main.go b/ts_auto_deps/main.go deleted file mode 100644 index 553afbc..0000000 --- a/ts_auto_deps/main.go +++ /dev/null
@@ -1,54 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/updater" -) - -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.") -) - -func usage() { - fmt.Fprintf(os.Stderr, `ts_auto_deps: generate BUILD rules for TypeScript sources. - -usage: %s [flags] [path...] - -ts_auto_deps generates and updates BUILD rules for each of the given package paths. -Paths are expected to reside underneath the workspace root. If none is given, -ts_auto_deps runs on the current working directory. - -For each of the given package paths, ts_auto_deps finds all TypeScript sources in the -package and adds sources that are not currently built to the appropriate -BUILD rule (ts_library or ts_declaration). - -If there is no matching BUILD rule, or no BUILD file, ts_auto_deps will create either. - -ts_auto_deps also updates BUILD rule dependencies ('deps') based on the source imports. - -Flags: -`, os.Args[0]) - flag.PrintDefaults() -} - -func main() { - flag.Usage = usage - flag.Parse() - - paths, err := updater.Paths(*isRoot, *files, *recursive) - if err != nil { - platform.Error(err) - } - - host := updater.New(false, false, updater.QueryBasedBazelAnalyze, updater.LocalUpdateFile) - if err := updater.Execute(host, paths, *isRoot, *recursive); err != nil { - platform.Error(err) - } -}
diff --git a/ts_auto_deps/platform/BUILD.bazel b/ts_auto_deps/platform/BUILD.bazel deleted file mode 100644 index 9e351ab..0000000 --- a/ts_auto_deps/platform/BUILD.bazel +++ /dev/null
@@ -1,11 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = [ - "file.go", - "log.go", - ], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform", - visibility = ["//visibility:public"], -)
diff --git a/ts_auto_deps/platform/file.go b/ts_auto_deps/platform/file.go deleted file mode 100644 index 7df93d0..0000000 --- a/ts_auto_deps/platform/file.go +++ /dev/null
@@ -1,53 +0,0 @@ -package platform - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -const ( - filePerms = 0666 -) - -var pathReplacer = strings.NewReplacer("\\", "/") - -// ReadFile reads the contents of name. -func ReadFile(ctx context.Context, name string) ([]byte, error) { - return ioutil.ReadFile(name) -} - -// ReadBytesFromFile reads bytes into the buffer provided, stopping when the -// buffer is full. -func ReadBytesFromFile(ctx context.Context, name string, buffer []byte) (int, error) { - f, err := os.Open(name) - if err != nil { - return 0, err - } - defer f.Close() - - n, err := f.Read(buffer) - return n, err -} - -// WriteFile writes data to filename. -func WriteFile(ctx context.Context, filename string, data []byte) error { - return ioutil.WriteFile(filename, data, filePerms) -} - -// Stat reads the file system information of name. -func Stat(ctx context.Context, name string) (interface{}, error) { - return os.Stat(name) -} - -// Glob returns all paths matching pattern. -func Glob(ctx context.Context, pattern string) ([]string, error) { - return filepath.Glob(pattern) -} - -// Normalize converts Windows path separators into POSIX -func Normalize(path string) string { - return pathReplacer.Replace(path) -}
diff --git a/ts_auto_deps/platform/log.go b/ts_auto_deps/platform/log.go deleted file mode 100644 index 4fb889e..0000000 --- a/ts_auto_deps/platform/log.go +++ /dev/null
@@ -1,22 +0,0 @@ -package platform - -import ( - "fmt" - "log" - "os" -) - -// Infof prints a formatted message to stdout. -func Infof(format string, args ...interface{}) { - fmt.Printf(format+"\n", args...) -} - -// Error prints a series of args to stderr. -func Error(args ...interface{}) { - fmt.Fprintln(os.Stderr, args...) -} - -// Fatalf prints a formatted message to stderr. Panics after printing. -func Fatalf(format string, v ...interface{}) { - log.Fatalf(format, v...) -}
diff --git a/ts_auto_deps/platform/walk.go b/ts_auto_deps/platform/walk.go deleted file mode 100644 index 05dd366..0000000 --- a/ts_auto_deps/platform/walk.go +++ /dev/null
@@ -1,15 +0,0 @@ -package platform - -import ( - "os" - "path/filepath" -) - -func Walk(root string, walkFn func(path string, typ os.FileMode) error) error { - return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - return walkFn(path, info.Mode()) - }) -}
diff --git a/ts_auto_deps/proto/BUILD.bazel b/ts_auto_deps/proto/BUILD.bazel deleted file mode 100644 index e7e793a..0000000 --- a/ts_auto_deps/proto/BUILD.bazel +++ /dev/null
@@ -1,22 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") - -proto_library( - name = "bazel_analyze_proto", - srcs = ["analyze_result.proto"], - visibility = ["//visibility:public"], -) - -go_proto_library( - name = "bazel_analyze_go_proto", - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto", - proto = ":bazel_analyze_proto", - visibility = ["//visibility:public"], -) - -go_library( - name = "go_default_library", - embed = [":bazel_analyze_go_proto"], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto", - visibility = ["//visibility:public"], -)
diff --git a/ts_auto_deps/proto/analyze_result.proto b/ts_auto_deps/proto/analyze_result.proto deleted file mode 100644 index 49ee500..0000000 --- a/ts_auto_deps/proto/analyze_result.proto +++ /dev/null
@@ -1,98 +0,0 @@ -syntax = "proto2"; - -package bazel_analyze; - -// A DependencyGroup represents a list of alternatives for satisfying -// a missing dependency. -message DependencyGroup { - // Represents the visibility status of this dependency group. All the - // dependencies have the same visibility status indicated by the visibility - // field. Producers of this message (i.e. checkdeps) will ensure that - // the invariant is true. - enum Visibility { - // Visibility was not considered. (Default) - UNKNOWN = 0; - // All the alternatives are visible - ALL_VISIBLE = 1; - // All the alternatives are invisible - ALL_INVISIBLE = 2; - } - - repeated string dependency = 1; - optional Visibility visibility = 2 [default = UNKNOWN]; - // The set of import paths for which any of these dependencies can provide. - // It is legal for there to be multiple entries there (in practice it is - // not very common). This happens if the dependencies required by one - // import_path is a subset of the other. In that case, the smaller set of - // dependencies is populated in dependency field, and both import_path's are - // listed here. - repeated string import_path = 3; -} - -// Represents information about arbitrary non-deps attributes. -message AttributeReport { - // Name of the attribute (e.g. "runtime_deps"). - optional string attribute = 1; - // Attribute value that is required but missing. - repeated string missing_value = 2; - // Attribute value that is declared but not needed. - repeated string unnecessary_value = 3; -} - -// The result of checking a single build rule (called X in comments -// below) for missing or unnecessary dependencies. Produced by -// java/com/google/devtools/build/checkbuilddeps (which for external -// users means it's produced by bazel analyze). -message DependencyReport { - required string rule = 1; // Name of rule X, in canonical google3 format. - - // Names of targets that are, or should be, declared as dependencies - // by rule X. Some need to be declared and some don't; in some - // cases it is impossible to tell by static analysis. Each is the - // full canonical name of the target, i.e. //x/y:z, or the shorthand - // form if the target name equals the package name, i.e. //x/y - // (shorthand for //x/y:y). - - // Declared and needed. Removing these dependencies should break the build. - repeated string necessary_dependency = 2; - - // Declared but not needed. Removing these dependencies should not break - // the build. - repeated string unnecessary_dependency = 3; - - // Can't tell. These may be dependencies where no corresponding imports - // were found, but alwayslink=1. - repeated string ambiguous_dependency = 4; - - // Needed but not declared. If there are imports which can be satisfied - // by more than one dependency declared in a best-matching package, list - // all of them. If this list is not empty, we expect the build to break. - repeated DependencyGroup missing_dependency_group = 8; - - // Files/classes that a source file imports, but for which no - // appropriate dependency could be found. We expect the build to break - // if this list is not empty. - // NOTE: If there are unreseolved imports, it may indicate that - // analysis failed to connect a declared dependency with a - // corresponding import statement, and so you may want to have less - // faith in the unnecessary_dependency field (though, in general, - // rules in unnecessary_dependency are still PROBABLY unnecessary). - repeated string unresolved_import = 7; - - // Source files that are inputs to the rule but cannot be found. - // Values are absolute labels, e.g. '//foo:bar/baz.c'. - repeated string missing_source_file = 13; - - // General-purpose feedback messages from the dependency checker - // (errors, warnings, any kind of info). - repeated string feedback = 6; - - // Indicates whether the dependency analysis completed without errors. - optional bool successful = 10 [default = true]; -} - -// Aggregate DependencyReports for multiple analysis -// targets - used to support bazel analyze --analysis_output=proto -message AnalyzeResult { - repeated DependencyReport dependency_report = 1; -}
diff --git a/ts_auto_deps/updater/BUILD.bazel b/ts_auto_deps/updater/BUILD.bazel deleted file mode 100644 index 53e319c..0000000 --- a/ts_auto_deps/updater/BUILD.bazel +++ /dev/null
@@ -1,29 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "go_default_library", - srcs = ["updater.go"], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/updater", - visibility = ["//visibility:public"], - deps = [ - "//ts_auto_deps/analyze:go_default_library", - "//ts_auto_deps/platform:go_default_library", - "//ts_auto_deps/proto:go_default_library", - "//ts_auto_deps/workspace:go_default_library", - "@com_github_bazelbuild_buildtools//build:go_default_library", - "@com_github_bazelbuild_buildtools//edit:go_default_library", - "@com_github_golang_protobuf//proto:go_default_library", - "@com_github_mattn_go_isatty//:go_default_library", - ], -) - -go_test( - name = "go_default_test", - srcs = ["updater_test.go"], - embed = [":go_default_library"], - deps = [ - "//ts_auto_deps/proto:go_default_library", - "@com_github_bazelbuild_buildtools//build:go_default_library", - "@com_github_golang_protobuf//proto:go_default_library", - ], -)
diff --git a/ts_auto_deps/updater/test_register.go b/ts_auto_deps/updater/test_register.go deleted file mode 100644 index 0f51e63..0000000 --- a/ts_auto_deps/updater/test_register.go +++ /dev/null
@@ -1,259 +0,0 @@ -package updater - -import ( - "context" - "fmt" - "path/filepath" - - "github.com/bazelbuild/buildtools/build" - "github.com/bazelbuild/buildtools/edit" - "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. It returns a set of the paths for the packages that were updated. -func (upd *Updater) RegisterTestRules(ctx context.Context, paths ...string) (bool, map[string]bool, error) { - reg := &buildRegistry{make(map[string]*build.File), make(map[*build.File]bool)} - var g3root string - updatedAncestorPackages := make(map[string]bool) - 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, nil, err - } - bld, err := reg.readBUILD(ctx, g3root, buildPath) - if err != nil { - return false, nil, err - } - for _, tr := range getRules(bld, "ts_library", ruleTypeTest) { - // 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()) - ancestorBuild, err := reg.registerTestRule(ctx, bld, tsConfig, g3root, target) - if err != nil { - return false, nil, err - } - if ancestorBuild != "" { - updatedAncestorPackages[ancestorBuild] = true - } - // 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" { - ancestorBuild, err := reg.registerTestRule(ctx, bld, tsDevSrcs, g3root, target) - if err != nil { - return false, nil, err - } - if ancestorBuild != "" { - updatedAncestorPackages[ancestorBuild] = true - } - } - } - } - - 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, nil, err - } - updated = updated || fileChanged - } - - return updated, updatedAncestorPackages, 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 := getWorkspaceRelativePath(workspaceRoot, buildFilePath) - if err != nil { - return nil, err - } - - if bld, ok := reg.bldFiles[normalizedG3Path]; ok { - return bld, nil - } - - bld, err := readBUILD(ctx, buildFilePath, normalizedG3Path) - if err != nil { - return nil, err - } - if bld == nil { - // The BUILD file didn't exist, so create a new, empty one. - bld = &build.File{Path: normalizedG3Path, Type: build.TypeBuild} - } - - 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) (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 filepath.Dir(bld.Path), 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 -} - -var wellKnownBuildRules = []struct { - name string - attrName string -}{ - { - name: "karma_polymer_test", - attrName: "test_ts_deps", - }, - { - name: "wct_closure_test_suite", - attrName: "js_deps", - }, - { - name: "jasmine_node_test", - attrName: "deps", - }, - { - name: "karma_web_test_suite", - attrName: "deps", - }, - { - name: "boq_jswire_test_library", - attrName: "deps", - }, -} - -// isRegisteredWithAlternateTestRule returns true if the rule is already -// registered with a well known test rule, such as karma_polymer_test, -// wct_closure_test_suite or jasmine_node_test. -func isRegisteredWithAlternateTestRule(bld *build.File, r *build.Rule, dep string) bool { - pkg := filepath.Dir(bld.Path) - for _, wkbr := range wellKnownBuildRules { - if isKind(r, wkbr.name) { - testTsDeps := r.Attr(wkbr.attrName) - if edit.ListFind(testTsDeps, dep, pkg) != nil { - return true - } - } - } - return false -}
diff --git a/ts_auto_deps/updater/updater.go b/ts_auto_deps/updater/updater.go deleted file mode 100644 index f606e5b..0000000 --- a/ts_auto_deps/updater/updater.go +++ /dev/null
@@ -1,1340 +0,0 @@ -// Package updater implements the main logic of the ts_auto_deps command. It reads BUILD files, -// discovers TypeScript sources, uses `bazel analyze` to update import/dependency information, -// and then modifies the BUILD file accordingly. -package updater - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "path/filepath" - "regexp" - "sort" - "strconv" - "strings" - "sync" - "time" - - "flag" - "github.com/bazelbuild/buildtools/build" - "github.com/bazelbuild/buildtools/edit" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/analyze" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" - "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace" - "github.com/golang/protobuf/proto" - "github.com/mattn/go-isatty" - - arpb "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto" -) - -var bazelErrorRE = regexp.MustCompile(`ERROR: ([^:]+):(\d+):\d+:`) - -// New creates a new updater from the given arguments. One updater can be used -// to update many packages, repeatedly, but not concurrently. -// bazelAnalyze and updateFile can be passed in to handle ts_auto_deps operation in -// different environments and for fakes in tests. -func New(removeUnusedDeclarations bool, updateComments bool, bazelAnalyze BazelAnalyzer, updateFile UpdateFile) *Updater { - return &Updater{removeUnusedDeclarations, updateComments, bazelAnalyze, updateFile} -} - -// UpdateFile updates the contents of filePath. Implementations may postpone or batch actually writing the given file, i.e. -// a subsequent read may return stale contents. -type UpdateFile func(ctx context.Context, filePath string, contents string) error - -// LocalUpdateFile simply writes to the given file. -func LocalUpdateFile(ctx context.Context, filePath string, contents string) error { - return platform.WriteFile(ctx, filePath, []byte(contents)) -} - -// BazelAnalyzer is a function that executes bazel analyze for the given -// absolute build file path and bazel targets, and returns the raw analysis -// result proto, or an error if the analyze call failed. -// It's used to abstract over execution using rabbit vs local execution. -// Returns the main output (serialized AnalysisResult proto), any error -// messages, or an error. -type BazelAnalyzer func(buildFilePath string, targets []string) ([]byte, []byte, error) - -// Updater encapsulates configuration for the ts_auto_deps process. -type Updater struct { - removeUnusedDeclarations bool - updateComments bool - bazelAnalyze BazelAnalyzer - updateFile UpdateFile -} - -func attrTruthy(r *build.Rule, attr string) bool { - attrVal := r.AttrLiteral(attr) - return attrVal == "True" || attrVal == "1" -} - -// Matches the warning TypeScriptRuleChecker prints for unused ts_declarations. -// TODO(martinprobst): in the long term, this should become the default and TypeScriptRuleChecker should no longer special case ts_declaration. -var unusedDeclarationRE = regexp.MustCompile( - `WARNING: [^:]+:\d+:(?:\d+:)? keeping possibly used ts_declaration '([^']+)'`) - -// GarbledBazelResponseError signals to callers that the proto returned by bazel -// analyze was garbled, and couldn't be unmarshalled. -// TODO(lucassloan): remove when b/112891536 is fixed -// Build Rabbit rewrites paths produced by bazel, which garbles the error -// messages from bazel analyze, since they're encoded in protobufs. -type GarbledBazelResponseError struct { - Message string -} - -func (g *GarbledBazelResponseError) Error() string { - return g.Message -} - -// runBazelAnalyze executes the `bazel analyze` command and extracts reports. -// It returns the dependency report with rule names referring to rules *before* -// macro expansion, or an error. runBazelAnalyze uses the given `analyze` -// function to actually run the `bazel analyze` operation, which allows -// exchanging it for a different implementation in the ts_auto_deps presubmit service. -func (upd *Updater) runBazelAnalyze(buildFilePath string, bld *build.File, rules []*build.Rule) ([]*arpb.DependencyReport, error) { - var targets []string - for _, r := range rules { - fullTarget := AbsoluteBazelTarget(bld, r.Name()) - targets = append(targets, fullTarget) - } - out, stderr, err := upd.bazelAnalyze(buildFilePath, targets) - if err != nil { - return nil, err - } - - var res arpb.AnalyzeResult - if err := proto.Unmarshal(out, &res); err != nil { - // TODO(lucassloan): remove when b/112891536 is fixed - // Build Rabbit rewrites paths produced by bazel, which garbles the error - // messages from bazel analyze, since they're encoded in protobufs. - return nil, &GarbledBazelResponseError{fmt.Sprintf("failed to unmarshal analysis result: %v\nin: %q", err, string(out))} - } - platform.Infof("analyze result %v", res) - reports := res.GetDependencyReport() - if len(targets) != len(reports) { - if len(stderr) > 0 { - // TODO(b/73321854): pretend second rule has a syntactical error, so bazel analyze produces no - // report for it, but also doesn't return an error code. Remove workaround once fixed. - return nil, fmt.Errorf("parsing reports failed (%d reports for %s):\n%s", - len(reports), targets, stderr) - } - return nil, fmt.Errorf("parsing reports failed (%d reports for %s) in output: %s", - len(reports), targets, out) - } - if upd.removeUnusedDeclarations { - for _, report := range reports { - for _, fb := range report.GetFeedback() { - m := unusedDeclarationRE.FindStringSubmatch(fb) - if m == nil { - continue - } - target := m[1] - platform.Infof("Removing (possibly) unused ts_declaration dependency %q", target) - // TODO(martinprobst): this warning is to educate users after changing removeUnusedDeclarations to true by default. - // Once existing code is fixed, this constitutes normal operation, and the logging below should be dropped. - fmt.Fprintf(os.Stderr, "WARNING: removing apparently unused ts_declaration() dependency from %q.\n", report.GetRule()) - - report.UnnecessaryDependency = append(report.UnnecessaryDependency, target) - } - } - } - return reports, nil -} - -func spin(prefix string) chan<- struct{} { - done := make(chan struct{}) - // Check for cygwin, important for Windows compatibility - if !isatty.IsTerminal(os.Stderr.Fd()) || !isatty.IsCygwinTerminal(os.Stdout.Fd()) { - return done - } - go func() { - // Wait a bit before starting to print to avoid flashing a spinner unnecessarily. - time.Sleep(100 * time.Millisecond) - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - i := 0 - str := []rune(`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) - for { - select { - case <-done: - fmt.Fprint(os.Stderr, "\r") - for i := 0; i < len(prefix)+2; i++ { - fmt.Fprint(os.Stderr, " ") - } - fmt.Fprint(os.Stderr, "\r") - return - case <-ticker.C: - fmt.Fprintf(os.Stderr, "\r%s %s", prefix, string(str[i])) - i = (i + 1) % len(str) - } - } - }() - return done -} - -// readBUILD loads the BUILD file, if present, or returns a nil pointer, if not. -// buildFilePath is relative to CWD, and workspaceRelativePath is relative to -// the workspace root (the directory containing the WORKSPACE file). -func readBUILD(ctx context.Context, buildFilePath, workspaceRelativePath string) (*build.File, error) { - data, err := platform.ReadFile(ctx, buildFilePath) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - bld, err := build.ParseBuild(workspaceRelativePath, data) - if err != nil { - if parseErr, ok := err.(build.ParseError); ok { - return nil, &AnalysisFailedError{ - []AnalysisFailureCause{ - AnalysisFailureCause{ - Message: parseErr.Error(), - Path: parseErr.Filename, - Line: parseErr.Pos.Line, - }, - }, - } - - } - - // wasn't an error we know how to parse - return nil, err - } - return bld, nil -} - -type srcSet map[string]bool - -type globResult struct { - srcs srcSet - err error -} - -// globSources finds sources in path with any of the given extensions. -// It also filters out temporary files, dangling symlinks, and symlinks into bazel-bin specifically. -// It returns file names relative to path. -func globSources(ctx context.Context, path string, extensions []string) (srcSet, error) { - var allSourcePaths []string - for _, extension := range extensions { - pattern := "*." + extension - matched, err := platform.Glob(ctx, filepath.Join(path, pattern)) - if err != nil { - return nil, fmt.Errorf("glob(%s): %s", pattern, err) - } - allSourcePaths = append(allSourcePaths, matched...) - } - srcs := make(srcSet) - for _, p := range allSourcePaths { - fileName := filepath.Base(p) - if isTempFile(fileName) { - continue // Ignore editor swap/backup files. - } - // Try platform.Stat as a fallback, for Google file systems. - _, err := platform.Stat(ctx, p) - if os.IsNotExist(err) { - platform.Infof("platform.Glob returned non-existent file (dangling symlink?). Ignoring %q.", p) - continue - } - if err != nil { - return nil, fmt.Errorf("cannot stat platform.Glob result %q: %v", p, err) - } - isMpeg, err := IsMpegTS(ctx, p) - if err != nil { - return nil, err - } - if isMpeg { - continue - } - p, err := filepath.Rel(path, p) - if err != nil { - return nil, fmt.Errorf("filepath.Rel(%s, %s): %v", path, p, err) - } - srcs[p] = true - } - return srcs, nil -} - -// IsMpegTS checks if a ".ts" file is an MPEG transport stream. Taze shouldn't -// treat them as TypeScript files. -func IsMpegTS(ctx context.Context, path string) (bool, error) { - var content [200]byte - n, err := platform.ReadBytesFromFile(ctx, path, content[:]) - if err != nil && err != io.EOF { - return false, err - } - - // MPEG TS' frame format starts with 0x47 every 189 bytes - detect that and return. - isMpeg := n > 188 && content[0] == 0x47 && content[188] == 0x47 - return isMpeg, nil -} - -func isTempFile(fileName string) bool { - return strings.HasPrefix(fileName, ".") || strings.HasSuffix(fileName, ".swp") || - strings.HasSuffix(fileName, "~") -} - -// updateSources adds any srcs that are not in some rule to the last ts_* rule -// in the package, or create a new rule for them. -func updateSources(bld *build.File, srcs srcSet) { - removeSourcesUsed(bld, "ts_library", "srcs", srcs) - removeSourcesUsed(bld, "ts_declaration", "srcs", srcs) - - if len(srcs) == 0 { - return - } - - // Sort the remaining sources for reproducibility (sadly Go has no LinkedHashSet) - var srcSlice []string - for s := range srcs { - srcSlice = append(srcSlice, s) - } - sort.Strings(srcSlice) - - pkgName := filepath.Base(filepath.Dir(bld.Path)) - platform.Infof("Adding new sources to targets in %q: %q", pkgName, srcSlice) - for _, s := range srcSlice { - var r *build.Rule - ruleName := pkgName - if strings.HasSuffix(s, ".d.ts") { - r = getOrCreateRule(bld, ruleName+"_dts", "ts_declaration", ruleTypeRegular) - } else { - rt := determineRuleType(bld.Path, s) - r = getOrCreateRule(bld, ruleName, "ts_library", rt) - } - addToSrcsClobbering(bld, r, s) - } -} - -// Adds the given value to the srcs attribute on the build rule. Clobbers any -// existing values for srcs that are not a list. -func addToSrcsClobbering(bld *build.File, r *build.Rule, s string) { - value := r.Attr("srcs") - switch value.(type) { - case nil, *build.ListExpr: - // expected - a list of files (labels) or absent. - default: - // Remove any glob calls, variables, etc. ts_auto_deps uses explicit source lists. - fmt.Fprintf(os.Stderr, "WARNING: clobbering non-list srcs attribute on %s\n", - AbsoluteBazelTarget(bld, r.Name())) - r.DelAttr("srcs") - } - val := &build.StringExpr{Value: s} - edit.AddValueToListAttribute(r, "srcs", "", val, nil) -} - -var testingRegexp = regexp.MustCompile(`\btesting\b`) - -func determineRuleType(path, s string) ruleType { - if strings.HasSuffix(s, "_test.ts") || strings.HasSuffix(s, "_test.tsx") { - return ruleTypeTest - } - - return ruleTypeRegular -} - -// AnalysisFailedError is returned by ts_auto_deps when the underlying analyze operation -// fails, e.g. because the BUILD files have syntactical errors. -type AnalysisFailedError struct { - Causes []AnalysisFailureCause -} - -// AnalysisFailureCause gives (one of) the reasons analysis failed, along with -// the path and line that caused the failure (if available). -type AnalysisFailureCause struct { - Message string - // workspace path of the file on which analysis failed ie foo/bar/baz.ts, not - // starting with google3/ - Path string - // 1-based line on which analysis failed - Line int -} - -func (a *AnalysisFailedError) Error() string { - var messages []string - for _, c := range a.Causes { - messages = append(messages, c.Message) - } - return strings.Join(messages, "\n") -} - -// updateDeps adds missing dependencies and removes unnecessary dependencies -// for the targets in the given DependencyReports to the build rules in bld. -func updateDeps(bld *build.File, reports []*arpb.DependencyReport) error { - // First, check *all* reports on whether they were successful, so that users - // get the complete set of errors at once. - var errors []AnalysisFailureCause - for _, report := range reports { - if !report.GetSuccessful() { - for _, fb := range report.GetFeedback() { - msg := fmt.Sprintf("dependency analysis failed for %s:\n%s", - report.GetRule(), fb) - - m := bazelErrorRE.FindStringSubmatch(fb) - if m == nil { - // error message didn't contain file and line number, so just use the - // path of the BUILD file that was analyzed - errors = append(errors, AnalysisFailureCause{Message: msg, Path: bld.Path}) - continue - } - - file := m[1] - line, err := strconv.Atoi(m[2]) - if err != nil { - return err - } - - errors = append(errors, AnalysisFailureCause{msg, file, line}) - } - } - } - if len(errors) > 0 { - return &AnalysisFailedError{errors} - } - - pkg := filepath.Dir(bld.Path) - for _, report := range reports { - platform.Infof("Applying report: %s", report.String()) - fullTarget := report.GetRule() - targetName := fullTarget[strings.LastIndex(fullTarget, ":")+1:] - r := edit.FindRuleByName(bld, targetName) - if r == nil { - return fmt.Errorf("could not find rule from report %v", targetName) - } - for _, md := range report.MissingDependencyGroup { - for _, d := range md.Dependency { - d = AbsoluteBazelTarget(bld, d) - if d == fullTarget { - return &AnalysisFailedError{ - []AnalysisFailureCause{ - AnalysisFailureCause{ - Message: fmt.Sprintf("target %s depends on itself. "+ - "Maybe you have an incorrect `// from %s` comment, or need to split application "+ - "entry point (main.ts) and ng_module() rule?", d, d), - Path: bld.Path, - }, - }, - } - } - platform.Infof("Adding dependency on %s to %s\n", d, fullTarget) - addDep(bld, r, d) - } - } - hadUnresolved := len(report.UnresolvedImport) > 0 - if hadUnresolved { - return &AnalysisFailedError{ - []AnalysisFailureCause{ - AnalysisFailureCause{ - Message: fmt.Sprintf("ERROR in %s: unresolved imports %s.\nMaybe you are missing a "+ - "'// from ...'' comment, or the target BUILD files are incorrect?\n%s\n", - fullTarget, report.UnresolvedImport, strings.Join(report.GetFeedback(), "\n")), - Path: bld.Path, - }, - }, - } - } - for _, d := range report.UnnecessaryDependency { - platform.Infof("Removing dependency on %s from %s\n", d, fullTarget) - edit.ListAttributeDelete(r, "deps", d, pkg) - } - for _, s := range report.MissingSourceFile { - platform.Infof("Removing missing source %s from %s\n", s, fullTarget) - edit.ListAttributeDelete(r, "srcs", s, pkg) - } - } - return nil -} - -// maybeWriteBUILD checks if the given file needs updating, i.e. whether the -// canonical serialized form of bld has changed from the file contents on disk. -// If so, writes the file and returns true, returns false otherwise. -func (upd *Updater) maybeWriteBUILD(ctx context.Context, path string, bld *build.File) (bool, error) { - ri := &build.RewriteInfo{} - build.Rewrite(bld, ri) - platform.Infof("Formatted %s: %s\n", path, ri) - newContent := build.Format(bld) - oldContent, err := platform.ReadFile(ctx, path) - if err != nil { - if !os.IsNotExist(err) { - return false, err - } else if len(newContent) == 0 { - // The BUILD file does not exist, and the newly created file has no content. - // Treat this as equivalent, and do not create a new BUILD file. - return false, nil - } - // Fall through to write a new file. - } else if bytes.Equal(oldContent, newContent) { - // Compare contents, only update if changed. - return false, nil - } - if err := upd.updateFile(ctx, path, string(newContent)); err != nil { - return false, fmt.Errorf("failed to update %q: %v", path, err) - } - return true, nil -} - -// getWorkspaceRelativePath takes a buildFilePath that's relative to the working -// directory, and returns a path to the BUILD file that's relative to the -// workspaceRoot (the absolute path of the directory containing the WORKSPACE -// file). -func getWorkspaceRelativePath(workspaceRoot, buildFilePath string) (string, error) { - absPath, err := filepath.Abs(buildFilePath) - if err != nil { - return "", err - } - workspaceRelativePath, err := filepath.Rel(workspaceRoot, absPath) - if err != nil { - return "", err - } - platform.Normalize(workspaceRelativePath) - - return workspaceRelativePath, nil -} - -// getBUILDPath takes in a package or BUILD file path, and returns the path of -// the workspace root (the absolute path of the directory containing the -// WORKSPACE file), the BUILD file path relative to the working directory, and -// the BUILD file path relative to the workspace root. -func getBUILDPath(ctx context.Context, path string) (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 "", "", "", err - } - buildFilePath := filepath.Join(path, "BUILD") - workspaceRoot, err := workspace.Root(buildFilePath) - if err != nil { - return "", "", "", err - } - - workspaceRelativePath, err := getWorkspaceRelativePath(workspaceRoot, buildFilePath) - if err != nil { - return "", "", "", err - } - - return workspaceRoot, buildFilePath, workspaceRelativePath, nil -} - -// isTazeDisabledInPackage checks the BUILD file, or if the BUILD doesn't exist, -// the nearest ancestor BUILD file for a disable_ts_auto_deps() rule. -func isTazeDisabledInPackage(ctx context.Context, g3root, buildFilePath, workspaceRelativePath string, bld *build.File) (bool, error) { - if bld == nil { - // Make sure ts_auto_deps hasn't been disabled in the next closest ancestor package. - ancestor, err := FindBUILDFile(ctx, make(map[string]*build.File), g3root, filepath.Dir(workspaceRelativePath)) - if _, ok := err.(*noAncestorBUILDError); ok { - platform.Infof("Could not find any ancestor BUILD for %q, continuing with a new BUILD file", - buildFilePath) - return false, nil - } else if err != nil { - return false, err - } else if buildHasDisableTaze(ancestor) { - fmt.Printf("ts_auto_deps disabled below %q\n", ancestor.Path) - return true, nil - } else { - platform.Infof("BUILD file missing and ts_auto_deps is enabled below %q. Creating new BUILD file.", - ancestor.Path) - return false, nil - } - } - - if buildHasDisableTaze(bld) { - fmt.Printf("ts_auto_deps disabled on %q\n", buildFilePath) - return true, nil - } - - return false, nil -} - -// 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, workspaceRelativePath string, bld *build.File) (bool, error) { - if bld == nil { - // 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(workspaceRelativePath)) - 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 - } else { - // there was an ancestor BUILD, but it didn't reference subdirectory sources - return false, nil - } - } - - if hasSubdirectorySources(bld) { - return true, nil - } - - return false, nil -} - -func (upd *Updater) addSourcesToBUILD(ctx context.Context, path string, buildFilePath string, bld *build.File, srcs srcSet) (bool, error) { - - platform.Infof("Updating sources") - if len(srcs) == 0 && len(allTSRules(bld)) == 0 { - // No TypeScript rules/sources, no need to update anything - return false, nil - } - updateSources(bld, srcs) - - return upd.maybeWriteBUILD(ctx, buildFilePath, bld) -} - -// updateBUILDAfterBazelAnalyze applies the BUILD file updates that depend on bazel -// analyze's DependencyReports, most notably updating any rules' deps. -func (upd *Updater) updateBUILDAfterBazelAnalyze(ctx context.Context, isRoot bool, - g3root string, buildFilePath string, bld *build.File, reports []*arpb.DependencyReport) (bool, error) { - platform.Infof("Updating deps") - if err := updateDeps(bld, reports); err != nil { - return false, err - } - - platform.Infof("Setting library rule kinds") - if err := setLibraryRuleKinds(ctx, buildFilePath, bld); err != nil { - return false, err - } - return upd.maybeWriteBUILD(ctx, buildFilePath, bld) -} - -// 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) { - g3root, buildFilePath, workspaceRelativePath, err := getBUILDPath(ctx, dir) - if err != nil { - return false, err - } - - bld, err := readBUILD(ctx, buildFilePath, workspaceRelativePath) - if err != nil { - platform.Infof("Error reading building file!") - return false, err - } - - return isTazeDisabledInPackage(ctx, g3root, buildFilePath, workspaceRelativePath, bld) -} - -// CantProgressAfterWriteError reports that ts_auto_deps was run in an environment -// where it can't make writes to the file system (such as when ts_auto_deps is running -// as a service for cider) and the writes it made need to be visible to bazel analyze, -// so it can continue updating the BUILD file(s). In such a case, the caller should -// collect the writes using a custom UpdateFile function, and re-call ts_auto_deps after -// applying the writes. -type CantProgressAfterWriteError struct{} - -func (a *CantProgressAfterWriteError) Error() string { - return "running ts_auto_deps in a non-writable environment, can't continue until writes are applied" -} - -// UpdateBUILDOptions bundles options for the UpdateBUILD function. -type UpdateBUILDOptions struct { - // InNonWritableEnvironment boolean indicates to ts_auto_deps that the writes it makes - // won't be immediately visible to bazel analyze, so it cannot proceed normally. - // In this case, if it makes a write that needs to be visible to bazel analyze, it - // will return a CantProgressAfterWriteError, which indicates that the caller - // should apply the writes made to its UpdateFile function, and re-call UpdateBUILD - // after the writes have been applied. - InNonWritableEnvironment bool - // IsRoot indicates that the directory is a project's root directory, so a tsconfig - // rule should be created. - IsRoot bool -} - -// LatencyReport contains timing measurements of the functions that are called -// when running the presubmit on a package without any TypeScript (since we -// return early to avoid the latency of RAS analyze). -type LatencyReport struct { - GetBUILD, TazeDisabled, SubdirSrcs, AddSrcs time.Duration -} - -// UpdateBUILD drives the main process of creating/updating the BUILD file -// underneath path based on the available sources. Returns true if it modified -// the BUILD file, false if the BUILD file was up to date already. bazelAnalyze -// is used to run the underlying `bazel analyze` process. Returns another -// boolean that's true iff the package doesn't contain any TypeScript (source -// files or BUILD rules). -func (upd *Updater) UpdateBUILD(ctx context.Context, path string, options UpdateBUILDOptions) (bool, *LatencyReport, error) { - latencyReport := &LatencyReport{} - - // asynchronously glob for TS sources in the package, since it can be slow on - // a network file system. - globChan := make(chan globResult) - go func() { - platform.Infof("Globbing TS sources in %s", path) - srcs, err := globSources(ctx, path, []string{"ts", "tsx"}) - globChan <- globResult{srcs, err} - }() - - start := time.Now() - g3root, buildFilePath, workspaceRelativePath, err := getBUILDPath(ctx, path) - if err != nil { - return false, nil, err - } - - bld, err := readBUILD(ctx, buildFilePath, workspaceRelativePath) - if err != nil { - platform.Infof("Error reading building file!") - return false, nil, err - } - latencyReport.GetBUILD = time.Since(start) - - start = time.Now() - ts_auto_depsDisabled, err := isTazeDisabledInPackage(ctx, g3root, buildFilePath, workspaceRelativePath, bld) - if err != nil { - return false, nil, err - } - latencyReport.TazeDisabled = time.Since(start) - if ts_auto_depsDisabled { - return false, nil, nil - } - - start = time.Now() - hasSubdirSrcs, err := directoryOrAncestorHasSubdirectorySources(ctx, g3root, workspaceRelativePath, bld) - latencyReport.SubdirSrcs = time.Since(start) - if err != nil { - return false, nil, err - } - if hasSubdirSrcs { - return false, nil, &SubdirectorySourcesError{} - } - - if bld == nil { - // The BUILD file didn't exist, so create a new, empty one. - bld = &build.File{Path: workspaceRelativePath, Type: build.TypeBuild} - } - - start = time.Now() - globRes := <-globChan - if globRes.err != nil { - return false, nil, globRes.err - } - changed, err := upd.addSourcesToBUILD(ctx, path, buildFilePath, bld, globRes.srcs) - latencyReport.AddSrcs = time.Since(start) - if err != nil { - return false, nil, err - } - if options.InNonWritableEnvironment && changed { - return true, nil, &CantProgressAfterWriteError{} - } - - rules := allTSRules(bld) - if len(rules) == 0 && !options.IsRoot { - // No TypeScript rules, no need to query for dependencies etc, so just exit early. - return changed, latencyReport, nil - } - rulesWithSrcs := []*build.Rule{} - for _, r := range rules { - srcs := r.Attr("srcs") - if srcs != nil { - if l, ok := srcs.(*build.ListExpr); ok && len(l.List) > 0 { - rulesWithSrcs = append(rulesWithSrcs, r) - } - } - } - platform.Infof("analyzing...") - reports, err := upd.runBazelAnalyze(buildFilePath, bld, rulesWithSrcs) - if err != nil { - return false, nil, err - } - - changedAfterBazelAnalyze, err := upd.updateBUILDAfterBazelAnalyze(ctx, options.IsRoot, g3root, buildFilePath, bld, reports) - if err != nil { - return false, nil, err - } - changed = changed || changedAfterBazelAnalyze - if options.InNonWritableEnvironment && changed { - return true, nil, &CantProgressAfterWriteError{} - } - - return changed, nil, nil -} - -// buildHasDisableTaze checks if the BUILD file should be managed using ts_auto_deps. -// Users can disable ts_auto_deps by adding a "disable_ts_auto_deps()" (or "dont_ts_auto_deps_me()") statement. -func buildHasDisableTaze(bld *build.File) bool { - for _, stmt := range bld.Stmt { - if call, ok := stmt.(*build.CallExpr); ok { - if fnName, ok := call.X.(*build.Ident); ok && (fnName.Name == "disable_ts_auto_deps" || fnName.Name == "dont_ts_auto_deps_me") { - return true - } - } - } - return false -} - -// QueryBasedBazelAnalyze uses bazel query to analyze targets. It is available under a flag or -// an environment variable on engineer's workstations. -func QueryBasedBazelAnalyze(buildFilePath string, targets []string) ([]byte, []byte, error) { - root, err := workspace.Root(buildFilePath) - if err != nil { - return nil, nil, err - } - reports, err := analyze.New(analyze.NewQueryBasedTargetLoader(root, "bazel")).Analyze(context.Background(), buildFilePath, targets) - if err != nil { - return nil, nil, err - } - s, err := proto.Marshal(&arpb.AnalyzeResult{ - DependencyReport: reports, - }) - return s, nil, err -} - -type ruleType int - -const ( - ruleTypeAny ruleType = iota - ruleTypeRegular - ruleTypeTest - 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 - // retrieve ng_module and ts_library intermixed, in the order in which they - // appear in the BUILD file. That allows ts_auto_deps to consistently always pick the - // last build rule in the file in case multiple match, regardless of kind. - allRules := bld.Rules("") - var res []*build.Rule - for _, r := range allRules { - if isKind(r, kind) { - res = append(res, r) - } - } - return res -} - -// hasDependency returns whether a build rule contains the specified dependency. -func hasDependency(bld *build.File, r *build.Rule, dep string) bool { - pkg := filepath.Dir(bld.Path) - oldDeps := r.Attr("deps") - if edit.ListFind(oldDeps, dep, pkg) != nil { - return true - } - runtimeDeps := r.Attr("runtime_deps") - return edit.ListFind(runtimeDeps, dep, pkg) != nil -} - -// addDep adds a dependency to the specified build rule -func addDep(bld *build.File, r *build.Rule, dep string) { - pkg := filepath.Dir(bld.Path) - dep = edit.ShortenLabel(dep, pkg) - if dep[0] != '/' && dep[0] != ':' { - dep = ":" + dep // ShortenLabel doesn't add the ':' - } - edit.AddValueToListAttribute(r, "deps", pkg, &build.StringExpr{Value: dep}, nil) -} - -// AbsoluteBazelTarget converts a ruleName to an absolute target string (//foo/bar:bar). -// It interprets ruleName relative to the given build file's package. It -// supports plain names, names starting with colons, absolute paths, and -// absolute paths with shorthand target syntax (i.e. "bar", ":bar", "//foo/bar", -// "//foo/bar:bar"). -func AbsoluteBazelTarget(bld *build.File, ruleName string) string { - if strings.HasPrefix(ruleName, "//") { - // already absolute - if colonIdx := strings.LastIndex(ruleName, ":"); colonIdx == -1 { - // expand shorthand syntax - return ruleName + ":" + ruleName[strings.LastIndex(ruleName, "/")+1:] - } - return ruleName - } - pkg := platform.Normalize(filepath.Dir(bld.Path)) - return fmt.Sprintf("//%s:%s", pkg, strings.TrimPrefix(ruleName, ":")) -} - -// Finds all ts_library and ts_declaration targets in the given BUILD file. -func allTSRules(bld *build.File) []*build.Rule { - var res []*build.Rule - res = append(res, buildRules(bld, "ts_library")...) - res = append(res, buildRules(bld, "ts_declaration")...) - return res -} - -// removeSourcesUsed removes sources used by rules of kind ruleKind in attribute -// attrName from the given set of sources. -func removeSourcesUsed(bld *build.File, ruleKind, attrName string, srcs srcSet) { - for _, rule := range buildRules(bld, ruleKind) { - for s := range srcs { - pkg := filepath.Dir(bld.Path) - // Handles ":foo.ts" references, and concatenated lists [foo.ts] + [bar.ts] - // TODO(martinprobst): What to do about sources that don't seem to exist? - if edit.ListFind(rule.Attr(attrName), s, pkg) != nil { - delete(srcs, s) - } - } - } -} - -const ( - tsSkylarkLabel = "@npm_bazel_typescript//:index.bzl" - ngSkylarkLabel = "@angular//:index.bzl" -) - -func removeUnusedLoad(bld *build.File, kind string) { - if len(bld.Rules(kind)) > 0 { - return // kind is still used somewhere. - } - var stmt []build.Expr - for _, s := range bld.Stmt { - load, ok := s.(*build.LoadStmt) - if !ok { - stmt = append(stmt, s) - continue - } - if len(load.To) == 0 { - // a load statement without actually loaded symbols, skip - continue - } - - var from, to []*build.Ident - for i, ca := range load.To { - if ca.Name != kind { - from = append(from, load.From[i]) - to = append(to, ca) - } - } - load.From = from - load.To = to - if len(to) > 0 { - stmt = append(stmt, load) - continue - } - } - bld.Stmt = stmt -} - -// setLibraryRuleKinds sets the kinds for recognized library rules. That is, it -// determines if a rule should be an ng_module, and sets the -// rule kind if so. It also takes care of having the appropriate load calls. -func setLibraryRuleKinds(ctx context.Context, buildFilePath string, bld *build.File) error { - hasNgModule := false - changed := false - for _, r := range buildRules(bld, "ts_library") { - shouldBeNgModule := false - isNgModule := r.Call.X.(*build.Ident).Name == "ng_module" - if hasAngularDependency(r) { - shouldBeNgModule = true - hasNgModule = true - } - if isNgModule && !shouldBeNgModule { - platform.Infof("Changing rule %s to ts_library()", r.AttrString("name")) - r.Call.X.(*build.Ident).Name = "ts_library" - r.DelAttr("assets") - changed = true - } else if !isNgModule && shouldBeNgModule { - platform.Infof("Changing rule %s to ng_module()", r.AttrString("name")) - r.Call.X.(*build.Ident).Name = "ng_module" - changed = true - } - } - if changed { - bld.Stmt = edit.InsertLoad(bld.Stmt, ngSkylarkLabel, - []string{"ng_module"}, []string{"ng_module"}) - bld.Stmt = edit.InsertLoad(bld.Stmt, tsSkylarkLabel, - []string{"ts_library"}, []string{"ts_library"}) - removeUnusedLoad(bld, "ts_library") - removeUnusedLoad(bld, "ng_module") - } - if !hasNgModule { - return nil - } - return updateWebAssets(ctx, buildFilePath, bld) -} - -// hasAngularDependency returns true if the given rule depends on a Angular -// build rule. -func hasAngularDependency(r *build.Rule) bool { - e := r.Attr("deps") - for _, li := range edit.AllLists(e) { - for _, elem := range li.List { - str, ok := elem.(*build.StringExpr) - if ok && strings.HasPrefix(str.Value, "//third_party/javascript/angular2") { - return true - } - } - } - return false -} - -// updateWebAssets finds web assets in the package of the BUILD file and adds -// them to the "assets" attribute of the ng_module rules. -func updateWebAssets(ctx context.Context, buildFilePath string, bld *build.File) error { - // TODO(martinprobst): should this be merged with updateSources above? Difference is that - // creates new rules, this just distributes assets across them. - // This must use buildFilePath, the absolute path to the directory, as our cwd - // might not be the workspace root. - absolutePkgPath := filepath.Dir(buildFilePath) - assetFiles, err := globSources(ctx, absolutePkgPath, []string{"html", "css"}) - if err != nil { - return err - } - platform.Infof("Found asset files in %s: %v", absolutePkgPath, assetFiles) - - pkg := filepath.Dir(bld.Path) - for _, r := range bld.Rules("ng_module") { - srcs := r.Attr("assets") - if call, ok := srcs.(*build.CallExpr); ok && call.X.(*build.Ident).Name == "glob" { - // Remove any glob calls, ts_auto_deps uses explicit source lists. - r.DelAttr("assets") - } - - for _, s := range r.AttrStrings("assets") { - if strings.HasPrefix(s, ":") || strings.HasPrefix(s, "//") { - continue // keep rule references - } - if _, ok := assetFiles[s]; !ok { - edit.ListAttributeDelete(r, "assets", s, pkg) - } - } - } - - removeSourcesUsed(bld, "ng_module", "assets", assetFiles) - if len(assetFiles) == 0 { - return nil - } - - // Add to the last rule, to match behaviour with *.ts sources. - lastModule := getLastRule(bld, "ng_module", ruleTypeRegular) - if lastModule == nil { - // Fall back to using any ng_module - lastModule = getLastRule(bld, "ng_module", ruleTypeAny) - } - if lastModule == nil { - // Should not happen by preconditions of this function. - return fmt.Errorf("no ng_module rules in BUILD?") - } - - for newAsset := range assetFiles { - val := &build.StringExpr{Value: newAsset} - edit.AddValueToListAttribute(lastModule, "assets", pkg, val, nil) - } - return nil -} - -// getOrCreateRule returns or creates a rule of the given kind, with testonly = 1 or 0 depending on -// rt. If there's no such rule, it creates a new rule with the given ruleName. -// If there is more than one rule matching, it returns the *last* rule. -func getOrCreateRule(bld *build.File, ruleName, ruleKind string, rt ruleType) *build.Rule { - if r := getLastRule(bld, ruleKind, rt); r != nil { - return r - } - - // TODO(calebegg): Switch this to "_test" but still support "_tests" - if rt == ruleTypeTest { - ruleName += "_tests" - } - - loadArgs := []string{ruleKind} - bld.Stmt = edit.InsertLoad(bld.Stmt, tsSkylarkLabel, loadArgs, loadArgs) - - r := &build.Rule{&build.CallExpr{X: &build.Ident{Name: ruleKind}}, ""} - // Rename to *_ts if there's a name collision. This leaves open a collision with another rule - // called _ts, but that failure mode is unlikely to happen accidentally. - if edit.FindRuleByName(bld, ruleName) != nil { - ruleName = ruleName + "_ts" - } else if filepath.Base(filepath.Dir(bld.Path)) == ruleName { - // The various *_ajd macros do not have a "name" attribute, but implicitly use the package name. - // Make sure not to use the package name if there is a *_ajd rule. - for _, r := range bld.Rules("") { - if r.Name() == "" && strings.HasSuffix(r.Kind(), "_ajd") { - ruleName = ruleName + "_ts" - break - } - } - } - r.SetAttr("name", &build.StringExpr{Value: ruleName}) - if rt == ruleTypeTest || rt == ruleTypeTestSupport { - r.SetAttr("testonly", &build.Ident{Name: "True"}) - } - bld.Stmt = append(bld.Stmt, r.Call) - return r -} - -// 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. - if rt == ruleTypeAny { - return true - } - if attrTruthy(r, "testonly") { - if inTestingDir && ((hasTestsName && rt == ruleTypeTest) || (!hasTestsName && rt == ruleTypeTestSupport)) { - return true - } - if !inTestingDir && rt == ruleTypeTest { - return true - } - } - return rt == ruleTypeRegular && !attrTruthy(r, "testonly") -} - -// 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 { - for _, r := range bld.Rules("") { - if ruleMatches(bld, r, ruleKind, rt) && hasDependency(bld, r, target) { - return true - } - } - return false -} - -// getRule returns the last rule in bld that has the given ruleKind and matches -// the specified rt value. -func getLastRule(bld *build.File, ruleKind string, rt ruleType) *build.Rule { - rules := getRules(bld, ruleKind, rt) - - if len(rules) == 0 { - return nil - } - - return rules[len(rules)-1] -} - -// getRules returns all the rules in bld that have the given ruleKind and -// matches the specified rt value. -func getRules(bld *build.File, ruleKind string, rt ruleType) []*build.Rule { - var rules []*build.Rule - for _, r := range bld.Rules("") { - if ruleMatches(bld, r, ruleKind, rt) { - rules = append(rules, r) - } - } - - return rules -} - -// FilterPaths filters the given paths, returning the deduplicated set of -// folders that contain TypeScript sources (.ts and .tsx) or BUILD files. -func FilterPaths(paths []string) []string { - fileSet := make(map[string]bool) - for _, p := range paths { - if !strings.HasSuffix(p, ".ts") && !strings.HasSuffix(p, ".tsx") && filepath.Base(p) != "BUILD" { - continue - } - fileSet[filepath.Dir(p)] = true - } - var newPaths []string - for k := range fileSet { - newPaths = append(newPaths, platform.Normalize(k)) - } - return newPaths -} - -// ResolvePackages resolves package paths, i.e. paths starting with '//', -// against the workspace root folder closest to the current working directory. -// It updates paths in place. -// It returns an error if it cannot find a workspace root or working directory. -func ResolvePackages(paths []string) error { - for i, p := range paths { - if strings.HasPrefix(p, "//") { - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %v", err) - } - g3root, err := workspace.Root(wd) - if err != nil { - return fmt.Errorf("failed to find workspace root under %q: %v", wd, err) - } - paths[i] = filepath.Join(g3root, p) - } - } - 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, &noAncestorBUILDError{} - } - if bld, ok := pkgToBUILD[packagePath]; ok { - return bld, nil - } - buildPath := filepath.Join(workspaceRoot, packagePath, "BUILD") - bld, err := readBUILD(ctx, buildPath, filepath.Join(packagePath, "BUILD")) - if err != nil { - return nil, err - } else if bld == nil { - // Recursively search parent package and cache its location below if found. - bld, err = FindBUILDFile(ctx, pkgToBUILD, workspaceRoot, filepath.Dir(packagePath)) - } - if err == nil { - // NB: The cache key is packagePath ('foo/bar/baz'), even if build file was - // found at a higher location ('foo/BUILD'). This avoids re-testing for file - // existence. - pkgToBUILD[packagePath] = bld - } - return bld, err -} - -// Paths gets the list of paths for the current execution of ts_auto_deps. -func Paths(isRoot bool, files bool, recursive bool) ([]string, error) { - paths := flag.Args() - if len(paths) == 0 { - wd, err := os.Getwd() - if err != nil { - return nil, fmt.Errorf("failed to get working directory: %v", err) - } - paths = []string{wd} - } - - if len(paths) > 1 && isRoot { - return nil, fmt.Errorf("can only take exactly one path with -root") - } - - if files { - paths = FilterPaths(paths) - if len(paths) == 0 { - return nil, fmt.Errorf("WARNING: found no TypeScript files in %s", paths) - } - } - - if err := ResolvePackages(paths); err != nil { - return nil, fmt.Errorf("failed to resolve packages: %s", err) - } - - if recursive { - var lock sync.Mutex // guards allPaths - var allPaths []string - for _, p := range paths { - err := platform.Walk(p, func(path string, info os.FileMode) error { - if info.IsDir() { - lock.Lock() - allPaths = append(allPaths, path) - lock.Unlock() - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("ts_auto_deps -recursive failed: %s", err) - } - } - sort.Sort(byLengthInverted(allPaths)) - paths = allPaths - } - - return paths, nil -} - -// Execute runs ts_auto_deps on paths using host. -func Execute(host *Updater, paths []string, isRoot, recursive bool) error { - ctx := context.Background() - for i, p := range paths { - isLastAndRoot := isRoot && i == len(paths)-1 - changed, _, err := host.UpdateBUILD(ctx, p, UpdateBUILDOptions{InNonWritableEnvironment: false, IsRoot: isLastAndRoot}) - if err != nil { - if recursive { - return fmt.Errorf("ts_auto_deps failed on %s/BUILD: %s", p, err) - } - return fmt.Errorf("ts_auto_deps failed: %s", err) - } - if changed { - if filepath.Base(p) == "BUILD" { - fmt.Printf("Wrote %s\n", p) - } else { - fmt.Printf("Wrote %s\n", filepath.Join(p, "BUILD")) - } - } - } - host.RegisterTestRules(ctx, paths...) - return nil -} - -// allPaths walks the file system and returns a list of all directories under -// all paths. -func allPaths(paths []string) ([]string, error) { - var allPaths []string - for _, p := range paths { - err := filepath.Walk(p, func(path string, info os.FileInfo, err error) error { - if err == nil && info.IsDir() { - allPaths = append(allPaths, path) - } - return err - }) - if err != nil { - return nil, err - } - } - sort.Sort(byLengthInverted(allPaths)) - return allPaths, nil -} - -type byLengthInverted []string - -func (s byLengthInverted) Len() int { return len(s) } -func (s byLengthInverted) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s byLengthInverted) Less(i, j int) bool { return len(s[i]) > len(s[j]) }
diff --git a/ts_auto_deps/updater/updater_test.go b/ts_auto_deps/updater/updater_test.go deleted file mode 100644 index 613b99f..0000000 --- a/ts_auto_deps/updater/updater_test.go +++ /dev/null
@@ -1,563 +0,0 @@ -package updater - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/bazelbuild/buildtools/build" - "github.com/golang/protobuf/proto" - - arpb "github.com/bazelbuild/rules_typescript/ts_auto_deps/proto" -) - -var ( - testTmpDir = os.Getenv("TEST_TMPDIR") -) - -func mktmp(fn string, content []byte) (string, error) { - p := fn - if !filepath.IsAbs(p) { - p = filepath.Join(testTmpDir, fn) - } - if err := os.MkdirAll(filepath.Dir(p), 0777); err != nil { - return "", err - } - return p, ioutil.WriteFile(p, content, 0666) -} - -func TestReadBuild(t *testing.T) { - p, err := mktmp("google3/foo/bar/BUILD", []byte(` -ts_library(name = 'a', srcs = ['b.ts']) -`)) - if err != nil { - t.Fatal(err) - } - bld, err := readBUILD(context.Background(), p, "foo/bar/BUILD") - if err != nil { - t.Fatal(err) - } - if bld.Path != "foo/bar/BUILD" { - t.Errorf("bld.Path: got %s, expected %s", bld.Path, "foo/bar/BUILD") - } -} - -func TestGlobSources(t *testing.T) { - for _, f := range []string{"a.ts", "a/b.ts", "c.tsx", "whatever", "foo.cpp", "d.d.ts", "._e.ts"} { - if _, err := mktmp(f, []byte("// content")); err != nil { - t.Fatal(err) - } - } - if err := os.Symlink("../bazel-bin/symlink.d.ts", filepath.Join(testTmpDir, "symlink.d.ts")); err != nil { - t.Fatal(err) - } - if err := os.Symlink("whatever", filepath.Join(testTmpDir, "whatever.d.ts")); err != nil { - t.Fatal(err) - } - srcs, err := globSources(context.Background(), testTmpDir, []string{"ts", "tsx"}) - if err != nil { - t.Fatal(err) - } - expected := srcSet(map[string]bool{ - "a.ts": true, - "c.tsx": true, - "d.d.ts": true, - "whatever.d.ts": true, - }) - if !reflect.DeepEqual(srcs, expected) { - t.Errorf("globSources: got %v, want %v", srcs, expected) - } -} - -func TestDetermineRuleType(t *testing.T) { - tests := []struct { - path string - source string - rt ruleType - }{ - {"java/com/google/myapp/BUILD", "foo.ts", ruleTypeRegular}, - {"java/com/google/myapp/BUILD", "foo_test.ts", ruleTypeTest}, - {"java/com/google/myapp/BUILD", "foo_test.tsx", ruleTypeTest}, - - {"java/com/google/testing/mytesttool/BUILD", "foo.ts", ruleTypeRegular}, - {"testing/mytesttool/BUILD", "foo.ts", ruleTypeRegular}, - {"testing/mytesttool/BUILD", "foo_test.ts", ruleTypeTest}, - {"testing/mytesttool/BUILD", "foo_test.ts", ruleTypeTest}, - } - for _, tst := range tests { - rt := determineRuleType(tst.path, tst.source) - if rt != tst.rt { - t.Errorf("determineRuleType(%q, %q): got %v, expected %v", tst.path, tst.source, rt, tst.rt) - } - } -} - -func parseReport(t *testing.T, input string) *arpb.DependencyReport { - report := &arpb.DependencyReport{} - if err := proto.UnmarshalText(input, report); err != nil { - t.Error(err) - } - return report -} - -func TestBazelAnalyzeError(t *testing.T) { - bld, err := build.ParseBuild("rules/BUILD", []byte(` -ts_library( - name = "firstrule", - srcs = [], -) -ts_library( - name = "secondrule", - srcs = [], -) - `)) - if err != nil { - t.Fatal(err) - } - mockAnalyze := func(_ string, targets []string) ([]byte, []byte, error) { - data, err := proto.Marshal(&arpb.AnalyzeResult{ - DependencyReport: []*arpb.DependencyReport{&arpb.DependencyReport{ - Rule: proto.String("//rules:firstrule"), - }}, - }) - return data, []byte(`Here's the actual error`), err - } - upd := &Updater{} - upd.bazelAnalyze = mockAnalyze - report, err := upd.runBazelAnalyze("firstrule/BUILD", bld, bld.Rules("ts_library")) - if err == nil { - t.Fatalf("expected an error, got a report: %v", report) - } - expected := `parsing reports failed (1 reports for [//rules:firstrule //rules:secondrule]): -Here's the actual error` - if err.Error() != expected { - t.Errorf("runBazelAnalyze: got %q, expected %q", err.Error(), expected) - } -} - -func TestUpdateDeps(t *testing.T) { - report := parseReport(t, ` - rule: "//foo:bar" - unnecessary_dependency: "//unnecessary_dep" - missing_dependency_group: { - dependency: "//missing_dep" - } - missing_source_file: "missing_file.ts"`) - - tests := []struct { - name string - before string - after string - changed bool - }{ - { - "Add missing dependency", - `ts_library( - name = "bar", - deps = [], - )`, - `ts_library( - name = "bar", - deps = ["//missing_dep"], - )`, - true, - }, - { - "Remove + Add dependency", - `ts_library( - name = "bar", - deps = ["//unnecessary_dep"], - )`, - `ts_library( - name = "bar", - deps = ["//missing_dep"], - )`, - true, - }, - { - "Remove nonexistent dep (e.g. due to macro)", - `ts_library( - name = "bar", - deps = ["//missing_dep"], - )`, - `ts_library( - name = "bar", - deps = ["//missing_dep"], - )`, - false, // Unchanged! - }, - { - "Remove nonexistent src", - `ts_library( - name = "bar", - srcs = ["hello.ts"], - deps = ["//missing_dep"], - )`, - `ts_library( - name = "bar", - srcs = ["hello.ts"], - deps = ["//missing_dep"], - )`, - false, // Unchanged! - }, - } - for _, tst := range tests { - bld, err := build.ParseBuild("foo/BUILD", []byte(tst.before)) - if err != nil { - t.Errorf("parse %s failed: %s in %s", tst.name, err, tst.before) - } - bldAft, err := build.ParseBuild("foo/BUILD", []byte(tst.after)) - if err != nil { - t.Errorf("parse %s after failed: %s", tst.name, err) - } - if err := updateDeps(bld, []*arpb.DependencyReport{report}); err != nil { - t.Errorf("update %s failed: %s", tst.name, err) - } - updated := string(build.Format(bld)) - after := string(build.Format(bldAft)) - if updated != after { - t.Errorf("update(%s), got:\n%s\n\nexpected:\n%s", tst.name, updated, after) - } - } -} - -func TestUnresolvedImportError(t *testing.T) { - report := parseReport(t, ` - rule: "//foo:bar" - unresolved_import: "unresolved/import"`) - - bld, err := build.ParseBuild("foo/BUILD", []byte(`ts_library( - name = "bar", - srcs = ["hello.ts"], - )`)) - if err != nil { - t.Fatal(err) - } - - expectedErr := "'// from ...'' comment, or the target BUILD files are incorrect?" - - err = updateDeps(bld, []*arpb.DependencyReport{report}) - if !strings.Contains(err.Error(), expectedErr) { - t.Errorf("returned error %s: expected it to contain %s", err, expectedErr) - } -} - -func TestDottedCall(t *testing.T) { - // Repro for a crash, b/35389044 - buildText := `foo.bar("baz")` - bld, err := build.ParseBuild("test", []byte(buildText)) - if err != nil { - t.Error(err) - } - removeUnusedLoad(bld, "ignored") -} - -func TestFilterPaths(t *testing.T) { - tests := []struct { - in []string - expected []string - }{ - {[]string{"foo/bar.txt", "foo/baz.ts"}, []string{"foo"}}, - {[]string{"bam.ts"}, []string{"."}}, - {[]string{"foo/BUILD"}, []string{"foo"}}, - {[]string{"r/foo.tsx"}, []string{"r"}}, - {[]string{"../../x.ts"}, []string{"../.."}}, - {[]string{"a.txt", "foo/b.txt"}, []string(nil)}, - } - for _, tst := range tests { - res := FilterPaths(tst.in) - if !reflect.DeepEqual(res, tst.expected) { - t.Errorf("FilterPaths(%v): got %v, expected %v", tst.in, res, tst.expected) - } - } -} - -func TestAddDep(t *testing.T) { - tests := []struct { - buildFile string - newDep string - expected string - }{ - {`ts_library(name = "lib", deps = ["//a", "//b", "//c"])`, - "//b", - `ts_library(name = "lib", deps = ["//a", "//b", "//c"])`}, - {`ts_library(name = "lib", deps = ["//a", "//b", "//c"])`, - "//d", - `ts_library(name = "lib", deps = ["//a", "//b", "//c", "//d"])`}, - {`ts_library(name = "lib", deps = ["//a", ":b", "//c"])`, - ":b", - `ts_library(name = "lib", deps = ["//a", ":b", "//c"])`}, - {`ts_library(name = "lib", deps = ["//a", ":b", "//c"])`, - "//buildloc:b", - `ts_library(name = "lib", deps = ["//a", ":b", "//c"])`}, - {`ts_library(name = "lib", deps = ["//a", "//buildloc:b", "//c"])`, - ":b", - `ts_library(name = "lib", deps = ["//a", "//buildloc:b", "//c"])`}, - {`ts_library(name = "lib", deps = ["//a", "//other:b", "//c"])`, - ":b", - `ts_library(name = "lib", deps = [":b", "//a", "//other:b", "//c"])`}, - {`ts_library(name = "lib", deps = ["//a", "//other:b", "//c"])`, - "//a:a", - `ts_library(name = "lib", deps = ["//a", "//other:b", "//c"])`}, - } - for _, tst := range tests { - bld, err := build.ParseBuild("buildloc/BUILD", []byte(tst.buildFile)) - if err != nil { - t.Fatalf("parse failure: %s - %v", tst.buildFile, err) - } - addDep(bld, bld.Rules("ts_library")[0], tst.newDep) - newContent := string(build.Format(bld)) - expectedBld, err := build.ParseBuild("buildloc/BUILD", []byte(tst.expected)) - if err != nil { - t.Fatalf("parse failure: %s - %v", tst.expected, err) - } - expected := string(build.Format(expectedBld)) - if newContent != expected { - t.Errorf("addDep(%s, %s): got %v, expected %v", tst.buildFile, tst.newDep, newContent, tst.expected) - } - } -} - -func TestRemoveSourcesUsed(t *testing.T) { - tests := []struct { - name string - buildFile string - ruleKind string - attrName string - srcs srcSet - expectedSrcs srcSet - }{ - { - name: "RemovesSources", - buildFile: `ts_library(name = "lib", srcs = ["foo.ts", "bar.ts"])`, - ruleKind: "ts_library", - attrName: "srcs", - srcs: map[string]bool{"foo.ts": true}, - expectedSrcs: map[string]bool{}, - }, - { - name: "WrongRuleKind", - buildFile: `ts_library(name = "lib", srcs = ["foo.ts", "bar.ts"])`, - ruleKind: "ng_module", - attrName: "srcs", - srcs: map[string]bool{"foo.ts": true}, - expectedSrcs: map[string]bool{"foo.ts": true}, - }, - { - name: "WrongAttrName", - buildFile: `ts_library(name = "lib", srcs = ["foo.ts", "bar.ts"])`, - ruleKind: "ts_library", - attrName: "deps", - srcs: map[string]bool{"foo.ts": true}, - expectedSrcs: map[string]bool{"foo.ts": true}, - }, - { - name: "MultipleRules", - buildFile: `ts_library(name = "lib", srcs = ["foo.ts"]) - ts_library(name = "lib2", srcs = ["bar.ts"])`, - ruleKind: "ts_library", - attrName: "srcs", - srcs: map[string]bool{"foo.ts": true, "bar.ts": true}, - expectedSrcs: map[string]bool{}, - }, - { - name: "ConcatenatedLists", - buildFile: `ts_library(name = "lib", srcs = ["foo.ts"] + ["bar.ts"])`, - ruleKind: "ts_library", - attrName: "srcs", - srcs: map[string]bool{"foo.ts": true, "bar.ts": true}, - expectedSrcs: map[string]bool{}, - }, - { - name: "ColonReferences", - buildFile: `ts_library(name = "lib", srcs = [":foo.ts", "bar.ts"])`, - ruleKind: "ts_library", - attrName: "srcs", - srcs: map[string]bool{"foo.ts": true}, - expectedSrcs: map[string]bool{}, - }, - } - 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) - } - - removeSourcesUsed(bld, test.ruleKind, test.attrName, test.srcs) - if !reflect.DeepEqual(test.srcs, test.expectedSrcs) { - t.Errorf("expected removeSourcesUsed() = %v, expected %v", test.srcs, test.expectedSrcs) - } - }) - } -} - -func TestUpdateWebAssets(t *testing.T) { - ctx := context.Background() - bld, err := build.ParseBuild("foo/bar/BUILD", - []byte(`ng_module(name = "m", assets = [":rule", "gone.html"])`)) - if err != nil { - t.Fatalf("parse failure: %v", err) - } - testHTML, err := mktmp("google3/foo/bar/test.html", []byte(`<p>test</p>`)) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := os.Remove(testHTML); err != nil { - t.Error(err) - } - }() - testCSS, err := mktmp("google3/foo/bar/test.css", []byte(`.test {}`)) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := os.Remove(testCSS); err != nil { - t.Error(err) - } - }() - absoluteBuildPath := filepath.Join(filepath.Dir(testCSS), "BUILD") - if err := updateWebAssets(ctx, absoluteBuildPath, bld); err != nil { - t.Fatal(err) - } - data := string(build.Format(bld)) - expected := `ng_module( - name = "m", - assets = [ - ":rule", - "test.css", - "test.html", - ], -) -` - if data != expected { - t.Errorf("build file mismatch, got %s, expected %s", data, expected) - } -} - -func TestWebAssetReferredByColon(t *testing.T) { - ctx := context.Background() - bld, err := build.ParseBuild("foo/bar/BUILD", - []byte(`ng_module(name = "m", assets = [":colon.html"])`)) - if err != nil { - t.Fatalf("parse failure: %v", err) - } - colon, err := mktmp("google3/foo/bar/colon.html", []byte(`<p>test</p>`)) - if err != nil { - t.Fatal(err) - } - defer func() { - if err := os.Remove(colon); err != nil { - t.Error(err) - } - }() - absolutBuildPath := filepath.Join(filepath.Dir(colon), "BUILD") - if err := updateWebAssets(ctx, absolutBuildPath, bld); err != nil { - t.Error(err) - } - data := string(build.Format(bld)) - expected := `ng_module( - name = "m", - assets = [":colon.html"], -) -` - if data != expected { - t.Errorf("build file mismatch, got %s, expected %s", data, expected) - } -} - -func TestAbsoluteBazelTarget(t *testing.T) { - bld := &build.File{Path: "foo/bar/BUILD", Type: build.TypeBuild} - tests := []struct{ target, expected string }{ - {"//foo/bar:bar", "//foo/bar:bar"}, - {":bar", "//foo/bar:bar"}, - {"bar", "//foo/bar:bar"}, - {"//foo/bar", "//foo/bar:bar"}, - } - for _, tst := range tests { - abs := AbsoluteBazelTarget(bld, tst.target) - if abs != tst.expected { - t.Errorf("AbsoluteBazelTarget(%q): got %q, expected %q", tst.target, abs, tst.expected) - } - } -} - -func TestFindBUILDFileCacheOnError(t *testing.T) { - ctx := context.Background() - cache := make(map[string]*build.File) - p, err := mktmp("google3/pkg/file", []byte("")) - if err != nil { - t.Fatal(err) - } - g3root := filepath.Dir(filepath.Dir(p)) - if filepath.Base(g3root) != "google3" { - t.Errorf("g3root should be called google3, got %q", g3root) - } - // No BUILD file was created in the file system so FindBUILDFile should - // return an error. - if _, err = FindBUILDFile(ctx, cache, g3root, "pkg"); err == nil { - t.Fatalf("returned no error, expected some error to occur") - } - if _, ok := cache["pkg"]; ok { - 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) - } - }) - } -}
diff --git a/ts_auto_deps/workspace/BUILD.bazel b/ts_auto_deps/workspace/BUILD.bazel deleted file mode 100644 index 38f8123..0000000 --- a/ts_auto_deps/workspace/BUILD.bazel +++ /dev/null
@@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = ["workspace.go"], - importpath = "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace", - visibility = ["//visibility:public"], - deps = ["@com_github_bazelbuild_buildtools//wspace:go_default_library"], -)
diff --git a/ts_auto_deps/workspace/workspace.go b/ts_auto_deps/workspace/workspace.go deleted file mode 100644 index c7f1ac2..0000000 --- a/ts_auto_deps/workspace/workspace.go +++ /dev/null
@@ -1,13 +0,0 @@ -package workspace - -import "github.com/bazelbuild/buildtools/wspace" - -// Root finds the closest directory containing a WORKSPACE file from p. -func Root(p string) (string, error) { - return wspace.Find(p) -} - -// Name returns the name of the workspace. -func Name() string { - return "TODO" -}