| 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, "/") |
| } |