1
2
3
4
5 package imports
6
7 import (
8 "context"
9 "fmt"
10 "path"
11 "path/filepath"
12 "strings"
13 "sync"
14
15 "golang.org/x/mod/module"
16 "golang.org/x/tools/internal/gopathwalk"
17 "golang.org/x/tools/internal/stdlib"
18 )
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 type directoryPackageStatus int
39
40 const (
41 _ directoryPackageStatus = iota
42 directoryScanned
43 nameLoaded
44 exportsLoaded
45 )
46
47
48
49 type directoryPackageInfo struct {
50
51 status directoryPackageStatus
52
53 err error
54
55
56
57
58 dir string
59 rootType gopathwalk.RootType
60
61
62 nonCanonicalImportPath string
63
64
65 moduleDir string
66 moduleName string
67
68
69
70 packageName string
71
72
73
74
75
76
77 exports []stdlib.Symbol
78 }
79
80
81
82 func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) {
83 if info.err == nil {
84 return info.status >= target, nil
85 }
86 if info.status == target {
87 return true, info.err
88 }
89 return true, nil
90 }
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105 type DirInfoCache struct {
106 mu sync.Mutex
107
108 dirs map[string]*directoryPackageInfo
109 listeners map[*int]cacheListener
110 }
111
112 func NewDirInfoCache() *DirInfoCache {
113 return &DirInfoCache{
114 dirs: make(map[string]*directoryPackageInfo),
115 listeners: make(map[*int]cacheListener),
116 }
117 }
118
119 type cacheListener func(directoryPackageInfo)
120
121
122
123
124 func (d *DirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() {
125 ctx, cancel := context.WithCancel(ctx)
126
127
128
129 const maxInFlight = 10
130 sema := make(chan struct{}, maxInFlight)
131 for i := 0; i < maxInFlight; i++ {
132 sema <- struct{}{}
133 }
134
135 cookie := new(int)
136
137
138 d.mu.Lock()
139 var keys []string
140 for key := range d.dirs {
141 keys = append(keys, key)
142 }
143 d.listeners[cookie] = func(info directoryPackageInfo) {
144 select {
145 case <-ctx.Done():
146 return
147 case <-sema:
148 }
149 listener(info)
150 sema <- struct{}{}
151 }
152 d.mu.Unlock()
153
154 stop := func() {
155 cancel()
156 d.mu.Lock()
157 delete(d.listeners, cookie)
158 d.mu.Unlock()
159 for i := 0; i < maxInFlight; i++ {
160 <-sema
161 }
162 }
163
164
165 for _, k := range keys {
166 select {
167 case <-ctx.Done():
168 return stop
169 default:
170 }
171 if v, ok := d.Load(k); ok {
172 listener(v)
173 }
174 }
175
176 return stop
177 }
178
179
180 func (d *DirInfoCache) Store(dir string, info directoryPackageInfo) {
181 d.mu.Lock()
182
183
184 _, old := d.dirs[dir]
185 d.dirs[dir] = &info
186 var listeners []cacheListener
187 for _, l := range d.listeners {
188 listeners = append(listeners, l)
189 }
190 d.mu.Unlock()
191
192 if !old {
193 for _, l := range listeners {
194 l(info)
195 }
196 }
197 }
198
199
200 func (d *DirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
201 d.mu.Lock()
202 defer d.mu.Unlock()
203 info, ok := d.dirs[dir]
204 if !ok {
205 return directoryPackageInfo{}, false
206 }
207 return *info, true
208 }
209
210
211 func (d *DirInfoCache) Keys() (keys []string) {
212 d.mu.Lock()
213 defer d.mu.Unlock()
214 for key := range d.dirs {
215 keys = append(keys, key)
216 }
217 return keys
218 }
219
220 func (d *DirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) {
221 if loaded, err := info.reachedStatus(nameLoaded); loaded {
222 return info.packageName, err
223 }
224 if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil {
225 return "", fmt.Errorf("cannot read package name, scan error: %v", err)
226 }
227 info.packageName, info.err = packageDirToName(info.dir)
228 info.status = nameLoaded
229 d.Store(info.dir, info)
230 return info.packageName, info.err
231 }
232
233 func (d *DirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []stdlib.Symbol, error) {
234 if reached, _ := info.reachedStatus(exportsLoaded); reached {
235 return info.packageName, info.exports, info.err
236 }
237 if reached, err := info.reachedStatus(nameLoaded); reached && err != nil {
238 return "", nil, err
239 }
240 info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false)
241 if info.err == context.Canceled || info.err == context.DeadlineExceeded {
242 return info.packageName, info.exports, info.err
243 }
244
245
246 if info.status == nameLoaded || info.err == nil {
247 info.status = exportsLoaded
248 } else {
249 info.status = nameLoaded
250 }
251 d.Store(info.dir, info)
252 return info.packageName, info.exports, info.err
253 }
254
255
256
257 func ScanModuleCache(dir string, cache *DirInfoCache, logf func(string, ...any)) {
258
259
260
261
262
263
264
265
266 root := gopathwalk.Root{
267 Path: filepath.Clean(dir),
268 Type: gopathwalk.RootModuleCache,
269 }
270
271 directoryInfo := func(root gopathwalk.Root, dir string) directoryPackageInfo {
272
273
274
275 subdir := ""
276 if dir != root.Path {
277 subdir = dir[len(root.Path)+len("/"):]
278 }
279
280 matches := modCacheRegexp.FindStringSubmatch(subdir)
281 if len(matches) == 0 {
282 return directoryPackageInfo{
283 status: directoryScanned,
284 err: fmt.Errorf("invalid module cache path: %v", subdir),
285 }
286 }
287 modPath, err := module.UnescapePath(filepath.ToSlash(matches[1]))
288 if err != nil {
289 if logf != nil {
290 logf("decoding module cache path %q: %v", subdir, err)
291 }
292 return directoryPackageInfo{
293 status: directoryScanned,
294 err: fmt.Errorf("decoding module cache path %q: %v", subdir, err),
295 }
296 }
297 importPath := path.Join(modPath, filepath.ToSlash(matches[3]))
298 index := strings.Index(dir, matches[1]+"@"+matches[2])
299 modDir := filepath.Join(dir[:index], matches[1]+"@"+matches[2])
300 modName := readModName(filepath.Join(modDir, "go.mod"))
301 return directoryPackageInfo{
302 status: directoryScanned,
303 dir: dir,
304 rootType: root.Type,
305 nonCanonicalImportPath: importPath,
306 moduleDir: modDir,
307 moduleName: modName,
308 }
309 }
310
311 add := func(root gopathwalk.Root, dir string) {
312 info := directoryInfo(root, dir)
313 cache.Store(info.dir, info)
314 }
315
316 skip := func(_ gopathwalk.Root, dir string) bool {
317
318
319
320
321
322 info, ok := cache.Load(dir)
323 if !ok {
324 return false
325 }
326 packageScanned, _ := info.reachedStatus(directoryScanned)
327 return packageScanned
328 }
329
330 gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: logf, ModulesEnabled: true})
331 }
332
View as plain text