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 moduleSource workspaceSource
59 // modFiles holds the active go.mod files.
60 modFiles map[span.URI]struct{}
62 // The workspace module is lazily re-built once after being invalidated.
63 // buildMu+built guards this reconstruction.
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.
72 wsDirs map[span.URI]struct{}
75 func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, experimental bool) (*workspace, error) {
77 modFiles, err := getLegacyModules(ctx, root, fs)
84 moduleSource: legacyWorkspace,
87 goplsModFH, err := fs.GetFile(ctx, goplsModURI(root))
91 contents, err := goplsModFH.Read()
93 file, modFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents)
101 moduleSource: goplsModWorkspace,
104 modFiles, err := findAllModules(ctx, root)
111 moduleSource: fileSystemWorkspace,
115 func (wm *workspace) activeModFiles() map[span.URI]struct{} {
119 // modFile gets the workspace modfile associated with this workspace,
120 // computing it if it doesn't exist.
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) {
128 return wm.file, wm.buildErr
131 func (wm *workspace) build(ctx context.Context, fs source.FileSource) {
133 defer wm.buildMu.Unlock()
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)
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)
151 // Parsing failed, but we have a previous file version.
152 event.Error(ctx, "building workspace mod file", err)
154 // No file to fall back on.
159 wm.wsDirs = map[span.URI]struct{}{
162 for _, r := range wm.file.Replace {
163 // We may be replacing a module with a different version, not a path
165 if r.New.Version != "" {
168 wm.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{}
171 // Ensure that there is always at least the root dir.
172 if len(wm.wsDirs) == 0 {
173 wm.wsDirs = map[span.URI]struct{}{
180 // dirs returns the workspace directories for the loaded modules.
181 func (wm *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI {
184 for d := range wm.wsDirs {
185 dirs = append(dirs, d)
187 sort.Slice(dirs, func(i, j int) bool {
188 return span.CompareURI(dirs[i], dirs[j]) < 0
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
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.
204 // New values. We return a new workspace module if and only if modFiles is
206 modFiles map[span.URI]struct{}
207 moduleSource = wm.moduleSource
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).
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 {
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)
228 moduleSource = goplsModWorkspace
229 modFiles = parsedModules
231 // Note that modFile is not invalidated here.
232 event.Error(ctx, "parsing gopls.mod", err)
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.
240 event.Error(ctx, "finding file system modules", err)
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
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
257 if wm.moduleSource == legacyWorkspace && !equalURI(modURI(wm.root), uri) {
258 // Legacy mode only considers a module a workspace root.
261 if !source.InDir(wm.root.Filename(), uri.Filename()) {
262 // Otherwise, the module must be contained within the workspace root.
266 modFiles = make(map[span.URI]struct{})
267 for k := range wm.modFiles {
268 modFiles[k] = struct{}{}
272 modFiles[uri] = struct{}{}
274 delete(modFiles, uri)
279 // Any change to modules triggers a new version.
282 moduleSource: moduleSource,
288 // No change. Just return wm, since it is immutable.
292 func equalURI(left, right span.URI) bool {
293 return span.CompareURI(left, right) == 0
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"))
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"))
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"
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"
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)
322 return fileHandleExists(fh)
325 // fileHandleExists reports if the file underlying fh actually exits.
326 func fileHandleExists(fh source.FileHandle) (bool, error) {
331 if os.IsNotExist(err) {
337 // TODO(rFindley): replace this (and similar) with a uripath package analogous
339 func dirURI(uri span.URI) span.URI {
340 return span.URIFromPath(filepath.Dir(uri.Filename()))
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)
352 modules[uri] = struct{}{}
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)
360 return nil, nil, errors.Errorf("parsing gopls.mod: %w", err)
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)
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
374 modURI := span.URIFromPath(filepath.Join(dirFP, "go.mod"))
375 modFiles[modURI] = struct{}{}
377 return modFile, modFiles, nil
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 {
388 // Probably a permission error. Keep looking.
389 return filepath.SkipDir
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())
397 case checkIgnored(suffix),
398 strings.Contains(filepath.ToSlash(suffix), "/vendor/"):
399 return filepath.SkipDir
402 // We're only interested in go.mod files.
403 uri := span.URIFromPath(path)
405 modFiles[uri] = struct{}{}