go/packages: use "=" instead of ":" for special queries

This change updates the language accepted through the arguments
to packages.Load to make it more consistent. There are now two layers.
A pattern containing an "=" is considered to be a special query, and the
part of the pattern up to the first "=" is considered the query type. All
other patterns are to be interpreted as the build system interprets it.
For now two special queries exist. file= has the behavior that contains:
did: finding packages containing the given file. The query type pattern=
is used to pass through a pattern to be interpreted by the build system.
pattern= is a type of escaping to allow passing through patterns that
contain an "=" to be interpreted by the underlying buildsystem. To allow
for new query types to be introduced, packages.Load will report an
error if the qury type is not understood. We expect name= to be added in
an  upcoming change.

"contains:" changes to "file=". A new

Change-Id: I1b208d1c998c67d5556cdc872d7694273cedb7e4
Reviewed-on: https://go-review.googlesource.com/c/141681
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/go/packages/doc.go b/go/packages/doc.go
index 1240974..61933de 100644
--- a/go/packages/doc.go
+++ b/go/packages/doc.go
@@ -15,9 +15,29 @@
 structs describing individual packages matched by those patterns.
 The LoadMode controls the amount of detail in the loaded packages.
 
-The patterns are used as arguments to the underlying build tool,
-such as the go command or Bazel, and are interpreted according to
-that tool's conventions.
+Load passes most patterns directly to the underlying build tool,
+but all patterns with the prefix "query=", where query is a
+non-empty string of letters from [a-z], are reserved and may be
+interpreted as query operators.
+
+Only two query operators are currently supported, "file" and "pattern".
+
+The query "file=path/to/file.go" matches the package or packages enclosing
+the Go source file path/to/file.go.  For example "file=~/go/src/fmt/print.go"
+might returns the packages "fmt" and "fmt [fmt.test]".
+
+The query "pattern=string" causes "string" to be passed directly to
+the underlying build tool. In most cases this is unnecessary,
+but an application can use Load("pattern=" + x) as an escaping mechanism
+to ensure that x is not interpreted as a query operator if it contains '='.
+
+A third query "name=identifier" will be added soon.
+It will match packages whose package declaration contains the specified identifier.
+For example, "name=rand" would match the packages "math/rand" and "crypto/rand",
+and "name=main" would match all executables.
+
+All other query operators are reserved for future use and currently
+cause Load to report an error.
 
 The Package struct provides basic information about the package, including
 
diff --git a/go/packages/golist.go b/go/packages/golist.go
index 8f86b42..ec17b8a 100644
--- a/go/packages/golist.go
+++ b/go/packages/golist.go
@@ -28,6 +28,38 @@
 	// Determine files requested in contains patterns
 	var containFiles []string
 	restPatterns := make([]string, 0, len(patterns))
+	// Extract file= and other [querytype]= patterns. Report an error if querytype
+	// doesn't exist.
+extractQueries:
+	for _, pattern := range patterns {
+		eqidx := strings.Index(pattern, "=")
+		if eqidx < 0 {
+			restPatterns = append(restPatterns, pattern)
+		} else {
+			query, value := pattern[:eqidx], pattern[eqidx+len("="):]
+			switch query {
+			case "file":
+				containFiles = append(containFiles, value)
+			case "pattern":
+				restPatterns = append(containFiles, value)
+			case "": // not a reserved query
+				restPatterns = append(restPatterns, pattern)
+			default:
+				for _, rune := range query {
+					if rune < 'a' || rune > 'z' { // not a reserved query
+						restPatterns = append(restPatterns, pattern)
+						continue extractQueries
+					}
+				}
+				// Reject all other patterns containing "="
+				return nil, fmt.Errorf("invalid query type %q in query pattern %q", query, pattern)
+			}
+		}
+	}
+	patterns = restPatterns
+	// Look for the deprecated contains: syntax.
+	// TODO(matloob): delete this around mid-October 2018.
+	restPatterns = restPatterns[:0]
 	for _, pattern := range patterns {
 		if strings.HasPrefix(pattern, "contains:") {
 			containFile := strings.TrimPrefix(pattern, "contains:")
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go
index f5e99ba..3c996ae 100644
--- a/go/packages/packages_test.go
+++ b/go/packages/packages_test.go
@@ -1013,7 +1013,7 @@
 	}
 }
 
-func TestContains(t *testing.T) {
+func TestFile(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"`,
@@ -1026,7 +1026,7 @@
 		Dir:  tmp,
 		Env:  append(os.Environ(), "GOPATH="+tmp, "GO111MODULE=off"),
 	}
-	initial, err := packages.Load(cfg, "contains:src/b/b.go")
+	initial, err := packages.Load(cfg, "file=src/b/b.go")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1093,7 +1093,7 @@
 		Dir:  tmp,
 		Env:  append(os.Environ(), "GOPATH="+tmp, "GO111MODULE=off"),
 	}
-	initial, err := packages.Load(cfg, "a", "contains:src/b/b.go")
+	initial, err := packages.Load(cfg, "a", "file=src/b/b.go")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1261,6 +1261,46 @@
 	}
 }
 
+func TestRejectInvalidQueries(t *testing.T) {
+	queries := []string{"=", "key=", "key=value", "file/a/b=c/..."}
+	cfg := &packages.Config{
+		Mode: packages.LoadImports,
+		Env:  append(os.Environ(), "GO111MODULE=off"),
+	}
+	for _, q := range queries {
+		if _, err := packages.Load(cfg, q); err == nil {
+			t.Errorf("packages.Load(%q) succeeded. Expected \"invalid query type\" error", q)
+		} else if !strings.Contains(err.Error(), "invalid query type") {
+			t.Errorf("packages.Load(%q): got error %v, want \"invalid query type\" error", q, err)
+		}
+	}
+}
+
+func TestPatternPassthrough(t *testing.T) {
+	tmp, cleanup := makeTree(t, map[string]string{
+		"src/a/a.go": `package a;`,
+	})
+	defer cleanup()
+
+	cfg := &packages.Config{
+		Mode: packages.LoadImports,
+		Env:  append(os.Environ(), "GOPATH="+tmp, "GO111MODULE=off"),
+	}
+	initial, err := packages.Load(cfg, "pattern=a")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	graph, _ := importGraph(initial)
+	wantGraph := `
+* a
+`[1:]
+	if graph != wantGraph {
+		t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph)
+	}
+
+}
+
 func TestConfigDefaultEnv(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		// TODO(jayconrod): write an equivalent batch script for windows.