Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.0.0-20201105173854-bc9fc8d8c4bc / 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         moduleSource workspaceSource
58
59         // modFiles holds the active go.mod files.
60         modFiles map[span.URI]struct{}
61
62         // The workspace module is lazily re-built once after being invalidated.
63         // buildMu+built guards this reconstruction.
64         //
65         // file and wsDirs may be non-nil even if built == false, if they were copied
66         // from the previous workspace module version. In this case, they will be
67         // preserved if building fails.
68         buildMu  sync.Mutex
69         built    bool
70         buildErr error
71         file     *modfile.File
72         wsDirs   map[span.URI]struct{}
73 }
74
75 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, experimental bool) (*workspace, error) {
76         if !experimental {
77                 modFiles, err := getLegacyModules(ctx, root, fs)
78                 if err != nil {
79                         return nil, err
80                 }
81                 return &workspace{
82                         root:         root,
83                         modFiles:     modFiles,
84                         moduleSource: legacyWorkspace,
85                 }, nil
86         }
87         goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
88         if err != nil {
89                 return nil, err
90         }
91         contents, err := goplsModFH.Read()
92         if err == nil {
93                 file, modFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
94                 if err != nil {
95                         return nil, err
96                 }
97                 return &workspace{
98                         root:         root,
99                         modFiles:     modFiles,
100                         file:         file,
101                         moduleSource: goplsModWorkspace,
102                 }, nil
103         }
104         modFiles, err := findAllModules(ctx, root)
105         if err != nil {
106                 return nil, err
107         }
108         return &workspace{
109                 root:         root,
110                 modFiles:     modFiles,
111                 moduleSource: fileSystemWorkspace,
112         }, nil
113 }
114
115 func (wm *workspace) activeModFiles() map[span.URI]struct{} {
116         return wm.modFiles
117 }
118
119 // modFile gets the workspace modfile associated with this workspace,
120 // computing it if it doesn't exist.
121 //
122 // A fileSource must be passed in to solve a chicken-egg problem: it is not
123 // correct to pass in the snapshot file source to newWorkspace when
124 // invalidating, because at the time these are called the snapshot is locked.
125 // So we must pass it in later on when actually using the modFile.
126 func (wm *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) {
127         wm.build(ctx, fs)
128         return wm.file, wm.buildErr
129 }
130
131 func (wm *workspace) build(ctx context.Context, fs source.FileSource) {
132         wm.buildMu.Lock()
133         defer wm.buildMu.Unlock()
134
135         if wm.built {
136                 return
137         }
138         // Building should never be cancelled. Since the workspace module is shared
139         // across multiple snapshots, doing so would put us in a bad state, and it
140         // would not be obvious to the user how to recover.
141         ctx = xcontext.Detach(ctx)
142
143         // If our module source is not gopls.mod, try to build the workspace module
144         // from modules. Fall back on the pre-existing mod file if parsing fails.
145         if wm.moduleSource != goplsModWorkspace {
146                 file, err := buildWorkspaceModFile(ctx, wm.modFiles, fs)
147                 switch {
148                 case err == nil:
149                         wm.file = file
150                 case wm.file != nil:
151                         // Parsing failed, but we have a previous file version.
152                         event.Error(ctx, "building workspace mod file", err)
153                 default:
154                         // No file to fall back on.
155                         wm.buildErr = err
156                 }
157         }
158         if wm.file != nil {
159                 wm.wsDirs = map[span.URI]struct{}{
160                         wm.root: {},
161                 }
162                 for _, r := range wm.file.Replace {
163                         // We may be replacing a module with a different version, not a path
164                         // on disk.
165                         if r.New.Version != "" {
166                                 continue
167                         }
168                         wm.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{}
169                 }
170         }
171         // Ensure that there is always at least the root dir.
172         if len(wm.wsDirs) == 0 {
173                 wm.wsDirs = map[span.URI]struct{}{
174                         wm.root: {},
175                 }
176         }
177         wm.built = true
178 }
179
180 // dirs returns the workspace directories for the loaded modules.
181 func (wm *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI {
182         wm.build(ctx, fs)
183         var dirs []span.URI
184         for d := range wm.wsDirs {
185                 dirs = append(dirs, d)
186         }
187         sort.Slice(dirs, func(i, j int) bool {
188                 return span.CompareURI(dirs[i], dirs[j]) < 0
189         })
190         return dirs
191 }
192
193 // invalidate returns a (possibly) new workspaceModule after invalidating
194 // changedURIs. If wm is still valid in the presence of changedURIs, it returns
195 // itself unmodified.
196 func (wm *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange) (*workspace, bool) {
197         // Prevent races to wm.modFile or wm.wsDirs below, if wm has not yet been
198         // built.
199         wm.buildMu.Lock()
200         defer wm.buildMu.Unlock()
201         // Any gopls.mod change is processed first, followed by go.mod changes, as
202         // changes to gopls.mod may affect the set of active go.mod files.
203         var (
204                 // New values. We return a new workspace module if and only if modFiles is
205                 // non-nil.
206                 modFiles     map[span.URI]struct{}
207                 moduleSource = wm.moduleSource
208                 modFile      = wm.file
209                 err          error
210         )
211         if wm.moduleSource == goplsModWorkspace {
212                 // If we are currently reading the modfile from gopls.mod, we default to
213                 // preserving it even if module metadata changes (which may be the case if
214                 // a go.sum file changes).
215                 modFile = wm.file
216         }
217         // First handle changes to the gopls.mod file.
218         if wm.moduleSource != legacyWorkspace {
219                 // If gopls.mod has changed we need to either re-read it if it exists or
220                 // walk the filesystem if it doesn't exist.
221                 gmURI := goplsModURI(wm.root)
222                 if change, ok := changes[gmURI]; ok {
223                         if change.exists {
224                                 // Only invalidate if the gopls.mod actually parses. Otherwise, stick with the current gopls.mod
225                                 parsedFile, parsedModules, err := parseGoplsMod(wm.root, gmURI, change.content)
226                                 if err == nil {
227                                         modFile = parsedFile
228                                         moduleSource = goplsModWorkspace
229                                         modFiles = parsedModules
230                                 } else {
231                                         // Note that modFile is not invalidated here.
232                                         event.Error(ctx, "parsing gopls.mod", err)
233                                 }
234                         } else {
235                                 // gopls.mod is deleted. search for modules again.
236                                 moduleSource = fileSystemWorkspace
237                                 modFiles, err = findAllModules(ctx, wm.root)
238                                 // the modFile is no longer valid.
239                                 if err != nil {
240                                         event.Error(ctx, "finding file system modules", err)
241                                 }
242                                 modFile = nil
243                         }
244                 }
245         }
246
247         // Next, handle go.mod changes that could affect our set of tracked modules.
248         // If we're reading our tracked modules from the gopls.mod, there's nothing
249         // to do here.
250         if wm.moduleSource != goplsModWorkspace {
251                 for uri, change := range changes {
252                         // If a go.mod file has changed, we may need to update the set of active
253                         // modules.
254                         if !isGoMod(uri) {
255                                 continue
256                         }
257                         if wm.moduleSource == legacyWorkspace && !equalURI(modURI(wm.root), uri) {
258                                 // Legacy mode only considers a module a workspace root.
259                                 continue
260                         }
261                         if !source.InDir(wm.root.Filename(), uri.Filename()) {
262                                 // Otherwise, the module must be contained within the workspace root.
263                                 continue
264                         }
265                         if modFiles == nil {
266                                 modFiles = make(map[span.URI]struct{})
267                                 for k := range wm.modFiles {
268                                         modFiles[k] = struct{}{}
269                                 }
270                         }
271                         if change.exists {
272                                 modFiles[uri] = struct{}{}
273                         } else {
274                                 delete(modFiles, uri)
275                         }
276                 }
277         }
278         if modFiles != nil {
279                 // Any change to modules triggers a new version.
280                 return &workspace{
281                         root:         wm.root,
282                         moduleSource: moduleSource,
283                         modFiles:     modFiles,
284                         file:         modFile,
285                         wsDirs:       wm.wsDirs,
286                 }, true
287         }
288         // No change. Just return wm, since it is immutable.
289         return wm, false
290 }
291
292 func equalURI(left, right span.URI) bool {
293         return span.CompareURI(left, right) == 0
294 }
295
296 // goplsModURI returns the URI for the gopls.mod file contained in root.
297 func goplsModURI(root span.URI) span.URI {
298         return span.URIFromPath(filepath.Join(root.Filename(), "gopls.mod"))
299 }
300
301 // modURI returns the URI for the go.mod file contained in root.
302 func modURI(root span.URI) span.URI {
303         return span.URIFromPath(filepath.Join(root.Filename(), "go.mod"))
304 }
305
306 // isGoMod reports if uri is a go.mod file.
307 func isGoMod(uri span.URI) bool {
308         return filepath.Base(uri.Filename()) == "go.mod"
309 }
310
311 // isGoMod reports if uri is a go.sum file.
312 func isGoSum(uri span.URI) bool {
313         return filepath.Base(uri.Filename()) == "go.sum"
314 }
315
316 // fileExists reports if the file uri exists within source.
317 func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) {
318         fh, err := source.GetFile(ctx, uri)
319         if err != nil {
320                 return false, err
321         }
322         return fileHandleExists(fh)
323 }
324
325 // fileHandleExists reports if the file underlying fh actually exits.
326 func fileHandleExists(fh source.FileHandle) (bool, error) {
327         _, err := fh.Read()
328         if err == nil {
329                 return true, nil
330         }
331         if os.IsNotExist(err) {
332                 return false, nil
333         }
334         return false, err
335 }
336
337 // TODO(rFindley): replace this (and similar) with a uripath package analogous
338 // to filepath.
339 func dirURI(uri span.URI) span.URI {
340         return span.URIFromPath(filepath.Dir(uri.Filename()))
341 }
342
343 // getLegacyModules returns a module set containing at most the root module.
344 func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) {
345         uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod"))
346         modules := make(map[span.URI]struct{})
347         exists, err := fileExists(ctx, uri, fs)
348         if err != nil {
349                 return nil, err
350         }
351         if exists {
352                 modules[uri] = struct{}{}
353         }
354         return modules, nil
355 }
356
357 func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span.URI]struct{}, error) {
358         modFile, err := modfile.Parse(uri.Filename(), contents, nil)
359         if err != nil {
360                 return nil, nil, errors.Errorf("parsing gopls.mod: %w", err)
361         }
362         modFiles := make(map[span.URI]struct{})
363         for _, replace := range modFile.Replace {
364                 if replace.New.Version != "" {
365                         return nil, nil, errors.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version)
366                 }
367                 dirFP := filepath.FromSlash(replace.New.Path)
368                 if !filepath.IsAbs(dirFP) {
369                         dirFP = filepath.Join(root.Filename(), dirFP)
370                         // The resulting modfile must use absolute paths, so that it can be
371                         // written to a temp directory.
372                         replace.New.Path = dirFP
373                 }
374                 modURI := span.URIFromPath(filepath.Join(dirFP, "go.mod"))
375                 modFiles[modURI] = struct{}{}
376         }
377         return modFile, modFiles, nil
378 }
379
380 // findAllModules recursively walks the root directory looking for go.mod
381 // files, returning the set of modules it discovers.
382 // TODO(rfindley): consider overlays.
383 func findAllModules(ctx context.Context, root span.URI) (map[span.URI]struct{}, error) {
384         // Walk the view's folder to find all modules in the view.
385         modFiles := make(map[span.URI]struct{})
386         return modFiles, filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
387                 if err != nil {
388                         // Probably a permission error. Keep looking.
389                         return filepath.SkipDir
390                 }
391                 // For any path that is not the workspace folder, check if the path
392                 // would be ignored by the go command. Vendor directories also do not
393                 // contain workspace modules.
394                 if info.IsDir() && path != root.Filename() {
395                         suffix := strings.TrimPrefix(path, root.Filename())
396                         switch {
397                         case checkIgnored(suffix),
398                                 strings.Contains(filepath.ToSlash(suffix), "/vendor/"):
399                                 return filepath.SkipDir
400                         }
401                 }
402                 // We're only interested in go.mod files.
403                 uri := span.URIFromPath(path)
404                 if isGoMod(uri) {
405                         modFiles[uri] = struct{}{}
406                 }
407                 return nil
408         })
409 }