go/packages: add support for 'contains:' words

If a 'word' provided to go/packages' Load function starts with contains,
go/packages will interpret that word as the package containing the given
file.

For example:
   packages.Load(config, "contains:/usr/local/go/src/fmt/format.go")
would load the fmt package from the Go installation at /usr/local/go.

This implementation uses "go list ." in the directory the file is
contained in to find the package, but this won't work in the module
cache. We plan to add support to go list directly to help find the
containing package. Then, because we won't need to change directory,
go list will have knowledge of the correct vgo root module, and will
be able to surface correct results.

Change-Id: I6bff62447c12f13dae5e4c0c65f729d9f271c388
Reviewed-on: https://go-review.googlesource.com/126177
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/go/packages/packages.go b/go/packages/packages.go
index 5a42be1..c124f7b 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -18,6 +18,8 @@
 	"sync"
 
 	"golang.org/x/tools/go/gcexportdata"
+	"path/filepath"
+	"strings"
 )
 
 // A LoadMode specifies the amount of detail to return when loading packages.
@@ -231,6 +233,12 @@
 	if ld.Context == nil {
 		ld.Context = context.Background()
 	}
+	// Determine directory to be used for relative contains: paths.
+	if ld.Dir == "" {
+		if cwd, err := os.Getwd(); err == nil {
+			ld.Dir = cwd
+		}
+	}
 	if ld.Mode >= LoadSyntax {
 		if ld.Fset == nil {
 			ld.Fset = token.NewFileSet()
@@ -257,21 +265,79 @@
 		return nil, fmt.Errorf("no packages to load")
 	}
 
+	if ld.Dir == "" {
+		return nil, fmt.Errorf("failed to get working directory")
+	}
+
+	// Determine files requested in contains patterns
+	var containFiles []string
+	{
+		restPatterns := patterns[:0]
+		for _, pattern := range patterns {
+			if containFile := strings.TrimPrefix(pattern, "contains:"); containFile != pattern {
+				containFiles = append(containFiles, containFile)
+			} else {
+				restPatterns = append(restPatterns, pattern)
+			}
+		}
+		containFiles = absJoin(ld.Dir, containFiles)
+		patterns = restPatterns
+	}
+
 	// Do the metadata query and partial build.
 	// TODO(adonovan): support alternative build systems at this seam.
 	rawCfg := newRawConfig(&ld.Config)
-	list, err := golistPackages(rawCfg, patterns...)
+	listfunc := golistPackages
+	// TODO(matloob): Patterns may now be empty, if it was solely comprised of contains: patterns.
+	// See if the extra process invocation can be avoided.
+	list, err := listfunc(rawCfg, patterns...)
 	if _, ok := err.(GoTooOldError); ok {
 		if ld.Config.Mode >= LoadTypes {
 			// Upgrade to LoadAllSyntax because we can't depend on the existance
 			// of export data. We can remove this once iancottrell's cl is in.
 			ld.Config.Mode = LoadAllSyntax
 		}
-		list, err = golistPackagesFallback(rawCfg, patterns...)
+		listfunc = golistPackagesFallback
+		list, err = listfunc(rawCfg, patterns...)
 	}
 	if err != nil {
 		return nil, err
 	}
+
+	// Run go list for contains: patterns.
+	seenPkgs := make(map[string]bool) // for deduplication. different containing queries could produce same packages
+	if len(containFiles) > 0 {
+		for _, pkg := range list {
+			seenPkgs[pkg.ID] = true
+		}
+	}
+	for _, f := range containFiles {
+		// TODO(matloob): Do only one query per directory.
+		fdir := filepath.Dir(f)
+		rawCfg.Dir = fdir
+		cList, err := listfunc(rawCfg, ".")
+		if err != nil {
+			return nil, err
+		}
+		// Deduplicate and set deplist to set of packages requested files.
+		dedupedList := cList[:0] // invariant: only packages that haven't been seen before
+		for _, pkg := range cList {
+			if seenPkgs[pkg.ID] {
+				continue
+			}
+			seenPkgs[pkg.ID] = true
+			dedupedList = append(dedupedList, pkg)
+			pkg.DepOnly = true
+			for _, pkgFile := range pkg.GoFiles {
+				if filepath.Base(f) == filepath.Base(pkgFile) {
+					pkg.DepOnly = false
+					break
+				}
+			}
+		}
+		list = append(list, dedupedList...)
+	}
+
 	return ld.loadFrom(list...)
 }
 
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index d475f71..3a63908 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -732,6 +732,31 @@
 	}
 }
 
+func TestContains(t *testing.T) {
+	tmp, cleanup := makeTree(t, map[string]string{
+		"src/a/a.go": `package a; import "b"`,
+		"src/b/b.go": `package b; import "c"`,
+		"src/c/c.go": `package c`,
+	})
+	defer cleanup()
+
+	opts := &packages.Config{Env: append(os.Environ(), "GOPATH="+tmp), Dir: tmp, Mode: packages.LoadImports}
+	initial, err := packages.Load(opts, "contains:src/b/b.go")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	graph, _ := importGraph(initial)
+	wantGraph := `
+* b
+  c
+  b -> c
+`[1:]
+	if graph != wantGraph {
+		t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph)
+	}
+}
+
 func errorMessages(errors []error) []string {
 	var msgs []string
 	for _, err := range errors {