332 lines
10 KiB
Go
332 lines
10 KiB
Go
// Copyright 2019 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 imports
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/mod/module"
|
|
"golang.org/x/tools/internal/gopathwalk"
|
|
"golang.org/x/tools/internal/stdlib"
|
|
)
|
|
|
|
// To find packages to import, the resolver needs to know about all of
|
|
// the packages that could be imported. This includes packages that are
|
|
// already in modules that are in (1) the current module, (2) replace targets,
|
|
// and (3) packages in the module cache. Packages in (1) and (2) may change over
|
|
// time, as the client may edit the current module and locally replaced modules.
|
|
// The module cache (which includes all of the packages in (3)) can only
|
|
// ever be added to.
|
|
//
|
|
// The resolver can thus save state about packages in the module cache
|
|
// and guarantee that this will not change over time. To obtain information
|
|
// about new modules added to the module cache, the module cache should be
|
|
// rescanned.
|
|
//
|
|
// It is OK to serve information about modules that have been deleted,
|
|
// as they do still exist.
|
|
// TODO(suzmue): can we share information with the caller about
|
|
// what module needs to be downloaded to import this package?
|
|
|
|
type directoryPackageStatus int
|
|
|
|
const (
|
|
_ directoryPackageStatus = iota
|
|
directoryScanned
|
|
nameLoaded
|
|
exportsLoaded
|
|
)
|
|
|
|
// directoryPackageInfo holds (possibly incomplete) information about packages
|
|
// contained in a given directory.
|
|
type directoryPackageInfo struct {
|
|
// status indicates the extent to which this struct has been filled in.
|
|
status directoryPackageStatus
|
|
// err is non-nil when there was an error trying to reach status.
|
|
err error
|
|
|
|
// Set when status >= directoryScanned.
|
|
|
|
// dir is the absolute directory of this package.
|
|
dir string
|
|
rootType gopathwalk.RootType
|
|
// nonCanonicalImportPath is the package's expected import path. It may
|
|
// not actually be importable at that path.
|
|
nonCanonicalImportPath string
|
|
|
|
// Module-related information.
|
|
moduleDir string // The directory that is the module root of this dir.
|
|
moduleName string // The module name that contains this dir.
|
|
|
|
// Set when status >= nameLoaded.
|
|
|
|
packageName string // the package name, as declared in the source.
|
|
|
|
// Set when status >= exportsLoaded.
|
|
// TODO(rfindley): it's hard to see this, but exports depend implicitly on
|
|
// the default build context GOOS and GOARCH.
|
|
//
|
|
// We can make this explicit, and key exports by GOOS, GOARCH.
|
|
exports []stdlib.Symbol
|
|
}
|
|
|
|
// reachedStatus returns true when info has a status at least target and any error associated with
|
|
// an attempt to reach target.
|
|
func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) {
|
|
if info.err == nil {
|
|
return info.status >= target, nil
|
|
}
|
|
if info.status == target {
|
|
return true, info.err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// DirInfoCache is a concurrency-safe map for storing information about
|
|
// directories that may contain packages.
|
|
//
|
|
// The information in this cache is built incrementally. Entries are initialized in scan.
|
|
// No new keys should be added in any other functions, as all directories containing
|
|
// packages are identified in scan.
|
|
//
|
|
// Other functions, including loadExports and findPackage, may update entries in this cache
|
|
// as they discover new things about the directory.
|
|
//
|
|
// The information in the cache is not expected to change for the cache's
|
|
// lifetime, so there is no protection against competing writes. Users should
|
|
// take care not to hold the cache across changes to the underlying files.
|
|
type DirInfoCache struct {
|
|
mu sync.Mutex
|
|
// dirs stores information about packages in directories, keyed by absolute path.
|
|
dirs map[string]*directoryPackageInfo
|
|
listeners map[*int]cacheListener
|
|
}
|
|
|
|
func NewDirInfoCache() *DirInfoCache {
|
|
return &DirInfoCache{
|
|
dirs: make(map[string]*directoryPackageInfo),
|
|
listeners: make(map[*int]cacheListener),
|
|
}
|
|
}
|
|
|
|
type cacheListener func(directoryPackageInfo)
|
|
|
|
// ScanAndListen calls listener on all the items in the cache, and on anything
|
|
// newly added. The returned stop function waits for all in-flight callbacks to
|
|
// finish and blocks new ones.
|
|
func (d *DirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// Flushing out all the callbacks is tricky without knowing how many there
|
|
// are going to be. Setting an arbitrary limit makes it much easier.
|
|
const maxInFlight = 10
|
|
sema := make(chan struct{}, maxInFlight)
|
|
for i := 0; i < maxInFlight; i++ {
|
|
sema <- struct{}{}
|
|
}
|
|
|
|
cookie := new(int) // A unique ID we can use for the listener.
|
|
|
|
// We can't hold mu while calling the listener.
|
|
d.mu.Lock()
|
|
var keys []string
|
|
for key := range d.dirs {
|
|
keys = append(keys, key)
|
|
}
|
|
d.listeners[cookie] = func(info directoryPackageInfo) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-sema:
|
|
}
|
|
listener(info)
|
|
sema <- struct{}{}
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
stop := func() {
|
|
cancel()
|
|
d.mu.Lock()
|
|
delete(d.listeners, cookie)
|
|
d.mu.Unlock()
|
|
for i := 0; i < maxInFlight; i++ {
|
|
<-sema
|
|
}
|
|
}
|
|
|
|
// Process the pre-existing keys.
|
|
for _, k := range keys {
|
|
select {
|
|
case <-ctx.Done():
|
|
return stop
|
|
default:
|
|
}
|
|
if v, ok := d.Load(k); ok {
|
|
listener(v)
|
|
}
|
|
}
|
|
|
|
return stop
|
|
}
|
|
|
|
// Store stores the package info for dir.
|
|
func (d *DirInfoCache) Store(dir string, info directoryPackageInfo) {
|
|
d.mu.Lock()
|
|
// TODO(rfindley, golang/go#59216): should we overwrite an existing entry?
|
|
// That seems incorrect as the cache should be idempotent.
|
|
_, old := d.dirs[dir]
|
|
d.dirs[dir] = &info
|
|
var listeners []cacheListener
|
|
for _, l := range d.listeners {
|
|
listeners = append(listeners, l)
|
|
}
|
|
d.mu.Unlock()
|
|
|
|
if !old {
|
|
for _, l := range listeners {
|
|
l(info)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load returns a copy of the directoryPackageInfo for absolute directory dir.
|
|
func (d *DirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
info, ok := d.dirs[dir]
|
|
if !ok {
|
|
return directoryPackageInfo{}, false
|
|
}
|
|
return *info, true
|
|
}
|
|
|
|
// Keys returns the keys currently present in d.
|
|
func (d *DirInfoCache) Keys() (keys []string) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
for key := range d.dirs {
|
|
keys = append(keys, key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (d *DirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) {
|
|
if loaded, err := info.reachedStatus(nameLoaded); loaded {
|
|
return info.packageName, err
|
|
}
|
|
if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil {
|
|
return "", fmt.Errorf("cannot read package name, scan error: %v", err)
|
|
}
|
|
info.packageName, info.err = packageDirToName(info.dir)
|
|
info.status = nameLoaded
|
|
d.Store(info.dir, info)
|
|
return info.packageName, info.err
|
|
}
|
|
|
|
func (d *DirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []stdlib.Symbol, error) {
|
|
if reached, _ := info.reachedStatus(exportsLoaded); reached {
|
|
return info.packageName, info.exports, info.err
|
|
}
|
|
if reached, err := info.reachedStatus(nameLoaded); reached && err != nil {
|
|
return "", nil, err
|
|
}
|
|
info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false)
|
|
if info.err == context.Canceled || info.err == context.DeadlineExceeded {
|
|
return info.packageName, info.exports, info.err
|
|
}
|
|
// The cache structure wants things to proceed linearly. We can skip a
|
|
// step here, but only if we succeed.
|
|
if info.status == nameLoaded || info.err == nil {
|
|
info.status = exportsLoaded
|
|
} else {
|
|
info.status = nameLoaded
|
|
}
|
|
d.Store(info.dir, info)
|
|
return info.packageName, info.exports, info.err
|
|
}
|
|
|
|
// ScanModuleCache walks the given directory, which must be a GOMODCACHE value,
|
|
// for directory package information, storing the results in cache.
|
|
func ScanModuleCache(dir string, cache *DirInfoCache, logf func(string, ...any)) {
|
|
// Note(rfindley): it's hard to see, but this function attempts to implement
|
|
// just the side effects on cache of calling PrimeCache with a ProcessEnv
|
|
// that has the given dir as its GOMODCACHE.
|
|
//
|
|
// Teasing out the control flow, we see that we can avoid any handling of
|
|
// vendor/ and can infer module info entirely from the path, simplifying the
|
|
// logic here.
|
|
|
|
root := gopathwalk.Root{
|
|
Path: filepath.Clean(dir),
|
|
Type: gopathwalk.RootModuleCache,
|
|
}
|
|
|
|
directoryInfo := func(root gopathwalk.Root, dir string) directoryPackageInfo {
|
|
// This is a copy of ModuleResolver.scanDirForPackage, trimmed down to
|
|
// logic that applies to a module cache directory.
|
|
|
|
subdir := ""
|
|
if dir != root.Path {
|
|
subdir = dir[len(root.Path)+len("/"):]
|
|
}
|
|
|
|
matches := modCacheRegexp.FindStringSubmatch(subdir)
|
|
if len(matches) == 0 {
|
|
return directoryPackageInfo{
|
|
status: directoryScanned,
|
|
err: fmt.Errorf("invalid module cache path: %v", subdir),
|
|
}
|
|
}
|
|
modPath, err := module.UnescapePath(filepath.ToSlash(matches[1]))
|
|
if err != nil {
|
|
if logf != nil {
|
|
logf("decoding module cache path %q: %v", subdir, err)
|
|
}
|
|
return directoryPackageInfo{
|
|
status: directoryScanned,
|
|
err: fmt.Errorf("decoding module cache path %q: %v", subdir, err),
|
|
}
|
|
}
|
|
importPath := path.Join(modPath, filepath.ToSlash(matches[3]))
|
|
index := strings.Index(dir, matches[1]+"@"+matches[2])
|
|
modDir := filepath.Join(dir[:index], matches[1]+"@"+matches[2])
|
|
modName := readModName(filepath.Join(modDir, "go.mod"))
|
|
return directoryPackageInfo{
|
|
status: directoryScanned,
|
|
dir: dir,
|
|
rootType: root.Type,
|
|
nonCanonicalImportPath: importPath,
|
|
moduleDir: modDir,
|
|
moduleName: modName,
|
|
}
|
|
}
|
|
|
|
add := func(root gopathwalk.Root, dir string) {
|
|
info := directoryInfo(root, dir)
|
|
cache.Store(info.dir, info)
|
|
}
|
|
|
|
skip := func(_ gopathwalk.Root, dir string) bool {
|
|
// Skip directories that have already been scanned.
|
|
//
|
|
// Note that gopathwalk only adds "package" directories, which must contain
|
|
// a .go file, and all such package directories in the module cache are
|
|
// immutable. So if we can load a dir, it can be skipped.
|
|
info, ok := cache.Load(dir)
|
|
if !ok {
|
|
return false
|
|
}
|
|
packageScanned, _ := info.reachedStatus(directoryScanned)
|
|
return packageScanned
|
|
}
|
|
|
|
gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: logf, ModulesEnabled: true})
|
|
}
|