8 "mvdan.cc/gofumpt/gofumports/internal/gopathwalk"
11 // To find packages to import, the resolver needs to know about all of the
12 // the packages that could be imported. This includes packages that are
13 // already in modules that are in (1) the current module, (2) replace targets,
14 // and (3) packages in the module cache. Packages in (1) and (2) may change over
15 // time, as the client may edit the current module and locally replaced modules.
16 // The module cache (which includes all of the packages in (3)) can only
19 // The resolver can thus save state about packages in the module cache
20 // and guarantee that this will not change over time. To obtain information
21 // about new modules added to the module cache, the module cache should be
24 // It is OK to serve information about modules that have been deleted,
25 // as they do still exist.
26 // TODO(suzmue): can we share information with the caller about
27 // what module needs to be downloaded to import this package?
29 type directoryPackageStatus int
32 _ directoryPackageStatus = iota
38 type directoryPackageInfo struct {
39 // status indicates the extent to which this struct has been filled in.
40 status directoryPackageStatus
41 // err is non-nil when there was an error trying to reach status.
44 // Set when status >= directoryScanned.
46 // dir is the absolute directory of this package.
48 rootType gopathwalk.RootType
49 // nonCanonicalImportPath is the package's expected import path. It may
50 // not actually be importable at that path.
51 nonCanonicalImportPath string
53 // Module-related information.
54 moduleDir string // The directory that is the module root of this dir.
55 moduleName string // The module name that contains this dir.
57 // Set when status >= nameLoaded.
59 packageName string // the package name, as declared in the source.
61 // Set when status >= exportsLoaded.
66 // reachedStatus returns true when info has a status at least target and any error associated with
67 // an attempt to reach target.
68 func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) {
70 return info.status >= target, nil
72 if info.status == target {
78 // dirInfoCache is a concurrency safe map for storing information about
79 // directories that may contain packages.
81 // The information in this cache is built incrementally. Entries are initialized in scan.
82 // No new keys should be added in any other functions, as all directories containing
83 // packages are identified in scan.
85 // Other functions, including loadExports and findPackage, may update entries in this cache
86 // as they discover new things about the directory.
88 // The information in the cache is not expected to change for the cache's
89 // lifetime, so there is no protection against competing writes. Users should
90 // take care not to hold the cache across changes to the underlying files.
92 // TODO(suzmue): consider other concurrency strategies and data structures (RWLocks, sync.Map, etc)
93 type dirInfoCache struct {
95 // dirs stores information about packages in directories, keyed by absolute path.
96 dirs map[string]*directoryPackageInfo
97 listeners map[*int]cacheListener
100 type cacheListener func(directoryPackageInfo)
102 // ScanAndListen calls listener on all the items in the cache, and on anything
103 // newly added. The returned stop function waits for all in-flight callbacks to
104 // finish and blocks new ones.
105 func (d *dirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() {
106 ctx, cancel := context.WithCancel(ctx)
108 // Flushing out all the callbacks is tricky without knowing how many there
109 // are going to be. Setting an arbitrary limit makes it much easier.
110 const maxInFlight = 10
111 sema := make(chan struct{}, maxInFlight)
112 for i := 0; i < maxInFlight; i++ {
116 cookie := new(int) // A unique ID we can use for the listener.
118 // We can't hold mu while calling the listener.
121 for key := range d.dirs {
122 keys = append(keys, key)
124 d.listeners[cookie] = func(info directoryPackageInfo) {
138 delete(d.listeners, cookie)
140 for i := 0; i < maxInFlight; i++ {
145 // Process the pre-existing keys.
146 for _, k := range keys {
152 if v, ok := d.Load(k); ok {
160 // Store stores the package info for dir.
161 func (d *dirInfoCache) Store(dir string, info directoryPackageInfo) {
163 _, old := d.dirs[dir]
165 var listeners []cacheListener
166 for _, l := range d.listeners {
167 listeners = append(listeners, l)
172 for _, l := range listeners {
178 // Load returns a copy of the directoryPackageInfo for absolute directory dir.
179 func (d *dirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
182 info, ok := d.dirs[dir]
184 return directoryPackageInfo{}, false
189 // Keys returns the keys currently present in d.
190 func (d *dirInfoCache) Keys() (keys []string) {
193 for key := range d.dirs {
194 keys = append(keys, key)
199 func (d *dirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) {
200 if loaded, err := info.reachedStatus(nameLoaded); loaded {
201 return info.packageName, err
203 if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil {
204 return "", fmt.Errorf("cannot read package name, scan error: %v", err)
206 info.packageName, info.err = packageDirToName(info.dir)
207 info.status = nameLoaded
208 d.Store(info.dir, info)
209 return info.packageName, info.err
212 func (d *dirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) {
213 if reached, _ := info.reachedStatus(exportsLoaded); reached {
214 return info.packageName, info.exports, info.err
216 if reached, err := info.reachedStatus(nameLoaded); reached && err != nil {
219 info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false)
220 if info.err == context.Canceled || info.err == context.DeadlineExceeded {
221 return info.packageName, info.exports, info.err
223 // The cache structure wants things to proceed linearly. We can skip a
224 // step here, but only if we succeed.
225 if info.status == nameLoaded || info.err == nil {
226 info.status = exportsLoaded
228 info.status = nameLoaded
230 d.Store(info.dir, info)
231 return info.packageName, info.exports, info.err