1 | // Copyright 2019 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | package imports |
6 | |
7 | import ( |
8 | "context" |
9 | "fmt" |
10 | "sync" |
11 | |
12 | "golang.org/x/tools/internal/gopathwalk" |
13 | ) |
14 | |
15 | // To find packages to import, the resolver needs to know about all of the |
16 | // the packages that could be imported. This includes packages that are |
17 | // already in modules that are in (1) the current module, (2) replace targets, |
18 | // and (3) packages in the module cache. Packages in (1) and (2) may change over |
19 | // time, as the client may edit the current module and locally replaced modules. |
20 | // The module cache (which includes all of the packages in (3)) can only |
21 | // ever be added to. |
22 | // |
23 | // The resolver can thus save state about packages in the module cache |
24 | // and guarantee that this will not change over time. To obtain information |
25 | // about new modules added to the module cache, the module cache should be |
26 | // rescanned. |
27 | // |
28 | // It is OK to serve information about modules that have been deleted, |
29 | // as they do still exist. |
30 | // TODO(suzmue): can we share information with the caller about |
31 | // what module needs to be downloaded to import this package? |
32 | |
33 | type directoryPackageStatus int |
34 | |
35 | const ( |
36 | _ directoryPackageStatus = iota |
37 | directoryScanned |
38 | nameLoaded |
39 | exportsLoaded |
40 | ) |
41 | |
42 | type directoryPackageInfo struct { |
43 | // status indicates the extent to which this struct has been filled in. |
44 | status directoryPackageStatus |
45 | // err is non-nil when there was an error trying to reach status. |
46 | err error |
47 | |
48 | // Set when status >= directoryScanned. |
49 | |
50 | // dir is the absolute directory of this package. |
51 | dir string |
52 | rootType gopathwalk.RootType |
53 | // nonCanonicalImportPath is the package's expected import path. It may |
54 | // not actually be importable at that path. |
55 | nonCanonicalImportPath string |
56 | |
57 | // Module-related information. |
58 | moduleDir string // The directory that is the module root of this dir. |
59 | moduleName string // The module name that contains this dir. |
60 | |
61 | // Set when status >= nameLoaded. |
62 | |
63 | packageName string // the package name, as declared in the source. |
64 | |
65 | // Set when status >= exportsLoaded. |
66 | |
67 | exports []string |
68 | } |
69 | |
70 | // reachedStatus returns true when info has a status at least target and any error associated with |
71 | // an attempt to reach target. |
72 | func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) { |
73 | if info.err == nil { |
74 | return info.status >= target, nil |
75 | } |
76 | if info.status == target { |
77 | return true, info.err |
78 | } |
79 | return true, nil |
80 | } |
81 | |
82 | // dirInfoCache is a concurrency safe map for storing information about |
83 | // directories that may contain packages. |
84 | // |
85 | // The information in this cache is built incrementally. Entries are initialized in scan. |
86 | // No new keys should be added in any other functions, as all directories containing |
87 | // packages are identified in scan. |
88 | // |
89 | // Other functions, including loadExports and findPackage, may update entries in this cache |
90 | // as they discover new things about the directory. |
91 | // |
92 | // The information in the cache is not expected to change for the cache's |
93 | // lifetime, so there is no protection against competing writes. Users should |
94 | // take care not to hold the cache across changes to the underlying files. |
95 | // |
96 | // TODO(suzmue): consider other concurrency strategies and data structures (RWLocks, sync.Map, etc) |
97 | type dirInfoCache struct { |
98 | mu sync.Mutex |
99 | // dirs stores information about packages in directories, keyed by absolute path. |
100 | dirs map[string]*directoryPackageInfo |
101 | listeners map[*int]cacheListener |
102 | } |
103 | |
104 | type cacheListener func(directoryPackageInfo) |
105 | |
106 | // ScanAndListen calls listener on all the items in the cache, and on anything |
107 | // newly added. The returned stop function waits for all in-flight callbacks to |
108 | // finish and blocks new ones. |
109 | func (d *dirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() { |
110 | ctx, cancel := context.WithCancel(ctx) |
111 | |
112 | // Flushing out all the callbacks is tricky without knowing how many there |
113 | // are going to be. Setting an arbitrary limit makes it much easier. |
114 | const maxInFlight = 10 |
115 | sema := make(chan struct{}, maxInFlight) |
116 | for i := 0; i < maxInFlight; i++ { |
117 | sema <- struct{}{} |
118 | } |
119 | |
120 | cookie := new(int) // A unique ID we can use for the listener. |
121 | |
122 | // We can't hold mu while calling the listener. |
123 | d.mu.Lock() |
124 | var keys []string |
125 | for key := range d.dirs { |
126 | keys = append(keys, key) |
127 | } |
128 | d.listeners[cookie] = func(info directoryPackageInfo) { |
129 | select { |
130 | case <-ctx.Done(): |
131 | return |
132 | case <-sema: |
133 | } |
134 | listener(info) |
135 | sema <- struct{}{} |
136 | } |
137 | d.mu.Unlock() |
138 | |
139 | stop := func() { |
140 | cancel() |
141 | d.mu.Lock() |
142 | delete(d.listeners, cookie) |
143 | d.mu.Unlock() |
144 | for i := 0; i < maxInFlight; i++ { |
145 | <-sema |
146 | } |
147 | } |
148 | |
149 | // Process the pre-existing keys. |
150 | for _, k := range keys { |
151 | select { |
152 | case <-ctx.Done(): |
153 | return stop |
154 | default: |
155 | } |
156 | if v, ok := d.Load(k); ok { |
157 | listener(v) |
158 | } |
159 | } |
160 | |
161 | return stop |
162 | } |
163 | |
164 | // Store stores the package info for dir. |
165 | func (d *dirInfoCache) Store(dir string, info directoryPackageInfo) { |
166 | d.mu.Lock() |
167 | _, old := d.dirs[dir] |
168 | d.dirs[dir] = &info |
169 | var listeners []cacheListener |
170 | for _, l := range d.listeners { |
171 | listeners = append(listeners, l) |
172 | } |
173 | d.mu.Unlock() |
174 | |
175 | if !old { |
176 | for _, l := range listeners { |
177 | l(info) |
178 | } |
179 | } |
180 | } |
181 | |
182 | // Load returns a copy of the directoryPackageInfo for absolute directory dir. |
183 | func (d *dirInfoCache) Load(dir string) (directoryPackageInfo, bool) { |
184 | d.mu.Lock() |
185 | defer d.mu.Unlock() |
186 | info, ok := d.dirs[dir] |
187 | if !ok { |
188 | return directoryPackageInfo{}, false |
189 | } |
190 | return *info, true |
191 | } |
192 | |
193 | // Keys returns the keys currently present in d. |
194 | func (d *dirInfoCache) Keys() (keys []string) { |
195 | d.mu.Lock() |
196 | defer d.mu.Unlock() |
197 | for key := range d.dirs { |
198 | keys = append(keys, key) |
199 | } |
200 | return keys |
201 | } |
202 | |
203 | func (d *dirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) { |
204 | if loaded, err := info.reachedStatus(nameLoaded); loaded { |
205 | return info.packageName, err |
206 | } |
207 | if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil { |
208 | return "", fmt.Errorf("cannot read package name, scan error: %v", err) |
209 | } |
210 | info.packageName, info.err = packageDirToName(info.dir) |
211 | info.status = nameLoaded |
212 | d.Store(info.dir, info) |
213 | return info.packageName, info.err |
214 | } |
215 | |
216 | func (d *dirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) { |
217 | if reached, _ := info.reachedStatus(exportsLoaded); reached { |
218 | return info.packageName, info.exports, info.err |
219 | } |
220 | if reached, err := info.reachedStatus(nameLoaded); reached && err != nil { |
221 | return "", nil, err |
222 | } |
223 | info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false) |
224 | if info.err == context.Canceled || info.err == context.DeadlineExceeded { |
225 | return info.packageName, info.exports, info.err |
226 | } |
227 | // The cache structure wants things to proceed linearly. We can skip a |
228 | // step here, but only if we succeed. |
229 | if info.status == nameLoaded || info.err == nil { |
230 | info.status = exportsLoaded |
231 | } else { |
232 | info.status = nameLoaded |
233 | } |
234 | d.Store(info.dir, info) |
235 | return info.packageName, info.exports, info.err |
236 | } |
237 |
Members