Move QueryBasedTargetLoader to a separate file

PiperOrigin-RevId: 207270934
diff --git a/ts_auto_deps/analyze/loader.go b/ts_auto_deps/analyze/loader.go
new file mode 100644
index 0000000..e3a70cb
--- /dev/null
+++ b/ts_auto_deps/analyze/loader.go
@@ -0,0 +1,282 @@
+package analyze
+
+import (
+	"bytes"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"github.com/bazelbuild/buildtools/edit"
+	"github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace"
+	"github.com/golang/protobuf/proto"
+
+	appb "github.com/bazelbuild/buildtools/build_proto"
+)
+
+var (
+	commonModuleLocations = []string{}
+
+	ts_auto_depsManagedRuleClasses = []string{
+		"ts_library",
+		"ts_declaration",
+		"ng_module",
+		"js_library",
+	}
+)
+
+// QueryBasedTargetLoader uses Bazel query to load targets from BUILD files.
+type QueryBasedTargetLoader struct {
+	workdir     string
+	bazelBinary string
+}
+
+// NewQueryBasedTargetLoader constructs a new QueryBasedTargetLoader rooted
+// in workdir.
+func NewQueryBasedTargetLoader(workdir, bazelBinary string) *QueryBasedTargetLoader {
+	return &QueryBasedTargetLoader{
+		workdir:     workdir,
+		bazelBinary: bazelBinary,
+	}
+}
+
+// LoadLabels uses Bazel query to load targets associated with labels from BUILD
+// files.
+func (q *QueryBasedTargetLoader) LoadLabels(labels []string) (map[string]*appb.Rule, error) {
+	// Ensure the labels are unique to minimize the total number of targets that
+	// need to be loaded.
+	r, err := q.batchQuery(dedupeLabels(labels))
+	if err != nil {
+		return nil, err
+	}
+	labelToRule := make(map[string]*appb.Rule)
+	for _, target := range r.GetTarget() {
+		label, err := q.ruleLabel(target)
+		if err != nil {
+			return nil, err
+		}
+		labelToRule[label] = target.GetRule()
+	}
+	return labelToRule, nil
+}
+
+// LoadImportPaths uses Bazel Query to load targets associated with import
+// paths from BUILD files.
+func (q *QueryBasedTargetLoader) LoadImportPaths(_ string, paths []string) (map[string]*appb.Rule, error) {
+	var remainingImportPaths []string
+	results := make(map[string]*appb.Rule)
+	for _, path := range paths {
+		if trim := strings.TrimPrefix(path, workspace.Name()+"/"); trim != path {
+			// TODO(jdhamlik): Optimize by grouping the queries into one larger query.
+			// TODO(jdhamlik): Handle .d.ts and .tsx files.
+			r, err := q.query(trim + ".ts")
+			if err != nil {
+				return nil, err
+			}
+			targets := r.GetTarget()
+			// Expecting to get one, and only one, target per query.
+			n := len(targets)
+			if n < 1 {
+				return nil, fmt.Errorf("failed to resolved a target for file %q", trim+".ts")
+			}
+			if n > 1 {
+				return nil, fmt.Errorf("got %d targets when only one was expected", n)
+			}
+			t, err := q.loadRuleIncludingFile(targets[0].GetSourceFile().GetName())
+			if err != nil {
+				return nil, err
+			}
+			results[path] = t
+		} else if trim := strings.TrimPrefix(path, "goog:"); trim != path {
+			// There are no current heuristics in this implementation for
+			// 'goog:' imports. That is handled by the ts_auto_deps binary proper.
+			results[path] = nil
+		} else {
+			// The import is not explicitly under google3 or a closure-style
+			// import.
+			remainingImportPaths = append(remainingImportPaths, path)
+		}
+	}
+	var potentialCommonImports []string
+	// Attempt to locate the file rooted in the workspace even though it isn't
+	// prefixed by 'google3/'.
+	for _, imp := range remainingImportPaths {
+		// 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
+		path := strings.TrimSuffix(strings.TrimSuffix(imp, ".ngsummary"), ".ngfactory")
+		res, err := q.batchQuery(pathWithExtensions(path))
+		if err != nil {
+			return nil, err
+		}
+		if len(res.GetTarget()) > 0 {
+			target := res.GetTarget()[0]
+			label, err := q.sourceFileLabel(target)
+			if err != nil {
+				return nil, err
+			}
+			rule, err := q.loadRuleIncludingFile(label)
+			if err != nil {
+				return nil, err
+			}
+			results[imp] = rule
+			continue
+		}
+		potentialCommonImports = append(potentialCommonImports, imp)
+	}
+	if err := q.resolveImportsInCommonLocations(results, potentialCommonImports); err != nil {
+		return nil, err
+	}
+	return results, nil
+}
+
+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
+}
+
+func (q *QueryBasedTargetLoader) sourceFileLabel(target *appb.Target) (string, error) {
+	if t := target.GetType(); t != appb.Target_SOURCE_FILE {
+		return "", fmt.Errorf("target contains object of type %q instead of type %q", t, appb.Target_SOURCE_FILE)
+	}
+	return target.GetSourceFile().GetName(), nil
+}
+
+// loadRuleIncludingFile loads the target associated with a file label.
+func (q *QueryBasedTargetLoader) loadRuleIncludingFile(fileLabel string) (*appb.Rule, error) {
+	_, pkg, file := edit.ParseLabel(fileLabel)
+	// Filter the targets in the file label's package to only targets which
+	// include the file in their 'srcs' attribute.
+	r, err := q.query(fmt.Sprintf("attr('srcs', %s, //%s:*)", file, pkg))
+	if err != nil {
+		return nil, err
+	}
+	for _, target := range r.GetTarget() {
+		rule := target.GetRule()
+		for _, src := range listAttribute(rule, "srcs") {
+			_, _, path := edit.ParseLabel(src)
+			// Return the first rule which has a source file exactly matching
+			// the requested file path.
+			if path == file {
+				return rule, nil
+			}
+		}
+	}
+	return nil, fmt.Errorf("failed to resolved a target for file label %q", fileLabel)
+}
+
+// searchCommonLocations searches through common locations like third_party to
+// find a target providing imports which cannot be resolved using other
+// techniques.
+func (q *QueryBasedTargetLoader) resolveImportsInCommonLocations(results map[string]*appb.Rule, paths []string) error {
+	var queries []string
+	labelToPath := make(map[string]string)
+
+	for _, path := range paths {
+		// third_party uses '_' instead of '-' since the latter is not allowed
+		// in target labels
+		underscored := strings.Replace(path, "-", "_", -1)
+		file := underscored
+		module := underscored
+		if i := strings.Index(underscored, "/"); i >= 0 {
+			// Use the slash in the import path as a separator between the
+			// module name and the path under the module.
+			file = underscored[i+1:]
+			module = commonModuleName(underscored[:i])
+		}
+		for _, l := range commonModuleLocations {
+			// Construct the potential target label in the common location.
+			target := fmt.Sprintf("%s:%s", fmt.Sprintf(l, module), file)
+			queries = append(queries, target)
+			labelToPath[target] = path
+		}
+	}
+	r, err := q.batchQuery(queries)
+	if err != nil {
+		return err
+	}
+	for _, target := range r.GetTarget() {
+		r := target.GetRule()
+		// TODO(jdhamlik): Determine if it's required that the alias resolves to
+		// an allowedRuleClass.
+		// Allow alias rules to provide imports. Alias rules should only appear
+		// in this context if they are special-cased above.
+		if c := r.GetRuleClass(); isTazeManagedRuleClass(c) || c == "alias" {
+			results[labelToPath[r.GetName()]] = r
+		}
+	}
+	return nil
+}
+
+// batchQuery runs a set of queries with a single call to Bazel query and the
+// '--keep_going' flag.
+func (q *QueryBasedTargetLoader) batchQuery(queries []string) (*appb.QueryResult, error) {
+	// 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")
+	}
+	if query := args[n-1]; 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...)
+	cmd := exec.Command(q.bazelBinary, args...)
+	cmd.Dir = q.workdir
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	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.
+		if err.Error() != "exit status 3" {
+			// The error provided as a result is less useful than the contents of
+			// stderr for debugging.
+			return nil, fmt.Errorf(stderr.String())
+		}
+	}
+	var result appb.QueryResult
+	if err := proto.Unmarshal(stdout.Bytes(), &result); err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+// 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
+}
+
+// commonModuleName maps module names to their common names. If no common name
+// is set for a module, it returns the module's name as is.
+func commonModuleName(path string) string {
+	return path
+}
+
+// isTazeManagedRuleClass checks if a class is a ts_auto_deps-managed rule class.
+func isTazeManagedRuleClass(class string) bool {
+	for _, c := range ts_auto_depsManagedRuleClasses {
+		if c == class {
+			return true
+		}
+	}
+	return false
+}
diff --git a/ts_auto_deps/analyze/query.go b/ts_auto_deps/analyze/query.go
index 402113b..66336a3 100644
--- a/ts_auto_deps/analyze/query.go
+++ b/ts_auto_deps/analyze/query.go
@@ -4,9 +4,7 @@
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 
@@ -20,19 +18,6 @@
 )
 
 var (
-	commonModuleLocations = []string{
-		"//third_party/javascript/%s",
-		"//third_party/javascript/node_modules/%s",
-		"//third_party/javascript/typings/%s",
-	}
-
-	ts_auto_depsManagedRuleClasses = []string{
-		"ts_library",
-		"ts_declaration",
-		"ng_module",
-		"js_library",
-	}
-
 	extensions = []string{
 		// '.d.ts' must come before '.ts' to completely remove the '.d.ts'
 		// extension.
@@ -391,7 +376,7 @@
 					}
 				}
 				report.MissingDependencyGroup = append(report.MissingDependencyGroup, &arpb.DependencyGroup{
-					Dependency: []string{i.knownTarget},
+					Dependency: []string{edit.ShortenLabel(i.knownTarget, "")},
 					ImportPath: []string{i.resolvedPath()},
 				})
 			}
@@ -451,303 +436,3 @@
 	}
 	return nil
 }
-
-// QueryBasedTargetLoader uses Bazel query to load targets from BUILD files.
-type QueryBasedTargetLoader struct {
-	workdir     string
-	bazelBinary string
-}
-
-// NewQueryBasedTargetLoader constructs a new QueryBasedTargetLoader rooted
-// in workdir.
-func NewQueryBasedTargetLoader(workdir, bazelBinary string) *QueryBasedTargetLoader {
-	return &QueryBasedTargetLoader{
-		workdir:     workdir,
-		bazelBinary: bazelBinary,
-	}
-}
-
-// LoadLabels uses Bazel query to load targets associated with labels from BUILD
-// files.
-func (q *QueryBasedTargetLoader) LoadLabels(labels []string) (map[string]*appb.Rule, error) {
-	// Ensure the labels are unique to minimize the total number of targets that
-	// need to be loaded.
-	r, err := q.batchQuery(dedupeLabels(labels))
-	if err != nil {
-		return nil, err
-	}
-	targets := make(map[string]*appb.Rule)
-	for _, target := range r.GetTarget() {
-		t, o, err := q.resultObject(target)
-		if err != nil {
-			return nil, err
-		}
-		if desired := appb.Target_RULE; t != desired {
-			return nil, fmt.Errorf("label %q included object of type %q instead of %q", o.GetName(), t, desired)
-		}
-		targets[o.GetName()] = o.(*appb.Rule)
-	}
-	return targets, nil
-}
-
-// dedupeLabels returns a new set of labels with no duplicates.
-func dedupeLabels(labels []string) []string {
-	m := make(map[string]bool)
-	var u []string
-	for _, label := range labels {
-		if _, ok := m[label]; !ok {
-			m[label] = true
-			u = append(u, label)
-		}
-	}
-	return u
-}
-
-// target is the object of an appb.Target instance. This can be any of:
-// source file, generated file, environment group, rule, or package group.
-//
-// Methods which are shared between all of the possible objects are added
-// here.
-type target interface {
-	GetName() string
-}
-
-// resultObject retrieves the object included in target. Uses the discriminator
-// present on the target to determine which object to return.
-func (q *QueryBasedTargetLoader) resultObject(target *appb.Target) (appb.Target_Discriminator, target, error) {
-	switch t := target.GetType(); t {
-	case appb.Target_ENVIRONMENT_GROUP:
-		return t, target.GetEnvironmentGroup(), nil
-	case appb.Target_GENERATED_FILE:
-		return t, target.GetGeneratedFile(), nil
-	case appb.Target_PACKAGE_GROUP:
-		return t, target.GetPackageGroup(), nil
-	case appb.Target_RULE:
-		return t, target.GetRule(), nil
-	case appb.Target_SOURCE_FILE:
-		return t, target.GetSourceFile(), nil
-	default:
-		// Unfortunately we cannot get the label of the target which caused
-		// the issue.
-		return t, nil, fmt.Errorf("target has object of unknown type %q", t)
-	}
-}
-
-// LoadImportPaths uses Bazel Query to load targets associated with import
-// paths from BUILD files.
-func (q *QueryBasedTargetLoader) LoadImportPaths(root string, paths []string) (map[string]*appb.Rule, error) {
-	var remainingImportPaths []string
-	results := make(map[string]*appb.Rule)
-	for _, path := range paths {
-		if trim := strings.TrimPrefix(path, workspace.Name()+"/"); trim != path {
-			// TODO(jdhamlik): Optimize by grouping the queries into one larger query.
-			// TODO(jdhamlik): Handle .d.ts and .tsx files.
-			r, err := q.query(trim + ".ts")
-			if err != nil {
-				return nil, err
-			}
-			targets := r.GetTarget()
-			// Expecting to get one, and only one, target per query.
-			n := len(targets)
-			if n < 1 {
-				return nil, fmt.Errorf("failed to resolved a target for file %q", trim+".ts")
-			}
-			if n > 1 {
-				return nil, fmt.Errorf("got %d targets when only one was expected", n)
-			}
-			t, err := q.loadRuleIncludingFile(targets[0].GetSourceFile().GetName())
-			if err != nil {
-				return nil, err
-			}
-			results[path] = t
-		} else if trim := strings.TrimPrefix(path, "goog:"); trim != path {
-			// There are no current heuristics in this implementation for
-			// 'goog:' imports. That is handled by the ts_auto_deps binary proper.
-			results[path] = nil
-		} else {
-			// The import is not explicitly under google3 or a closure-style
-			// import.
-			remainingImportPaths = append(remainingImportPaths, path)
-		}
-	}
-	var potentialCommonImports []string
-	// Attempt to locate the file rooted in the workspace even though it isn't
-	// prefixed by 'google3/'.
-	for _, imp := range remainingImportPaths {
-		// 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
-		path := strings.TrimSuffix(strings.TrimSuffix(imp, ".ngsummary"), ".ngfactory")
-		res, err := q.batchQuery(pathWithExtensions(path))
-		if err != nil {
-			return nil, err
-		}
-		if len(res.GetTarget()) > 0 {
-			t, obj, err := q.resultObject(res.GetTarget()[0])
-			if err != nil {
-				return nil, err
-			}
-			if desired := appb.Target_SOURCE_FILE; t != desired {
-				return nil, fmt.Errorf("label %q included object of type %q instead of %q", obj.GetName(), t, desired)
-			}
-			target, err := q.loadRuleIncludingFile(obj.GetName())
-			if err != nil {
-				return nil, err
-			}
-			results[imp] = target
-			continue
-		}
-		potentialCommonImports = append(potentialCommonImports, imp)
-	}
-	if err := q.resolveImportsInCommonLocations(results, potentialCommonImports); err != nil {
-		return nil, err
-	}
-	return results, nil
-}
-
-// loadRuleIncludingFile loads the target associated with a file label.
-func (q *QueryBasedTargetLoader) loadRuleIncludingFile(fileLabel string) (*appb.Rule, error) {
-	_, pkg, file := edit.ParseLabel(fileLabel)
-	// Filter the targets in the file label's package to only targets which
-	// include the file in their 'srcs' attribute.
-	r, err := q.query(fmt.Sprintf("attr('srcs', %s, //%s:*)", file, pkg))
-	if err != nil {
-		return nil, err
-	}
-	for _, target := range r.GetTarget() {
-		rule := target.GetRule()
-		for _, src := range listAttribute(rule, "srcs") {
-			_, _, path := edit.ParseLabel(src)
-			// Return the first rule which has a source file exactly matching
-			// the requested file path.
-			if path == file {
-				return rule, nil
-			}
-		}
-	}
-	return nil, fmt.Errorf("failed to resolved a target for file label %q", fileLabel)
-}
-
-// searchCommonLocations searches through common locations like third_party to
-// find a target providing imports which cannot be resolved using other
-// techniques.
-func (q *QueryBasedTargetLoader) resolveImportsInCommonLocations(results map[string]*appb.Rule, paths []string) error {
-	var queries []string
-	labelToPath := make(map[string]string)
-	// TODO(jdhamlik): Determine how to resolve this target. Whether alias
-	// support should be dropped entirely, this "platform_browser_safe" import
-	// should be special cased, or alias resolution should be implemented.
-
-	for _, path := range paths {
-		// third_party uses '_' instead of '-' since the latter is not allowed
-		// in target labels
-		underscored := strings.Replace(path, "-", "_", -1)
-		file := underscored
-		module := underscored
-		if i := strings.Index(underscored, "/"); i >= 0 {
-			// Use the slash in the import path as a separator between the
-			// module name and the path under the module.
-			file = underscored[i+1:]
-			module = commonModuleName(underscored[:i])
-		}
-		for _, l := range commonModuleLocations {
-			// Construct the potential target label in the common location.
-			target := fmt.Sprintf("%s:%s", fmt.Sprintf(l, module), file)
-			queries = append(queries, target)
-			labelToPath[target] = path
-		}
-	}
-	r, err := q.batchQuery(queries)
-	if err != nil {
-		return err
-	}
-	for _, target := range r.GetTarget() {
-		r := target.GetRule()
-		// TODO(jdhamlik): Determine if it's required that the alias resolves to
-		// an allowedRuleClass.
-		// Allow alias rules to provide imports. Alias rules should only appear
-		// in this context if they are special-cased above.
-		if c := r.GetRuleClass(); isTazeManagedRuleClass(c) || c == "alias" {
-			results[labelToPath[r.GetName()]] = r
-		}
-	}
-	return nil
-}
-
-// commonModuleName maps module names to their common names. If no common name
-// is set for a module, it returns the module's name as is.
-func commonModuleName(path string) string {
-	return path
-}
-
-// isTazeManagedRuleClass checks if a class is a ts_auto_deps-managed rule class.
-func isTazeManagedRuleClass(class string) bool {
-	for _, c := range ts_auto_depsManagedRuleClasses {
-		if c == class {
-			return true
-		}
-	}
-	return false
-}
-
-// 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")
-	}
-	if query := args[n-1]; query == "" {
-		// An empty query was provided so return an empty result without
-		// making a call to Bazel.
-		return &appb.QueryResult{}, nil
-	}
-	args = append([]string{"query", "--output=proto"}, args...)
-	cmd := exec.Command(q.bazelBinary, args...)
-	cmd.Dir = q.workdir
-	stdoutPipe, err := cmd.StdoutPipe()
-	if err != nil {
-		return nil, err
-	}
-	stderrPipe, err := cmd.StderrPipe()
-	if err != nil {
-		return nil, err
-	}
-	if err := cmd.Start(); err != nil {
-		return nil, err
-	}
-	// Collect all of stdout and stderr. Stdout will contain the QueryResult,
-	// if any, and stderr will contain any potential errors either as a result
-	// of the '--keep_going' flag or other, more problematic, errors.
-	stdout, err := ioutil.ReadAll(stdoutPipe)
-	if err != nil {
-		return nil, err
-	}
-	stderr, err := ioutil.ReadAll(stderrPipe)
-	if err != nil {
-		return nil, err
-	}
-	if err := cmd.Wait(); 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.
-		if err.Error() != "exit status 3" {
-			// The error provided as a result is less useful than the contents of
-			// stderr for debugging.
-			return nil, fmt.Errorf(string(stderr))
-		}
-	}
-	var result appb.QueryResult
-	if err := proto.Unmarshal(stdout, &result); err != nil {
-		return nil, err
-	}
-	return &result, nil
-}