+++ /dev/null
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package packages
-
-import (
- "encoding/json"
- "fmt"
- "go/parser"
- "go/token"
- "log"
- "os"
- "path/filepath"
- "regexp"
- "sort"
- "strconv"
- "strings"
-
- "golang.org/x/tools/internal/gocommand"
-)
-
-// processGolistOverlay provides rudimentary support for adding
-// files that don't exist on disk to an overlay. The results can be
-// sometimes incorrect.
-// TODO(matloob): Handle unsupported cases, including the following:
-// - determining the correct package to add given a new import path
-func (state *golistState) processGolistOverlay(response *responseDeduper) (modifiedPkgs, needPkgs []string, err error) {
- havePkgs := make(map[string]string) // importPath -> non-test package ID
- needPkgsSet := make(map[string]bool)
- modifiedPkgsSet := make(map[string]bool)
-
- pkgOfDir := make(map[string][]*Package)
- for _, pkg := range response.dr.Packages {
- // This is an approximation of import path to id. This can be
- // wrong for tests, vendored packages, and a number of other cases.
- havePkgs[pkg.PkgPath] = pkg.ID
- x := commonDir(pkg.GoFiles)
- if x != "" {
- pkgOfDir[x] = append(pkgOfDir[x], pkg)
- }
- }
-
- // If no new imports are added, it is safe to avoid loading any needPkgs.
- // Otherwise, it's hard to tell which package is actually being loaded
- // (due to vendoring) and whether any modified package will show up
- // in the transitive set of dependencies (because new imports are added,
- // potentially modifying the transitive set of dependencies).
- var overlayAddsImports bool
-
- // If both a package and its test package are created by the overlay, we
- // need the real package first. Process all non-test files before test
- // files, and make the whole process deterministic while we're at it.
- var overlayFiles []string
- for opath := range state.cfg.Overlay {
- overlayFiles = append(overlayFiles, opath)
- }
- sort.Slice(overlayFiles, func(i, j int) bool {
- iTest := strings.HasSuffix(overlayFiles[i], "_test.go")
- jTest := strings.HasSuffix(overlayFiles[j], "_test.go")
- if iTest != jTest {
- return !iTest // non-tests are before tests.
- }
- return overlayFiles[i] < overlayFiles[j]
- })
- for _, opath := range overlayFiles {
- contents := state.cfg.Overlay[opath]
- base := filepath.Base(opath)
- dir := filepath.Dir(opath)
- var pkg *Package // if opath belongs to both a package and its test variant, this will be the test variant
- var testVariantOf *Package // if opath is a test file, this is the package it is testing
- var fileExists bool
- isTestFile := strings.HasSuffix(opath, "_test.go")
- pkgName, ok := extractPackageName(opath, contents)
- if !ok {
- // Don't bother adding a file that doesn't even have a parsable package statement
- // to the overlay.
- continue
- }
- // If all the overlay files belong to a different package, change the
- // package name to that package.
- maybeFixPackageName(pkgName, isTestFile, pkgOfDir[dir])
- nextPackage:
- for _, p := range response.dr.Packages {
- if pkgName != p.Name && p.ID != "command-line-arguments" {
- continue
- }
- for _, f := range p.GoFiles {
- if !sameFile(filepath.Dir(f), dir) {
- continue
- }
- // Make sure to capture information on the package's test variant, if needed.
- if isTestFile && !hasTestFiles(p) {
- // TODO(matloob): Are there packages other than the 'production' variant
- // of a package that this can match? This shouldn't match the test main package
- // because the file is generated in another directory.
- testVariantOf = p
- continue nextPackage
- } else if !isTestFile && hasTestFiles(p) {
- // We're examining a test variant, but the overlaid file is
- // a non-test file. Because the overlay implementation
- // (currently) only adds a file to one package, skip this
- // package, so that we can add the file to the production
- // variant of the package. (https://golang.org/issue/36857
- // tracks handling overlays on both the production and test
- // variant of a package).
- continue nextPackage
- }
- if pkg != nil && p != pkg && pkg.PkgPath == p.PkgPath {
- // We have already seen the production version of the
- // for which p is a test variant.
- if hasTestFiles(p) {
- testVariantOf = pkg
- }
- }
- pkg = p
- if filepath.Base(f) == base {
- fileExists = true
- }
- }
- }
- // The overlay could have included an entirely new package or an
- // ad-hoc package. An ad-hoc package is one that we have manually
- // constructed from inadequate `go list` results for a file= query.
- // It will have the ID command-line-arguments.
- if pkg == nil || pkg.ID == "command-line-arguments" {
- // Try to find the module or gopath dir the file is contained in.
- // Then for modules, add the module opath to the beginning.
- pkgPath, ok, err := state.getPkgPath(dir)
- if err != nil {
- return nil, nil, err
- }
- if !ok {
- break
- }
- var forTest string // only set for x tests
- isXTest := strings.HasSuffix(pkgName, "_test")
- if isXTest {
- forTest = pkgPath
- pkgPath += "_test"
- }
- id := pkgPath
- if isTestFile {
- if isXTest {
- id = fmt.Sprintf("%s [%s.test]", pkgPath, forTest)
- } else {
- id = fmt.Sprintf("%s [%s.test]", pkgPath, pkgPath)
- }
- }
- if pkg != nil {
- // TODO(rstambler): We should change the package's path and ID
- // here. The only issue is that this messes with the roots.
- } else {
- // Try to reclaim a package with the same ID, if it exists in the response.
- for _, p := range response.dr.Packages {
- if reclaimPackage(p, id, opath, contents) {
- pkg = p
- break
- }
- }
- // Otherwise, create a new package.
- if pkg == nil {
- pkg = &Package{
- PkgPath: pkgPath,
- ID: id,
- Name: pkgName,
- Imports: make(map[string]*Package),
- }
- response.addPackage(pkg)
- havePkgs[pkg.PkgPath] = id
- // Add the production package's sources for a test variant.
- if isTestFile && !isXTest && testVariantOf != nil {
- pkg.GoFiles = append(pkg.GoFiles, testVariantOf.GoFiles...)
- pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, testVariantOf.CompiledGoFiles...)
- // Add the package under test and its imports to the test variant.
- pkg.forTest = testVariantOf.PkgPath
- for k, v := range testVariantOf.Imports {
- pkg.Imports[k] = &Package{ID: v.ID}
- }
- }
- if isXTest {
- pkg.forTest = forTest
- }
- }
- }
- }
- if !fileExists {
- pkg.GoFiles = append(pkg.GoFiles, opath)
- // TODO(matloob): Adding the file to CompiledGoFiles can exhibit the wrong behavior
- // if the file will be ignored due to its build tags.
- pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, opath)
- modifiedPkgsSet[pkg.ID] = true
- }
- imports, err := extractImports(opath, contents)
- if err != nil {
- // Let the parser or type checker report errors later.
- continue
- }
- for _, imp := range imports {
- // TODO(rstambler): If the package is an x test and the import has
- // a test variant, make sure to replace it.
- if _, found := pkg.Imports[imp]; found {
- continue
- }
- overlayAddsImports = true
- id, ok := havePkgs[imp]
- if !ok {
- var err error
- id, err = state.resolveImport(dir, imp)
- if err != nil {
- return nil, nil, err
- }
- }
- pkg.Imports[imp] = &Package{ID: id}
- // Add dependencies to the non-test variant version of this package as well.
- if testVariantOf != nil {
- testVariantOf.Imports[imp] = &Package{ID: id}
- }
- }
- }
-
- // toPkgPath guesses the package path given the id.
- toPkgPath := func(sourceDir, id string) (string, error) {
- if i := strings.IndexByte(id, ' '); i >= 0 {
- return state.resolveImport(sourceDir, id[:i])
- }
- return state.resolveImport(sourceDir, id)
- }
-
- // Now that new packages have been created, do another pass to determine
- // the new set of missing packages.
- for _, pkg := range response.dr.Packages {
- for _, imp := range pkg.Imports {
- if len(pkg.GoFiles) == 0 {
- return nil, nil, fmt.Errorf("cannot resolve imports for package %q with no Go files", pkg.PkgPath)
- }
- pkgPath, err := toPkgPath(filepath.Dir(pkg.GoFiles[0]), imp.ID)
- if err != nil {
- return nil, nil, err
- }
- if _, ok := havePkgs[pkgPath]; !ok {
- needPkgsSet[pkgPath] = true
- }
- }
- }
-
- if overlayAddsImports {
- needPkgs = make([]string, 0, len(needPkgsSet))
- for pkg := range needPkgsSet {
- needPkgs = append(needPkgs, pkg)
- }
- }
- modifiedPkgs = make([]string, 0, len(modifiedPkgsSet))
- for pkg := range modifiedPkgsSet {
- modifiedPkgs = append(modifiedPkgs, pkg)
- }
- return modifiedPkgs, needPkgs, err
-}
-
-// resolveImport finds the ID of a package given its import path.
-// In particular, it will find the right vendored copy when in GOPATH mode.
-func (state *golistState) resolveImport(sourceDir, importPath string) (string, error) {
- env, err := state.getEnv()
- if err != nil {
- return "", err
- }
- if env["GOMOD"] != "" {
- return importPath, nil
- }
-
- searchDir := sourceDir
- for {
- vendorDir := filepath.Join(searchDir, "vendor")
- exists, ok := state.vendorDirs[vendorDir]
- if !ok {
- info, err := os.Stat(vendorDir)
- exists = err == nil && info.IsDir()
- state.vendorDirs[vendorDir] = exists
- }
-
- if exists {
- vendoredPath := filepath.Join(vendorDir, importPath)
- if info, err := os.Stat(vendoredPath); err == nil && info.IsDir() {
- // We should probably check for .go files here, but shame on anyone who fools us.
- path, ok, err := state.getPkgPath(vendoredPath)
- if err != nil {
- return "", err
- }
- if ok {
- return path, nil
- }
- }
- }
-
- // We know we've hit the top of the filesystem when we Dir / and get /,
- // or C:\ and get C:\, etc.
- next := filepath.Dir(searchDir)
- if next == searchDir {
- break
- }
- searchDir = next
- }
- return importPath, nil
-}
-
-func hasTestFiles(p *Package) bool {
- for _, f := range p.GoFiles {
- if strings.HasSuffix(f, "_test.go") {
- return true
- }
- }
- return false
-}
-
-// determineRootDirs returns a mapping from absolute directories that could
-// contain code to their corresponding import path prefixes.
-func (state *golistState) determineRootDirs() (map[string]string, error) {
- env, err := state.getEnv()
- if err != nil {
- return nil, err
- }
- if env["GOMOD"] != "" {
- state.rootsOnce.Do(func() {
- state.rootDirs, state.rootDirsError = state.determineRootDirsModules()
- })
- } else {
- state.rootsOnce.Do(func() {
- state.rootDirs, state.rootDirsError = state.determineRootDirsGOPATH()
- })
- }
- return state.rootDirs, state.rootDirsError
-}
-
-func (state *golistState) determineRootDirsModules() (map[string]string, error) {
- // List all of the modules--the first will be the directory for the main
- // module. Any replaced modules will also need to be treated as roots.
- // Editing files in the module cache isn't a great idea, so we don't
- // plan to ever support that.
- out, err := state.invokeGo("list", "-m", "-json", "all")
- if err != nil {
- // 'go list all' will fail if we're outside of a module and
- // GO111MODULE=on. Try falling back without 'all'.
- var innerErr error
- out, innerErr = state.invokeGo("list", "-m", "-json")
- if innerErr != nil {
- return nil, err
- }
- }
- roots := map[string]string{}
- modules := map[string]string{}
- var i int
- for dec := json.NewDecoder(out); dec.More(); {
- mod := new(gocommand.ModuleJSON)
- if err := dec.Decode(mod); err != nil {
- return nil, err
- }
- if mod.Dir != "" && mod.Path != "" {
- // This is a valid module; add it to the map.
- absDir, err := filepath.Abs(mod.Dir)
- if err != nil {
- return nil, err
- }
- modules[absDir] = mod.Path
- // The first result is the main module.
- if i == 0 || mod.Replace != nil && mod.Replace.Path != "" {
- roots[absDir] = mod.Path
- }
- }
- i++
- }
- return roots, nil
-}
-
-func (state *golistState) determineRootDirsGOPATH() (map[string]string, error) {
- m := map[string]string{}
- for _, dir := range filepath.SplitList(state.mustGetEnv()["GOPATH"]) {
- absDir, err := filepath.Abs(dir)
- if err != nil {
- return nil, err
- }
- m[filepath.Join(absDir, "src")] = ""
- }
- return m, nil
-}
-
-func extractImports(filename string, contents []byte) ([]string, error) {
- f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.ImportsOnly) // TODO(matloob): reuse fileset?
- if err != nil {
- return nil, err
- }
- var res []string
- for _, imp := range f.Imports {
- quotedPath := imp.Path.Value
- path, err := strconv.Unquote(quotedPath)
- if err != nil {
- return nil, err
- }
- res = append(res, path)
- }
- return res, nil
-}
-
-// reclaimPackage attempts to reuse a package that failed to load in an overlay.
-//
-// If the package has errors and has no Name, GoFiles, or Imports,
-// then it's possible that it doesn't yet exist on disk.
-func reclaimPackage(pkg *Package, id string, filename string, contents []byte) bool {
- // TODO(rstambler): Check the message of the actual error?
- // It differs between $GOPATH and module mode.
- if pkg.ID != id {
- return false
- }
- if len(pkg.Errors) != 1 {
- return false
- }
- if pkg.Name != "" || pkg.ExportFile != "" {
- return false
- }
- if len(pkg.GoFiles) > 0 || len(pkg.CompiledGoFiles) > 0 || len(pkg.OtherFiles) > 0 {
- return false
- }
- if len(pkg.Imports) > 0 {
- return false
- }
- pkgName, ok := extractPackageName(filename, contents)
- if !ok {
- return false
- }
- pkg.Name = pkgName
- pkg.Errors = nil
- return true
-}
-
-func extractPackageName(filename string, contents []byte) (string, bool) {
- // TODO(rstambler): Check the message of the actual error?
- // It differs between $GOPATH and module mode.
- f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.PackageClauseOnly) // TODO(matloob): reuse fileset?
- if err != nil {
- return "", false
- }
- return f.Name.Name, true
-}
-
-func commonDir(a []string) string {
- seen := make(map[string]bool)
- x := append([]string{}, a...)
- for _, f := range x {
- seen[filepath.Dir(f)] = true
- }
- if len(seen) > 1 {
- log.Fatalf("commonDir saw %v for %v", seen, x)
- }
- for k := range seen {
- // len(seen) == 1
- return k
- }
- return "" // no files
-}
-
-// It is possible that the files in the disk directory dir have a different package
-// name from newName, which is deduced from the overlays. If they all have a different
-// package name, and they all have the same package name, then that name becomes
-// the package name.
-// It returns true if it changes the package name, false otherwise.
-func maybeFixPackageName(newName string, isTestFile bool, pkgsOfDir []*Package) {
- names := make(map[string]int)
- for _, p := range pkgsOfDir {
- names[p.Name]++
- }
- if len(names) != 1 {
- // some files are in different packages
- return
- }
- var oldName string
- for k := range names {
- oldName = k
- }
- if newName == oldName {
- return
- }
- // We might have a case where all of the package names in the directory are
- // the same, but the overlay file is for an x test, which belongs to its
- // own package. If the x test does not yet exist on disk, we may not yet
- // have its package name on disk, but we should not rename the packages.
- //
- // We use a heuristic to determine if this file belongs to an x test:
- // The test file should have a package name whose package name has a _test
- // suffix or looks like "newName_test".
- maybeXTest := strings.HasPrefix(oldName+"_test", newName) || strings.HasSuffix(newName, "_test")
- if isTestFile && maybeXTest {
- return
- }
- for _, p := range pkgsOfDir {
- p.Name = newName
- }
-}
-
-// This function is copy-pasted from
-// https://github.com/golang/go/blob/9706f510a5e2754595d716bd64be8375997311fb/src/cmd/go/internal/search/search.go#L360.
-// It should be deleted when we remove support for overlays from go/packages.
-//
-// NOTE: This does not handle any ./... or ./ style queries, as this function
-// doesn't know the working directory.
-//
-// matchPattern(pattern)(name) reports whether
-// name matches pattern. Pattern is a limited glob
-// pattern in which '...' means 'any string' and there
-// is no other special syntax.
-// Unfortunately, there are two special cases. Quoting "go help packages":
-//
-// First, /... at the end of the pattern can match an empty string,
-// so that net/... matches both net and packages in its subdirectories, like net/http.
-// Second, any slash-separated pattern element containing a wildcard never
-// participates in a match of the "vendor" element in the path of a vendored
-// package, so that ./... does not match packages in subdirectories of
-// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
-// Note, however, that a directory named vendor that itself contains code
-// is not a vendored package: cmd/vendor would be a command named vendor,
-// and the pattern cmd/... matches it.
-func matchPattern(pattern string) func(name string) bool {
- // Convert pattern to regular expression.
- // The strategy for the trailing /... is to nest it in an explicit ? expression.
- // The strategy for the vendor exclusion is to change the unmatchable
- // vendor strings to a disallowed code point (vendorChar) and to use
- // "(anything but that codepoint)*" as the implementation of the ... wildcard.
- // This is a bit complicated but the obvious alternative,
- // namely a hand-written search like in most shell glob matchers,
- // is too easy to make accidentally exponential.
- // Using package regexp guarantees linear-time matching.
-
- const vendorChar = "\x00"
-
- if strings.Contains(pattern, vendorChar) {
- return func(name string) bool { return false }
- }
-
- re := regexp.QuoteMeta(pattern)
- re = replaceVendor(re, vendorChar)
- switch {
- case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
- re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
- case re == vendorChar+`/\.\.\.`:
- re = `(/vendor|/` + vendorChar + `/\.\.\.)`
- case strings.HasSuffix(re, `/\.\.\.`):
- re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
- }
- re = strings.ReplaceAll(re, `\.\.\.`, `[^`+vendorChar+`]*`)
-
- reg := regexp.MustCompile(`^` + re + `$`)
-
- return func(name string) bool {
- if strings.Contains(name, vendorChar) {
- return false
- }
- return reg.MatchString(replaceVendor(name, vendorChar))
- }
-}
-
-// replaceVendor returns the result of replacing
-// non-trailing vendor path elements in x with repl.
-func replaceVendor(x, repl string) string {
- if !strings.Contains(x, "vendor") {
- return x
- }
- elem := strings.Split(x, "/")
- for i := 0; i < len(elem)-1; i++ {
- if elem[i] == "vendor" {
- elem[i] = repl
- }
- }
- return strings.Join(elem, "/")
-}