.gitignore added
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.1.1-0.20210319172145-bda8f5cee399 / internal / lsp / cache / workspace.go
1 // Copyright 2020 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 cache
6
7 import (
8         "context"
9         "os"
10         "path/filepath"
11         "sort"
12         "strings"
13         "sync"
14
15         "golang.org/x/mod/modfile"
16         "golang.org/x/tools/internal/event"
17         "golang.org/x/tools/internal/lsp/source"
18         "golang.org/x/tools/internal/span"
19         "golang.org/x/tools/internal/xcontext"
20         errors "golang.org/x/xerrors"
21 )
22
23 type workspaceSource int
24
25 const (
26         legacyWorkspace = iota
27         goplsModWorkspace
28         fileSystemWorkspace
29 )
30
31 func (s workspaceSource) String() string {
32         switch s {
33         case legacyWorkspace:
34                 return "legacy"
35         case goplsModWorkspace:
36                 return "gopls.mod"
37         case fileSystemWorkspace:
38                 return "file system"
39         default:
40                 return "!(unknown module source)"
41         }
42 }
43
44 // workspace tracks go.mod files in the workspace, along with the
45 // gopls.mod file, to provide support for multi-module workspaces.
46 //
47 // Specifically, it provides:
48 //  - the set of modules contained within in the workspace root considered to
49 //    be 'active'
50 //  - the workspace modfile, to be used for the go command `-modfile` flag
51 //  - the set of workspace directories
52 //
53 // This type is immutable (or rather, idempotent), so that it may be shared
54 // across multiple snapshots.
55 type workspace struct {
56         root         span.URI
57         excludePath  func(string) bool
58         moduleSource workspaceSource
59
60         // activeModFiles holds the active go.mod files.
61         activeModFiles map[span.URI]struct{}
62
63         // knownModFiles holds the set of all go.mod files in the workspace.
64         // In all modes except for legacy, this is equivalent to modFiles.
65         knownModFiles map[span.URI]struct{}
66
67         // go111moduleOff indicates whether GO111MODULE=off has been configured in
68         // the environment.
69         go111moduleOff bool
70
71         // The workspace module is lazily re-built once after being invalidated.
72         // buildMu+built guards this reconstruction.
73         //
74         // file and wsDirs may be non-nil even if built == false, if they were copied
75         // from the previous workspace module version. In this case, they will be
76         // preserved if building fails.
77         buildMu  sync.Mutex
78         built    bool
79         buildErr error
80         mod      *modfile.File
81         sum      []byte
82         wsDirs   map[span.URI]struct{}
83 }
84
85 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, experimental bool) (*workspace, error) {
86         // In experimental mode, the user may have a gopls.mod file that defines
87         // their workspace.
88         if experimental {
89                 goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
90                 if err != nil {
91                         return nil, err
92                 }
93                 contents, err := goplsModFH.Read()
94                 if err == nil {
95                         file, activeModFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
96                         if err != nil {
97                                 return nil, err
98                         }
99                         return &workspace{
100                                 root:           root,
101                                 excludePath:    excludePath,
102                                 activeModFiles: activeModFiles,
103                                 knownModFiles:  activeModFiles,
104                                 mod:            file,
105                                 moduleSource:   goplsModWorkspace,
106                         }, nil
107                 }
108         }
109         // Otherwise, in all other modes, search for all of the go.mod files in the
110         // workspace.
111         knownModFiles, err := findModules(ctx, root, excludePath, 0)
112         if err != nil {
113                 return nil, err
114         }
115         // When GO111MODULE=off, there are no active go.mod files.
116         if go111moduleOff {
117                 return &workspace{
118                         root:           root,
119                         excludePath:    excludePath,
120                         moduleSource:   legacyWorkspace,
121                         knownModFiles:  knownModFiles,
122                         go111moduleOff: true,
123                 }, nil
124         }
125         // In legacy mode, not all known go.mod files will be considered active.
126         if !experimental {
127                 activeModFiles, err := getLegacyModules(ctx, root, fs)
128                 if err != nil {
129                         return nil, err
130                 }
131                 return &workspace{
132                         root:           root,
133                         excludePath:    excludePath,
134                         activeModFiles: activeModFiles,
135                         knownModFiles:  knownModFiles,
136                         moduleSource:   legacyWorkspace,
137                 }, nil
138         }
139         return &workspace{
140                 root:           root,
141                 excludePath:    excludePath,
142                 activeModFiles: knownModFiles,
143                 knownModFiles:  knownModFiles,
144                 moduleSource:   fileSystemWorkspace,
145         }, nil
146 }
147
148 func (w *workspace) getKnownModFiles() map[span.URI]struct{} {
149         return w.knownModFiles
150 }
151
152 func (w *workspace) getActiveModFiles() map[span.URI]struct{} {
153         return w.activeModFiles
154 }
155
156 // modFile gets the workspace modfile associated with this workspace,
157 // computing it if it doesn't exist.
158 //
159 // A fileSource must be passed in to solve a chicken-egg problem: it is not
160 // correct to pass in the snapshot file source to newWorkspace when
161 // invalidating, because at the time these are called the snapshot is locked.
162 // So we must pass it in later on when actually using the modFile.
163 func (w *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) {
164         w.build(ctx, fs)
165         return w.mod, w.buildErr
166 }
167
168 func (w *workspace) sumFile(ctx context.Context, fs source.FileSource) ([]byte, error) {
169         w.build(ctx, fs)
170         return w.sum, w.buildErr
171 }
172
173 func (w *workspace) build(ctx context.Context, fs source.FileSource) {
174         w.buildMu.Lock()
175         defer w.buildMu.Unlock()
176
177         if w.built {
178                 return
179         }
180         // Building should never be cancelled. Since the workspace module is shared
181         // across multiple snapshots, doing so would put us in a bad state, and it
182         // would not be obvious to the user how to recover.
183         ctx = xcontext.Detach(ctx)
184
185         // If our module source is not gopls.mod, try to build the workspace module
186         // from modules. Fall back on the pre-existing mod file if parsing fails.
187         if w.moduleSource != goplsModWorkspace {
188                 file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs)
189                 switch {
190                 case err == nil:
191                         w.mod = file
192                 case w.mod != nil:
193                         // Parsing failed, but we have a previous file version.
194                         event.Error(ctx, "building workspace mod file", err)
195                 default:
196                         // No file to fall back on.
197                         w.buildErr = err
198                 }
199         }
200         if w.mod != nil {
201                 w.wsDirs = map[span.URI]struct{}{
202                         w.root: {},
203                 }
204                 for _, r := range w.mod.Replace {
205                         // We may be replacing a module with a different version, not a path
206                         // on disk.
207                         if r.New.Version != "" {
208                                 continue
209                         }
210                         w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{}
211                 }
212         }
213         // Ensure that there is always at least the root dir.
214         if len(w.wsDirs) == 0 {
215                 w.wsDirs = map[span.URI]struct{}{
216                         w.root: {},
217                 }
218         }
219         sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs)
220         if err == nil {
221                 w.sum = sum
222         } else {
223                 event.Error(ctx, "building workspace sum file", err)
224         }
225         w.built = true
226 }
227
228 // dirs returns the workspace directories for the loaded modules.
229 func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI {
230         w.build(ctx, fs)
231         var dirs []span.URI
232         for d := range w.wsDirs {
233                 dirs = append(dirs, d)
234         }
235         sort.Slice(dirs, func(i, j int) bool {
236                 return source.CompareURI(dirs[i], dirs[j]) < 0
237         })
238         return dirs
239 }
240
241 // invalidate returns a (possibly) new workspace after invalidating the changed
242 // files. If w is still valid in the presence of changedURIs, it returns itself
243 // unmodified.
244 //
245 // The returned changed and reload flags control the level of invalidation.
246 // Some workspace changes may affect workspace contents without requiring a
247 // reload of metadata (for example, unsaved changes to a go.mod or go.sum
248 // file).
249 func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange) (_ *workspace, changed, reload bool) {
250         // Prevent races to w.modFile or w.wsDirs below, if wmhas not yet been built.
251         w.buildMu.Lock()
252         defer w.buildMu.Unlock()
253
254         // Clone the workspace. This may be discarded if nothing changed.
255         result := &workspace{
256                 root:           w.root,
257                 moduleSource:   w.moduleSource,
258                 knownModFiles:  make(map[span.URI]struct{}),
259                 activeModFiles: make(map[span.URI]struct{}),
260                 go111moduleOff: w.go111moduleOff,
261                 mod:            w.mod,
262                 sum:            w.sum,
263                 wsDirs:         w.wsDirs,
264         }
265         for k, v := range w.knownModFiles {
266                 result.knownModFiles[k] = v
267         }
268         for k, v := range w.activeModFiles {
269                 result.activeModFiles[k] = v
270         }
271
272         // First handle changes to the gopls.mod file. This must be considered before
273         // any changes to go.mod or go.sum files, as the gopls.mod file determines
274         // which modules we care about. In legacy workspace mode we don't consider
275         // the gopls.mod file.
276         if w.moduleSource != legacyWorkspace {
277                 // If gopls.mod has changed we need to either re-read it if it exists or
278                 // walk the filesystem if it has been deleted.
279                 gmURI := goplsModURI(w.root)
280                 if change, ok := changes[gmURI]; ok {
281                         if change.exists {
282                                 // Only invalidate if the gopls.mod actually parses.
283                                 // Otherwise, stick with the current gopls.mod.
284                                 parsedFile, parsedModules, err := parseGoplsMod(w.root, gmURI, change.content)
285                                 if err == nil {
286                                         changed = true
287                                         reload = change.fileHandle.Saved()
288                                         result.mod = parsedFile
289                                         result.moduleSource = goplsModWorkspace
290                                         result.knownModFiles = parsedModules
291                                         result.activeModFiles = make(map[span.URI]struct{})
292                                         for k, v := range parsedModules {
293                                                 result.activeModFiles[k] = v
294                                         }
295                                 } else {
296                                         // An unparseable gopls.mod file should not invalidate the
297                                         // workspace: nothing good could come from changing the
298                                         // workspace in this case.
299                                         event.Error(ctx, "parsing gopls.mod", err)
300                                 }
301                         } else {
302                                 // gopls.mod is deleted. search for modules again.
303                                 changed = true
304                                 reload = true
305                                 result.moduleSource = fileSystemWorkspace
306                                 // The parsed gopls.mod is no longer valid.
307                                 result.mod = nil
308                                 knownModFiles, err := findModules(ctx, w.root, w.excludePath, 0)
309                                 if err != nil {
310                                         result.knownModFiles = nil
311                                         result.activeModFiles = nil
312                                         event.Error(ctx, "finding file system modules", err)
313                                 } else {
314                                         result.knownModFiles = knownModFiles
315                                         result.activeModFiles = make(map[span.URI]struct{})
316                                         for k, v := range result.knownModFiles {
317                                                 result.activeModFiles[k] = v
318                                         }
319                                 }
320                         }
321                 }
322         }
323
324         // Next, handle go.mod changes that could affect our workspace. If we're
325         // reading our tracked modules from the gopls.mod, there's nothing to do
326         // here.
327         if result.moduleSource != goplsModWorkspace {
328                 for uri, change := range changes {
329                         if !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) {
330                                 continue
331                         }
332                         changed = true
333                         active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0
334                         reload = reload || (active && change.fileHandle.Saved())
335                         if change.exists {
336                                 result.knownModFiles[uri] = struct{}{}
337                                 if active {
338                                         result.activeModFiles[uri] = struct{}{}
339                                 }
340                         } else {
341                                 delete(result.knownModFiles, uri)
342                                 delete(result.activeModFiles, uri)
343                         }
344                 }
345         }
346
347         // Finally, process go.sum changes for any modules that are now active.
348         for uri, change := range changes {
349                 if !isGoSum(uri) {
350                         continue
351                 }
352                 // TODO(rFindley) factor out this URI mangling.
353                 dir := filepath.Dir(uri.Filename())
354                 modURI := span.URIFromPath(filepath.Join(dir, "go.mod"))
355                 if _, active := result.activeModFiles[modURI]; !active {
356                         continue
357                 }
358                 // Only changes to active go.sum files actually cause the workspace to
359                 // change.
360                 changed = true
361                 reload = reload || change.fileHandle.Saved()
362         }
363
364         if !changed {
365                 return w, false, false
366         }
367
368         return result, changed, reload
369 }
370
371 // goplsModURI returns the URI for the gopls.mod file contained in root.
372 func goplsModURI(root span.URI) span.URI {
373         return span.URIFromPath(filepath.Join(root.Filename(), "gopls.mod"))
374 }
375
376 // modURI returns the URI for the go.mod file contained in root.
377 func modURI(root span.URI) span.URI {
378         return span.URIFromPath(filepath.Join(root.Filename(), "go.mod"))
379 }
380
381 // isGoMod reports if uri is a go.mod file.
382 func isGoMod(uri span.URI) bool {
383         return filepath.Base(uri.Filename()) == "go.mod"
384 }
385
386 func isGoSum(uri span.URI) bool {
387         return filepath.Base(uri.Filename()) == "go.sum"
388 }
389
390 // fileExists reports if the file uri exists within source.
391 func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) {
392         fh, err := source.GetFile(ctx, uri)
393         if err != nil {
394                 return false, err
395         }
396         return fileHandleExists(fh)
397 }
398
399 // fileHandleExists reports if the file underlying fh actually exits.
400 func fileHandleExists(fh source.FileHandle) (bool, error) {
401         _, err := fh.Read()
402         if err == nil {
403                 return true, nil
404         }
405         if os.IsNotExist(err) {
406                 return false, nil
407         }
408         return false, err
409 }
410
411 // TODO(rFindley): replace this (and similar) with a uripath package analogous
412 // to filepath.
413 func dirURI(uri span.URI) span.URI {
414         return span.URIFromPath(filepath.Dir(uri.Filename()))
415 }
416
417 // getLegacyModules returns a module set containing at most the root module.
418 func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) {
419         uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod"))
420         modules := make(map[span.URI]struct{})
421         exists, err := fileExists(ctx, uri, fs)
422         if err != nil {
423                 return nil, err
424         }
425         if exists {
426                 modules[uri] = struct{}{}
427         }
428         return modules, nil
429 }
430
431 func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span.URI]struct{}, error) {
432         modFile, err := modfile.Parse(uri.Filename(), contents, nil)
433         if err != nil {
434                 return nil, nil, errors.Errorf("parsing gopls.mod: %w", err)
435         }
436         modFiles := make(map[span.URI]struct{})
437         for _, replace := range modFile.Replace {
438                 if replace.New.Version != "" {
439                         return nil, nil, errors.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version)
440                 }
441                 dirFP := filepath.FromSlash(replace.New.Path)
442                 if !filepath.IsAbs(dirFP) {
443                         dirFP = filepath.Join(root.Filename(), dirFP)
444                         // The resulting modfile must use absolute paths, so that it can be
445                         // written to a temp directory.
446                         replace.New.Path = dirFP
447                 }
448                 modURI := span.URIFromPath(filepath.Join(dirFP, "go.mod"))
449                 modFiles[modURI] = struct{}{}
450         }
451         return modFile, modFiles, nil
452 }
453
454 // errExhausted is returned by findModules if the file scan limit is reached.
455 var errExhausted = errors.New("exhausted")
456
457 // Limit go.mod search to 1 million files. As a point of reference,
458 // Kubernetes has 22K files (as of 2020-11-24).
459 const fileLimit = 1000000
460
461 // findModules recursively walks the root directory looking for go.mod files,
462 // returning the set of modules it discovers. If modLimit is non-zero,
463 // searching stops once modLimit modules have been found.
464 //
465 // TODO(rfindley): consider overlays.
466 func findModules(ctx context.Context, root span.URI, excludePath func(string) bool, modLimit int) (map[span.URI]struct{}, error) {
467         // Walk the view's folder to find all modules in the view.
468         modFiles := make(map[span.URI]struct{})
469         searched := 0
470         errDone := errors.New("done")
471         err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
472                 if err != nil {
473                         // Probably a permission error. Keep looking.
474                         return filepath.SkipDir
475                 }
476                 // For any path that is not the workspace folder, check if the path
477                 // would be ignored by the go command. Vendor directories also do not
478                 // contain workspace modules.
479                 if info.IsDir() && path != root.Filename() {
480                         suffix := strings.TrimPrefix(path, root.Filename())
481                         switch {
482                         case checkIgnored(suffix),
483                                 strings.Contains(filepath.ToSlash(suffix), "/vendor/"),
484                                 excludePath(suffix):
485                                 return filepath.SkipDir
486                         }
487                 }
488                 // We're only interested in go.mod files.
489                 uri := span.URIFromPath(path)
490                 if isGoMod(uri) {
491                         modFiles[uri] = struct{}{}
492                 }
493                 if modLimit > 0 && len(modFiles) >= modLimit {
494                         return errDone
495                 }
496                 searched++
497                 if fileLimit > 0 && searched >= fileLimit {
498                         return errExhausted
499                 }
500                 return nil
501         })
502         if err == errDone {
503                 return modFiles, nil
504         }
505         return modFiles, err
506 }