123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- package imports
- import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "log"
- "os"
- "path"
- "path/filepath"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "golang.org/x/tools/internal/gopathwalk"
- "golang.org/x/tools/internal/module"
- )
- // moduleResolver implements resolver for modules using the go command as little
- // as feasible.
- type moduleResolver struct {
- env *fixEnv
- initialized bool
- main *moduleJSON
- modsByModPath []*moduleJSON // All modules, ordered by # of path components in module Path...
- modsByDir []*moduleJSON // ...or Dir.
- }
- type moduleJSON struct {
- Path string // module path
- Version string // module version
- Versions []string // available module versions (with -versions)
- Replace *moduleJSON // replaced by this module
- Time *time.Time // time version was created
- Update *moduleJSON // available update, if any (with -u)
- Main bool // is this the main module?
- Indirect bool // is this module only an indirect dependency of main module?
- Dir string // directory holding files for this module, if any
- GoMod string // path to go.mod file for this module, if any
- Error *moduleErrorJSON // error loading module
- }
- type moduleErrorJSON struct {
- Err string // the error itself
- }
- func (r *moduleResolver) init() error {
- if r.initialized {
- return nil
- }
- stdout, err := r.env.invokeGo("list", "-m", "-json", "...")
- if err != nil {
- return err
- }
- for dec := json.NewDecoder(stdout); dec.More(); {
- mod := &moduleJSON{}
- if err := dec.Decode(mod); err != nil {
- return err
- }
- if mod.Dir == "" {
- if Debug {
- log.Printf("module %v has not been downloaded and will be ignored", mod.Path)
- }
- // Can't do anything with a module that's not downloaded.
- continue
- }
- r.modsByModPath = append(r.modsByModPath, mod)
- r.modsByDir = append(r.modsByDir, mod)
- if mod.Main {
- r.main = mod
- }
- }
- sort.Slice(r.modsByModPath, func(i, j int) bool {
- count := func(x int) int {
- return strings.Count(r.modsByModPath[x].Path, "/")
- }
- return count(j) < count(i) // descending order
- })
- sort.Slice(r.modsByDir, func(i, j int) bool {
- count := func(x int) int {
- return strings.Count(r.modsByDir[x].Dir, "/")
- }
- return count(j) < count(i) // descending order
- })
- r.initialized = true
- return nil
- }
- // findPackage returns the module and directory that contains the package at
- // the given import path, or returns nil, "" if no module is in scope.
- func (r *moduleResolver) findPackage(importPath string) (*moduleJSON, string) {
- for _, m := range r.modsByModPath {
- if !strings.HasPrefix(importPath, m.Path) {
- continue
- }
- pathInModule := importPath[len(m.Path):]
- pkgDir := filepath.Join(m.Dir, pathInModule)
- if dirIsNestedModule(pkgDir, m) {
- continue
- }
- pkgFiles, err := ioutil.ReadDir(pkgDir)
- if err != nil {
- continue
- }
- // A module only contains a package if it has buildable go
- // files in that directory. If not, it could be provided by an
- // outer module. See #29736.
- for _, fi := range pkgFiles {
- if ok, _ := r.env.buildContext().MatchFile(pkgDir, fi.Name()); ok {
- return m, pkgDir
- }
- }
- }
- return nil, ""
- }
- // findModuleByDir returns the module that contains dir, or nil if no such
- // module is in scope.
- func (r *moduleResolver) findModuleByDir(dir string) *moduleJSON {
- // This is quite tricky and may not be correct. dir could be:
- // - a package in the main module.
- // - a replace target underneath the main module's directory.
- // - a nested module in the above.
- // - a replace target somewhere totally random.
- // - a nested module in the above.
- // - in the mod cache.
- // - in /vendor/ in -mod=vendor mode.
- // - nested module? Dunno.
- // Rumor has it that replace targets cannot contain other replace targets.
- for _, m := range r.modsByDir {
- if !strings.HasPrefix(dir, m.Dir) {
- continue
- }
- if dirIsNestedModule(dir, m) {
- continue
- }
- return m
- }
- return nil
- }
- // dirIsNestedModule reports if dir is contained in a nested module underneath
- // mod, not actually in mod.
- func dirIsNestedModule(dir string, mod *moduleJSON) bool {
- if !strings.HasPrefix(dir, mod.Dir) {
- return false
- }
- mf := findModFile(dir)
- if mf == "" {
- return false
- }
- return filepath.Dir(mf) != mod.Dir
- }
- func findModFile(dir string) string {
- for {
- f := filepath.Join(dir, "go.mod")
- info, err := os.Stat(f)
- if err == nil && !info.IsDir() {
- return f
- }
- d := filepath.Dir(dir)
- if len(d) >= len(dir) {
- return "" // reached top of file system, no go.mod
- }
- dir = d
- }
- }
- func (r *moduleResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
- if err := r.init(); err != nil {
- return nil, err
- }
- names := map[string]string{}
- for _, path := range importPaths {
- _, packageDir := r.findPackage(path)
- if packageDir == "" {
- continue
- }
- name, err := packageDirToName(packageDir)
- if err != nil {
- continue
- }
- names[path] = name
- }
- return names, nil
- }
- func (r *moduleResolver) scan(_ references) ([]*pkg, error) {
- if err := r.init(); err != nil {
- return nil, err
- }
- // Walk GOROOT, GOPATH/pkg/mod, and the main module.
- roots := []gopathwalk.Root{
- {filepath.Join(r.env.GOROOT, "/src"), gopathwalk.RootGOROOT},
- }
- if r.main != nil {
- roots = append(roots, gopathwalk.Root{r.main.Dir, gopathwalk.RootCurrentModule})
- }
- for _, p := range filepath.SplitList(r.env.GOPATH) {
- roots = append(roots, gopathwalk.Root{filepath.Join(p, "/pkg/mod"), gopathwalk.RootModuleCache})
- }
- // Walk replace targets, just in case they're not in any of the above.
- for _, mod := range r.modsByModPath {
- if mod.Replace != nil {
- roots = append(roots, gopathwalk.Root{mod.Dir, gopathwalk.RootOther})
- }
- }
- var result []*pkg
- dupCheck := make(map[string]bool)
- var mu sync.Mutex
- gopathwalk.Walk(roots, func(root gopathwalk.Root, dir string) {
- mu.Lock()
- defer mu.Unlock()
- if _, dup := dupCheck[dir]; dup {
- return
- }
- dupCheck[dir] = true
- subdir := ""
- if dir != root.Path {
- subdir = dir[len(root.Path)+len("/"):]
- }
- importPath := filepath.ToSlash(subdir)
- if strings.HasPrefix(importPath, "vendor/") {
- // Ignore vendor dirs. If -mod=vendor is on, then things
- // should mostly just work, but when it's not vendor/
- // is a mess. There's no easy way to tell if it's on.
- // We can still find things in the mod cache and
- // map them into /vendor when -mod=vendor is on.
- return
- }
- switch root.Type {
- case gopathwalk.RootCurrentModule:
- importPath = path.Join(r.main.Path, filepath.ToSlash(subdir))
- case gopathwalk.RootModuleCache:
- matches := modCacheRegexp.FindStringSubmatch(subdir)
- modPath, err := module.DecodePath(filepath.ToSlash(matches[1]))
- if err != nil {
- if Debug {
- log.Printf("decoding module cache path %q: %v", subdir, err)
- }
- return
- }
- importPath = path.Join(modPath, filepath.ToSlash(matches[3]))
- case gopathwalk.RootGOROOT:
- importPath = subdir
- }
- // Check if the directory is underneath a module that's in scope.
- if mod := r.findModuleByDir(dir); mod != nil {
- // It is. If dir is the target of a replace directive,
- // our guessed import path is wrong. Use the real one.
- if mod.Dir == dir {
- importPath = mod.Path
- } else {
- dirInMod := dir[len(mod.Dir)+len("/"):]
- importPath = path.Join(mod.Path, filepath.ToSlash(dirInMod))
- }
- } else {
- // The package is in an unknown module. Check that it's
- // not obviously impossible to import.
- var modFile string
- switch root.Type {
- case gopathwalk.RootModuleCache:
- matches := modCacheRegexp.FindStringSubmatch(subdir)
- modFile = filepath.Join(matches[1], "@", matches[2], "go.mod")
- default:
- modFile = findModFile(dir)
- }
- modBytes, err := ioutil.ReadFile(modFile)
- if err == nil && !strings.HasPrefix(importPath, modulePath(modBytes)) {
- // The module's declared path does not match
- // its expected path. It probably needs a
- // replace directive we don't have.
- return
- }
- }
- // We may have discovered a package that has a different version
- // in scope already. Canonicalize to that one if possible.
- if _, canonicalDir := r.findPackage(importPath); canonicalDir != "" {
- dir = canonicalDir
- }
- result = append(result, &pkg{
- importPathShort: VendorlessPath(importPath),
- dir: dir,
- })
- }, gopathwalk.Options{Debug: Debug, ModulesEnabled: true})
- return result, nil
- }
- // modCacheRegexp splits a path in a module cache into module, module version, and package.
- var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`)
- var (
- slashSlash = []byte("//")
- moduleStr = []byte("module")
- )
- // modulePath returns the module path from the gomod file text.
- // If it cannot find a module path, it returns an empty string.
- // It is tolerant of unrelated problems in the go.mod file.
- //
- // Copied from cmd/go/internal/modfile.
- func modulePath(mod []byte) string {
- for len(mod) > 0 {
- line := mod
- mod = nil
- if i := bytes.IndexByte(line, '\n'); i >= 0 {
- line, mod = line[:i], line[i+1:]
- }
- if i := bytes.Index(line, slashSlash); i >= 0 {
- line = line[:i]
- }
- line = bytes.TrimSpace(line)
- if !bytes.HasPrefix(line, moduleStr) {
- continue
- }
- line = line[len(moduleStr):]
- n := len(line)
- line = bytes.TrimSpace(line)
- if len(line) == n || len(line) == 0 {
- continue
- }
- if line[0] == '"' || line[0] == '`' {
- p, err := strconv.Unquote(string(line))
- if err != nil {
- return "" // malformed quoted string or multiline module path
- }
- return p
- }
- return string(line)
- }
- return "" // missing module path
- }
|