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.
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"
23 type workspaceSource int
26 legacyWorkspace = iota
31 func (s workspaceSource) String() string {
35 case goplsModWorkspace:
37 case fileSystemWorkspace:
40 return "!(unknown module source)"
44 // workspace tracks go.mod files in the workspace, along with the
45 // gopls.mod file, to provide support for multi-module workspaces.
47 // Specifically, it provides:
48 // - the set of modules contained within in the workspace root considered to
50 // - the workspace modfile, to be used for the go command `-modfile` flag
51 // - the set of workspace directories
53 // This type is immutable (or rather, idempotent), so that it may be shared
54 // across multiple snapshots.
55 type workspace struct {
57 excludePath func(string) bool
58 moduleSource workspaceSource
60 // activeModFiles holds the active go.mod files.
61 activeModFiles map[span.URI]struct{}
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{}
67 // go111moduleOff indicates whether GO111MODULE=off has been configured in
71 // The workspace module is lazily re-built once after being invalidated.
72 // buildMu+built guards this reconstruction.
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.
82 wsDirs map[span.URI]struct{}
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
89 goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
93 contents, err := goplsModFH.Read()
95 file, activeModFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
101 excludePath: excludePath,
102 activeModFiles: activeModFiles,
103 knownModFiles: activeModFiles,
105 moduleSource: goplsModWorkspace,
109 // Otherwise, in all other modes, search for all of the go.mod files in the
111 knownModFiles, err := findModules(ctx, root, excludePath, 0)
115 // When GO111MODULE=off, there are no active go.mod files.
119 excludePath: excludePath,
120 moduleSource: legacyWorkspace,
121 knownModFiles: knownModFiles,
122 go111moduleOff: true,
125 // In legacy mode, not all known go.mod files will be considered active.
127 activeModFiles, err := getLegacyModules(ctx, root, fs)
133 excludePath: excludePath,
134 activeModFiles: activeModFiles,
135 knownModFiles: knownModFiles,
136 moduleSource: legacyWorkspace,
141 excludePath: excludePath,
142 activeModFiles: knownModFiles,
143 knownModFiles: knownModFiles,
144 moduleSource: fileSystemWorkspace,
148 func (w *workspace) getKnownModFiles() map[span.URI]struct{} {
149 return w.knownModFiles
152 func (w *workspace) getActiveModFiles() map[span.URI]struct{} {
153 return w.activeModFiles
156 // modFile gets the workspace modfile associated with this workspace,
157 // computing it if it doesn't exist.
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) {
165 return w.mod, w.buildErr
168 func (w *workspace) sumFile(ctx context.Context, fs source.FileSource) ([]byte, error) {
170 return w.sum, w.buildErr
173 func (w *workspace) build(ctx context.Context, fs source.FileSource) {
175 defer w.buildMu.Unlock()
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)
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)
193 // Parsing failed, but we have a previous file version.
194 event.Error(ctx, "building workspace mod file", err)
196 // No file to fall back on.
201 w.wsDirs = map[span.URI]struct{}{
204 for _, r := range w.mod.Replace {
205 // We may be replacing a module with a different version, not a path
207 if r.New.Version != "" {
210 w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{}
213 // Ensure that there is always at least the root dir.
214 if len(w.wsDirs) == 0 {
215 w.wsDirs = map[span.URI]struct{}{
219 sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs)
223 event.Error(ctx, "building workspace sum file", err)
228 // dirs returns the workspace directories for the loaded modules.
229 func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI {
232 for d := range w.wsDirs {
233 dirs = append(dirs, d)
235 sort.Slice(dirs, func(i, j int) bool {
236 return source.CompareURI(dirs[i], dirs[j]) < 0
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
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
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.
252 defer w.buildMu.Unlock()
254 // Clone the workspace. This may be discarded if nothing changed.
255 result := &workspace{
257 moduleSource: w.moduleSource,
258 knownModFiles: make(map[span.URI]struct{}),
259 activeModFiles: make(map[span.URI]struct{}),
260 go111moduleOff: w.go111moduleOff,
265 for k, v := range w.knownModFiles {
266 result.knownModFiles[k] = v
268 for k, v := range w.activeModFiles {
269 result.activeModFiles[k] = v
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 {
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)
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
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)
302 // gopls.mod is deleted. search for modules again.
305 result.moduleSource = fileSystemWorkspace
306 // The parsed gopls.mod is no longer valid.
308 knownModFiles, err := findModules(ctx, w.root, w.excludePath, 0)
310 result.knownModFiles = nil
311 result.activeModFiles = nil
312 event.Error(ctx, "finding file system modules", err)
314 result.knownModFiles = knownModFiles
315 result.activeModFiles = make(map[span.URI]struct{})
316 for k, v := range result.knownModFiles {
317 result.activeModFiles[k] = v
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
327 if result.moduleSource != goplsModWorkspace {
328 for uri, change := range changes {
329 if !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) {
333 active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0
334 reload = reload || (active && change.fileHandle.Saved())
336 result.knownModFiles[uri] = struct{}{}
338 result.activeModFiles[uri] = struct{}{}
341 delete(result.knownModFiles, uri)
342 delete(result.activeModFiles, uri)
347 // Finally, process go.sum changes for any modules that are now active.
348 for uri, change := range changes {
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 {
358 // Only changes to active go.sum files actually cause the workspace to
361 reload = reload || change.fileHandle.Saved()
365 return w, false, false
368 return result, changed, reload
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"))
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"))
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"
386 func isGoSum(uri span.URI) bool {
387 return filepath.Base(uri.Filename()) == "go.sum"
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)
396 return fileHandleExists(fh)
399 // fileHandleExists reports if the file underlying fh actually exits.
400 func fileHandleExists(fh source.FileHandle) (bool, error) {
405 if os.IsNotExist(err) {
411 // TODO(rFindley): replace this (and similar) with a uripath package analogous
413 func dirURI(uri span.URI) span.URI {
414 return span.URIFromPath(filepath.Dir(uri.Filename()))
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)
426 modules[uri] = struct{}{}
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)
434 return nil, nil, errors.Errorf("parsing gopls.mod: %w", err)
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)
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
448 modURI := span.URIFromPath(filepath.Join(dirFP, "go.mod"))
449 modFiles[modURI] = struct{}{}
451 return modFile, modFiles, nil
454 // errExhausted is returned by findModules if the file scan limit is reached.
455 var errExhausted = errors.New("exhausted")
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
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.
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{})
470 errDone := errors.New("done")
471 err := filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error {
473 // Probably a permission error. Keep looking.
474 return filepath.SkipDir
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())
482 case checkIgnored(suffix),
483 strings.Contains(filepath.ToSlash(suffix), "/vendor/"),
485 return filepath.SkipDir
488 // We're only interested in go.mod files.
489 uri := span.URIFromPath(path)
491 modFiles[uri] = struct{}{}
493 if modLimit > 0 && len(modFiles) >= modLimit {
497 if fileLimit > 0 && searched >= fileLimit {