github.com/jd-ly/tools@v0.5.7/internal/imports/mod.go (about) 1 package imports 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 12 "regexp" 13 "sort" 14 "strconv" 15 "strings" 16 17 "golang.org/x/mod/module" 18 "github.com/jd-ly/tools/internal/gocommand" 19 "github.com/jd-ly/tools/internal/gopathwalk" 20 ) 21 22 // ModuleResolver implements resolver for modules using the go command as little 23 // as feasible. 24 type ModuleResolver struct { 25 env *ProcessEnv 26 moduleCacheDir string 27 dummyVendorMod *gocommand.ModuleJSON // If vendoring is enabled, the pseudo-module that represents the /vendor directory. 28 roots []gopathwalk.Root 29 scanSema chan struct{} // scanSema prevents concurrent scans and guards scannedRoots. 30 scannedRoots map[gopathwalk.Root]bool 31 32 initialized bool 33 main *gocommand.ModuleJSON 34 modsByModPath []*gocommand.ModuleJSON // All modules, ordered by # of path components in module Path... 35 modsByDir []*gocommand.ModuleJSON // ...or Dir. 36 37 // moduleCacheCache stores information about the module cache. 38 moduleCacheCache *dirInfoCache 39 otherCache *dirInfoCache 40 } 41 42 func newModuleResolver(e *ProcessEnv) *ModuleResolver { 43 r := &ModuleResolver{ 44 env: e, 45 scanSema: make(chan struct{}, 1), 46 } 47 r.scanSema <- struct{}{} 48 return r 49 } 50 51 func (r *ModuleResolver) init() error { 52 if r.initialized { 53 return nil 54 } 55 56 goenv, err := r.env.goEnv() 57 if err != nil { 58 return err 59 } 60 inv := gocommand.Invocation{ 61 BuildFlags: r.env.BuildFlags, 62 ModFlag: r.env.ModFlag, 63 ModFile: r.env.ModFile, 64 Env: r.env.env(), 65 Logf: r.env.Logf, 66 WorkingDir: r.env.WorkingDir, 67 } 68 mainMod, vendorEnabled, err := gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner) 69 if err != nil { 70 return err 71 } 72 73 if mainMod != nil && vendorEnabled { 74 // Vendor mode is on, so all the non-Main modules are irrelevant, 75 // and we need to search /vendor for everything. 76 r.main = mainMod 77 r.dummyVendorMod = &gocommand.ModuleJSON{ 78 Path: "", 79 Dir: filepath.Join(mainMod.Dir, "vendor"), 80 } 81 r.modsByModPath = []*gocommand.ModuleJSON{mainMod, r.dummyVendorMod} 82 r.modsByDir = []*gocommand.ModuleJSON{mainMod, r.dummyVendorMod} 83 } else { 84 // Vendor mode is off, so run go list -m ... to find everything. 85 r.initAllMods() 86 } 87 88 if gmc := r.env.Env["GOMODCACHE"]; gmc != "" { 89 r.moduleCacheDir = gmc 90 } else { 91 r.moduleCacheDir = filepath.Join(filepath.SplitList(goenv["GOPATH"])[0], "/pkg/mod") 92 } 93 94 sort.Slice(r.modsByModPath, func(i, j int) bool { 95 count := func(x int) int { 96 return strings.Count(r.modsByModPath[x].Path, "/") 97 } 98 return count(j) < count(i) // descending order 99 }) 100 sort.Slice(r.modsByDir, func(i, j int) bool { 101 count := func(x int) int { 102 return strings.Count(r.modsByDir[x].Dir, "/") 103 } 104 return count(j) < count(i) // descending order 105 }) 106 107 r.roots = []gopathwalk.Root{ 108 {filepath.Join(goenv["GOROOT"], "/src"), gopathwalk.RootGOROOT}, 109 } 110 if r.main != nil { 111 r.roots = append(r.roots, gopathwalk.Root{r.main.Dir, gopathwalk.RootCurrentModule}) 112 } 113 if vendorEnabled { 114 r.roots = append(r.roots, gopathwalk.Root{r.dummyVendorMod.Dir, gopathwalk.RootOther}) 115 } else { 116 addDep := func(mod *gocommand.ModuleJSON) { 117 if mod.Replace == nil { 118 // This is redundant with the cache, but we'll skip it cheaply enough. 119 r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootModuleCache}) 120 } else { 121 r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootOther}) 122 } 123 } 124 // Walk dependent modules before scanning the full mod cache, direct deps first. 125 for _, mod := range r.modsByModPath { 126 if !mod.Indirect && !mod.Main { 127 addDep(mod) 128 } 129 } 130 for _, mod := range r.modsByModPath { 131 if mod.Indirect && !mod.Main { 132 addDep(mod) 133 } 134 } 135 r.roots = append(r.roots, gopathwalk.Root{r.moduleCacheDir, gopathwalk.RootModuleCache}) 136 } 137 138 r.scannedRoots = map[gopathwalk.Root]bool{} 139 if r.moduleCacheCache == nil { 140 r.moduleCacheCache = &dirInfoCache{ 141 dirs: map[string]*directoryPackageInfo{}, 142 listeners: map[*int]cacheListener{}, 143 } 144 } 145 if r.otherCache == nil { 146 r.otherCache = &dirInfoCache{ 147 dirs: map[string]*directoryPackageInfo{}, 148 listeners: map[*int]cacheListener{}, 149 } 150 } 151 r.initialized = true 152 return nil 153 } 154 155 func (r *ModuleResolver) initAllMods() error { 156 stdout, err := r.env.invokeGo(context.TODO(), "list", "-m", "-json", "...") 157 if err != nil { 158 return err 159 } 160 for dec := json.NewDecoder(stdout); dec.More(); { 161 mod := &gocommand.ModuleJSON{} 162 if err := dec.Decode(mod); err != nil { 163 return err 164 } 165 if mod.Dir == "" { 166 if r.env.Logf != nil { 167 r.env.Logf("module %v has not been downloaded and will be ignored", mod.Path) 168 } 169 // Can't do anything with a module that's not downloaded. 170 continue 171 } 172 // golang/go#36193: the go command doesn't always clean paths. 173 mod.Dir = filepath.Clean(mod.Dir) 174 r.modsByModPath = append(r.modsByModPath, mod) 175 r.modsByDir = append(r.modsByDir, mod) 176 if mod.Main { 177 r.main = mod 178 } 179 } 180 return nil 181 } 182 183 func (r *ModuleResolver) ClearForNewScan() { 184 <-r.scanSema 185 r.scannedRoots = map[gopathwalk.Root]bool{} 186 r.otherCache = &dirInfoCache{ 187 dirs: map[string]*directoryPackageInfo{}, 188 listeners: map[*int]cacheListener{}, 189 } 190 r.scanSema <- struct{}{} 191 } 192 193 func (r *ModuleResolver) ClearForNewMod() { 194 <-r.scanSema 195 *r = ModuleResolver{ 196 env: r.env, 197 moduleCacheCache: r.moduleCacheCache, 198 otherCache: r.otherCache, 199 scanSema: r.scanSema, 200 } 201 r.init() 202 r.scanSema <- struct{}{} 203 } 204 205 // findPackage returns the module and directory that contains the package at 206 // the given import path, or returns nil, "" if no module is in scope. 207 func (r *ModuleResolver) findPackage(importPath string) (*gocommand.ModuleJSON, string) { 208 // This can't find packages in the stdlib, but that's harmless for all 209 // the existing code paths. 210 for _, m := range r.modsByModPath { 211 if !strings.HasPrefix(importPath, m.Path) { 212 continue 213 } 214 pathInModule := importPath[len(m.Path):] 215 pkgDir := filepath.Join(m.Dir, pathInModule) 216 if r.dirIsNestedModule(pkgDir, m) { 217 continue 218 } 219 220 if info, ok := r.cacheLoad(pkgDir); ok { 221 if loaded, err := info.reachedStatus(nameLoaded); loaded { 222 if err != nil { 223 continue // No package in this dir. 224 } 225 return m, pkgDir 226 } 227 if scanned, err := info.reachedStatus(directoryScanned); scanned && err != nil { 228 continue // Dir is unreadable, etc. 229 } 230 // This is slightly wrong: a directory doesn't have to have an 231 // importable package to count as a package for package-to-module 232 // resolution. package main or _test files should count but 233 // don't. 234 // TODO(heschi): fix this. 235 if _, err := r.cachePackageName(info); err == nil { 236 return m, pkgDir 237 } 238 } 239 240 // Not cached. Read the filesystem. 241 pkgFiles, err := ioutil.ReadDir(pkgDir) 242 if err != nil { 243 continue 244 } 245 // A module only contains a package if it has buildable go 246 // files in that directory. If not, it could be provided by an 247 // outer module. See #29736. 248 for _, fi := range pkgFiles { 249 if ok, _ := r.env.matchFile(pkgDir, fi.Name()); ok { 250 return m, pkgDir 251 } 252 } 253 } 254 return nil, "" 255 } 256 257 func (r *ModuleResolver) cacheLoad(dir string) (directoryPackageInfo, bool) { 258 if info, ok := r.moduleCacheCache.Load(dir); ok { 259 return info, ok 260 } 261 return r.otherCache.Load(dir) 262 } 263 264 func (r *ModuleResolver) cacheStore(info directoryPackageInfo) { 265 if info.rootType == gopathwalk.RootModuleCache { 266 r.moduleCacheCache.Store(info.dir, info) 267 } else { 268 r.otherCache.Store(info.dir, info) 269 } 270 } 271 272 func (r *ModuleResolver) cacheKeys() []string { 273 return append(r.moduleCacheCache.Keys(), r.otherCache.Keys()...) 274 } 275 276 // cachePackageName caches the package name for a dir already in the cache. 277 func (r *ModuleResolver) cachePackageName(info directoryPackageInfo) (string, error) { 278 if info.rootType == gopathwalk.RootModuleCache { 279 return r.moduleCacheCache.CachePackageName(info) 280 } 281 return r.otherCache.CachePackageName(info) 282 } 283 284 func (r *ModuleResolver) cacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) { 285 if info.rootType == gopathwalk.RootModuleCache { 286 return r.moduleCacheCache.CacheExports(ctx, env, info) 287 } 288 return r.otherCache.CacheExports(ctx, env, info) 289 } 290 291 // findModuleByDir returns the module that contains dir, or nil if no such 292 // module is in scope. 293 func (r *ModuleResolver) findModuleByDir(dir string) *gocommand.ModuleJSON { 294 // This is quite tricky and may not be correct. dir could be: 295 // - a package in the main module. 296 // - a replace target underneath the main module's directory. 297 // - a nested module in the above. 298 // - a replace target somewhere totally random. 299 // - a nested module in the above. 300 // - in the mod cache. 301 // - in /vendor/ in -mod=vendor mode. 302 // - nested module? Dunno. 303 // Rumor has it that replace targets cannot contain other replace targets. 304 for _, m := range r.modsByDir { 305 if !strings.HasPrefix(dir, m.Dir) { 306 continue 307 } 308 309 if r.dirIsNestedModule(dir, m) { 310 continue 311 } 312 313 return m 314 } 315 return nil 316 } 317 318 // dirIsNestedModule reports if dir is contained in a nested module underneath 319 // mod, not actually in mod. 320 func (r *ModuleResolver) dirIsNestedModule(dir string, mod *gocommand.ModuleJSON) bool { 321 if !strings.HasPrefix(dir, mod.Dir) { 322 return false 323 } 324 if r.dirInModuleCache(dir) { 325 // Nested modules in the module cache are pruned, 326 // so it cannot be a nested module. 327 return false 328 } 329 if mod != nil && mod == r.dummyVendorMod { 330 // The /vendor pseudomodule is flattened and doesn't actually count. 331 return false 332 } 333 modDir, _ := r.modInfo(dir) 334 if modDir == "" { 335 return false 336 } 337 return modDir != mod.Dir 338 } 339 340 func (r *ModuleResolver) modInfo(dir string) (modDir string, modName string) { 341 readModName := func(modFile string) string { 342 modBytes, err := ioutil.ReadFile(modFile) 343 if err != nil { 344 return "" 345 } 346 return modulePath(modBytes) 347 } 348 349 if r.dirInModuleCache(dir) { 350 if matches := modCacheRegexp.FindStringSubmatch(dir); len(matches) == 3 { 351 index := strings.Index(dir, matches[1]+"@"+matches[2]) 352 modDir := filepath.Join(dir[:index], matches[1]+"@"+matches[2]) 353 return modDir, readModName(filepath.Join(modDir, "go.mod")) 354 } 355 } 356 for { 357 if info, ok := r.cacheLoad(dir); ok { 358 return info.moduleDir, info.moduleName 359 } 360 f := filepath.Join(dir, "go.mod") 361 info, err := os.Stat(f) 362 if err == nil && !info.IsDir() { 363 return dir, readModName(f) 364 } 365 366 d := filepath.Dir(dir) 367 if len(d) >= len(dir) { 368 return "", "" // reached top of file system, no go.mod 369 } 370 dir = d 371 } 372 } 373 374 func (r *ModuleResolver) dirInModuleCache(dir string) bool { 375 if r.moduleCacheDir == "" { 376 return false 377 } 378 return strings.HasPrefix(dir, r.moduleCacheDir) 379 } 380 381 func (r *ModuleResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) { 382 if err := r.init(); err != nil { 383 return nil, err 384 } 385 names := map[string]string{} 386 for _, path := range importPaths { 387 _, packageDir := r.findPackage(path) 388 if packageDir == "" { 389 continue 390 } 391 name, err := packageDirToName(packageDir) 392 if err != nil { 393 continue 394 } 395 names[path] = name 396 } 397 return names, nil 398 } 399 400 func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error { 401 if err := r.init(); err != nil { 402 return err 403 } 404 405 processDir := func(info directoryPackageInfo) { 406 // Skip this directory if we were not able to get the package information successfully. 407 if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil { 408 return 409 } 410 pkg, err := r.canonicalize(info) 411 if err != nil { 412 return 413 } 414 415 if !callback.dirFound(pkg) { 416 return 417 } 418 pkg.packageName, err = r.cachePackageName(info) 419 if err != nil { 420 return 421 } 422 423 if !callback.packageNameLoaded(pkg) { 424 return 425 } 426 _, exports, err := r.loadExports(ctx, pkg, false) 427 if err != nil { 428 return 429 } 430 callback.exportsLoaded(pkg, exports) 431 } 432 433 // Start processing everything in the cache, and listen for the new stuff 434 // we discover in the walk below. 435 stop1 := r.moduleCacheCache.ScanAndListen(ctx, processDir) 436 defer stop1() 437 stop2 := r.otherCache.ScanAndListen(ctx, processDir) 438 defer stop2() 439 440 // We assume cached directories are fully cached, including all their 441 // children, and have not changed. We can skip them. 442 skip := func(root gopathwalk.Root, dir string) bool { 443 info, ok := r.cacheLoad(dir) 444 if !ok { 445 return false 446 } 447 // This directory can be skipped as long as we have already scanned it. 448 // Packages with errors will continue to have errors, so there is no need 449 // to rescan them. 450 packageScanned, _ := info.reachedStatus(directoryScanned) 451 return packageScanned 452 } 453 454 // Add anything new to the cache, and process it if we're still listening. 455 add := func(root gopathwalk.Root, dir string) { 456 r.cacheStore(r.scanDirForPackage(root, dir)) 457 } 458 459 // r.roots and the callback are not necessarily safe to use in the 460 // goroutine below. Process them eagerly. 461 roots := filterRoots(r.roots, callback.rootFound) 462 // We can't cancel walks, because we need them to finish to have a usable 463 // cache. Instead, run them in a separate goroutine and detach. 464 scanDone := make(chan struct{}) 465 go func() { 466 select { 467 case <-ctx.Done(): 468 return 469 case <-r.scanSema: 470 } 471 defer func() { r.scanSema <- struct{}{} }() 472 // We have the lock on r.scannedRoots, and no other scans can run. 473 for _, root := range roots { 474 if ctx.Err() != nil { 475 return 476 } 477 478 if r.scannedRoots[root] { 479 continue 480 } 481 gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: r.env.Logf, ModulesEnabled: true}) 482 r.scannedRoots[root] = true 483 } 484 close(scanDone) 485 }() 486 select { 487 case <-ctx.Done(): 488 case <-scanDone: 489 } 490 return nil 491 } 492 493 func (r *ModuleResolver) scoreImportPath(ctx context.Context, path string) float64 { 494 if _, ok := stdlib[path]; ok { 495 return MaxRelevance 496 } 497 mod, _ := r.findPackage(path) 498 return modRelevance(mod) 499 } 500 501 func modRelevance(mod *gocommand.ModuleJSON) float64 { 502 var relevance float64 503 switch { 504 case mod == nil: // out of scope 505 return MaxRelevance - 4 506 case mod.Indirect: 507 relevance = MaxRelevance - 3 508 case !mod.Main: 509 relevance = MaxRelevance - 2 510 default: 511 relevance = MaxRelevance - 1 // main module ties with stdlib 512 } 513 514 _, versionString, ok := module.SplitPathVersion(mod.Path) 515 if ok { 516 index := strings.Index(versionString, "v") 517 if index == -1 { 518 return relevance 519 } 520 if versionNumber, err := strconv.ParseFloat(versionString[index+1:], 64); err == nil { 521 relevance += versionNumber / 1000 522 } 523 } 524 525 return relevance 526 } 527 528 // canonicalize gets the result of canonicalizing the packages using the results 529 // of initializing the resolver from 'go list -m'. 530 func (r *ModuleResolver) canonicalize(info directoryPackageInfo) (*pkg, error) { 531 // Packages in GOROOT are already canonical, regardless of the std/cmd modules. 532 if info.rootType == gopathwalk.RootGOROOT { 533 return &pkg{ 534 importPathShort: info.nonCanonicalImportPath, 535 dir: info.dir, 536 packageName: path.Base(info.nonCanonicalImportPath), 537 relevance: MaxRelevance, 538 }, nil 539 } 540 541 importPath := info.nonCanonicalImportPath 542 mod := r.findModuleByDir(info.dir) 543 // Check if the directory is underneath a module that's in scope. 544 if mod != nil { 545 // It is. If dir is the target of a replace directive, 546 // our guessed import path is wrong. Use the real one. 547 if mod.Dir == info.dir { 548 importPath = mod.Path 549 } else { 550 dirInMod := info.dir[len(mod.Dir)+len("/"):] 551 importPath = path.Join(mod.Path, filepath.ToSlash(dirInMod)) 552 } 553 } else if !strings.HasPrefix(importPath, info.moduleName) { 554 // The module's name doesn't match the package's import path. It 555 // probably needs a replace directive we don't have. 556 return nil, fmt.Errorf("package in %q is not valid without a replace statement", info.dir) 557 } 558 559 res := &pkg{ 560 importPathShort: importPath, 561 dir: info.dir, 562 relevance: modRelevance(mod), 563 } 564 // We may have discovered a package that has a different version 565 // in scope already. Canonicalize to that one if possible. 566 if _, canonicalDir := r.findPackage(importPath); canonicalDir != "" { 567 res.dir = canonicalDir 568 } 569 return res, nil 570 } 571 572 func (r *ModuleResolver) loadExports(ctx context.Context, pkg *pkg, includeTest bool) (string, []string, error) { 573 if err := r.init(); err != nil { 574 return "", nil, err 575 } 576 if info, ok := r.cacheLoad(pkg.dir); ok && !includeTest { 577 return r.cacheExports(ctx, r.env, info) 578 } 579 return loadExportsFromFiles(ctx, r.env, pkg.dir, includeTest) 580 } 581 582 func (r *ModuleResolver) scanDirForPackage(root gopathwalk.Root, dir string) directoryPackageInfo { 583 subdir := "" 584 if dir != root.Path { 585 subdir = dir[len(root.Path)+len("/"):] 586 } 587 importPath := filepath.ToSlash(subdir) 588 if strings.HasPrefix(importPath, "vendor/") { 589 // Only enter vendor directories if they're explicitly requested as a root. 590 return directoryPackageInfo{ 591 status: directoryScanned, 592 err: fmt.Errorf("unwanted vendor directory"), 593 } 594 } 595 switch root.Type { 596 case gopathwalk.RootCurrentModule: 597 importPath = path.Join(r.main.Path, filepath.ToSlash(subdir)) 598 case gopathwalk.RootModuleCache: 599 matches := modCacheRegexp.FindStringSubmatch(subdir) 600 if len(matches) == 0 { 601 return directoryPackageInfo{ 602 status: directoryScanned, 603 err: fmt.Errorf("invalid module cache path: %v", subdir), 604 } 605 } 606 modPath, err := module.UnescapePath(filepath.ToSlash(matches[1])) 607 if err != nil { 608 if r.env.Logf != nil { 609 r.env.Logf("decoding module cache path %q: %v", subdir, err) 610 } 611 return directoryPackageInfo{ 612 status: directoryScanned, 613 err: fmt.Errorf("decoding module cache path %q: %v", subdir, err), 614 } 615 } 616 importPath = path.Join(modPath, filepath.ToSlash(matches[3])) 617 } 618 619 modDir, modName := r.modInfo(dir) 620 result := directoryPackageInfo{ 621 status: directoryScanned, 622 dir: dir, 623 rootType: root.Type, 624 nonCanonicalImportPath: importPath, 625 moduleDir: modDir, 626 moduleName: modName, 627 } 628 if root.Type == gopathwalk.RootGOROOT { 629 // stdlib packages are always in scope, despite the confusing go.mod 630 return result 631 } 632 return result 633 } 634 635 // modCacheRegexp splits a path in a module cache into module, module version, and package. 636 var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) 637 638 var ( 639 slashSlash = []byte("//") 640 moduleStr = []byte("module") 641 ) 642 643 // modulePath returns the module path from the gomod file text. 644 // If it cannot find a module path, it returns an empty string. 645 // It is tolerant of unrelated problems in the go.mod file. 646 // 647 // Copied from cmd/go/internal/modfile. 648 func modulePath(mod []byte) string { 649 for len(mod) > 0 { 650 line := mod 651 mod = nil 652 if i := bytes.IndexByte(line, '\n'); i >= 0 { 653 line, mod = line[:i], line[i+1:] 654 } 655 if i := bytes.Index(line, slashSlash); i >= 0 { 656 line = line[:i] 657 } 658 line = bytes.TrimSpace(line) 659 if !bytes.HasPrefix(line, moduleStr) { 660 continue 661 } 662 line = line[len(moduleStr):] 663 n := len(line) 664 line = bytes.TrimSpace(line) 665 if len(line) == n || len(line) == 0 { 666 continue 667 } 668 669 if line[0] == '"' || line[0] == '`' { 670 p, err := strconv.Unquote(string(line)) 671 if err != nil { 672 return "" // malformed quoted string or multiline module path 673 } 674 return p 675 } 676 677 return string(line) 678 } 679 return "" // missing module path 680 }