diff --git a/go/packages/doc.go b/go/packages/doc.go index 1240974a..61933de8 100644 --- a/go/packages/doc.go +++ b/go/packages/doc.go @@ -15,9 +15,29 @@ The Load function takes as input a list of patterns and return a list of Package 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 8f86b42c..ec17b8a5 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -28,6 +28,38 @@ func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) { // 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 f5e99ba8..3c996ae8 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -1013,7 +1013,7 @@ func TestAbsoluteFilenames(t *testing.T) { } } -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 @@ func TestContains(t *testing.T) { 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 @@ func TestContains_FallbackSticks(t *testing.T) { 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 TestJSON(t *testing.T) { } } +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.