// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package cache import ( "context" "os" "path/filepath" "sort" "strings" "sync" "golang.org/x/mod/modfile" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" errors "golang.org/x/xerrors" ) type workspaceSource int const ( legacyWorkspace = iota goplsModWorkspace fileSystemWorkspace ) func (s workspaceSource) String() string { switch s { case legacyWorkspace: return "legacy" case goplsModWorkspace: return "gopls.mod" case fileSystemWorkspace: return "file system" default: return "!(unknown module source)" } } // workspace tracks go.mod files in the workspace, along with the // gopls.mod file, to provide support for multi-module workspaces. // // Specifically, it provides: // - the set of modules contained within in the workspace root considered to // be 'active' // - the workspace modfile, to be used for the go command `-modfile` flag // - the set of workspace directories // // This type is immutable (or rather, idempotent), so that it may be shared // across multiple snapshots. type workspace struct { root span.URI moduleSource workspaceSource // modFiles holds the active go.mod files. modFiles map[span.URI]struct{} // The workspace module is lazily re-built once after being invalidated. // buildMu+built guards this reconstruction. // // file and wsDirs may be non-nil even if built == false, if they were copied // from the previous workspace module version. In this case, they will be // preserved if building fails. buildMu sync.Mutex built bool buildErr error file *modfile.File wsDirs map[span.URI]struct{} } func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, experimental bool) (*workspace, error) { if !experimental { modFiles, err := getLegacyModules(ctx, root, fs) if err != nil { return nil, err } return &workspace{ root: root, modFiles: modFiles, moduleSource: legacyWorkspace, }, nil } goplsModFH, err := fs.GetFile(ctx, goplsModURI(root)) if err != nil { return nil, err } contents, err := goplsModFH.Read() if err == nil { file, modFiles, err := parseGoplsMod(root, goplsModFH.URI(), contents) if err != nil { return nil, err } return &workspace{ root: root, modFiles: modFiles, file: file, moduleSource: goplsModWorkspace, }, nil } modFiles, err := findAllModules(ctx, root) if err != nil { return nil, err } return &workspace{ root: root, modFiles: modFiles, moduleSource: fileSystemWorkspace, }, nil } func (wm *workspace) activeModFiles() map[span.URI]struct{} { return wm.modFiles } // modFile gets the workspace modfile associated with this workspace, // computing it if it doesn't exist. // // A fileSource must be passed in to solve a chicken-egg problem: it is not // correct to pass in the snapshot file source to newWorkspace when // invalidating, because at the time these are called the snapshot is locked. // So we must pass it in later on when actually using the modFile. func (wm *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) { wm.build(ctx, fs) return wm.file, wm.buildErr } func (wm *workspace) build(ctx context.Context, fs source.FileSource) { wm.buildMu.Lock() defer wm.buildMu.Unlock() if wm.built { return } // Building should never be cancelled. Since the workspace module is shared // across multiple snapshots, doing so would put us in a bad state, and it // would not be obvious to the user how to recover. ctx = xcontext.Detach(ctx) // If our module source is not gopls.mod, try to build the workspace module // from modules. Fall back on the pre-existing mod file if parsing fails. if wm.moduleSource != goplsModWorkspace { file, err := buildWorkspaceModFile(ctx, wm.modFiles, fs) switch { case err == nil: wm.file = file case wm.file != nil: // Parsing failed, but we have a previous file version. event.Error(ctx, "building workspace mod file", err) default: // No file to fall back on. wm.buildErr = err } } if wm.file != nil { wm.wsDirs = map[span.URI]struct{}{ wm.root: {}, } for _, r := range wm.file.Replace { // We may be replacing a module with a different version, not a path // on disk. if r.New.Version != "" { continue } wm.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} } } // Ensure that there is always at least the root dir. if len(wm.wsDirs) == 0 { wm.wsDirs = map[span.URI]struct{}{ wm.root: {}, } } wm.built = true } // dirs returns the workspace directories for the loaded modules. func (wm *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { wm.build(ctx, fs) var dirs []span.URI for d := range wm.wsDirs { dirs = append(dirs, d) } sort.Slice(dirs, func(i, j int) bool { return span.CompareURI(dirs[i], dirs[j]) < 0 }) return dirs } // invalidate returns a (possibly) new workspaceModule after invalidating // changedURIs. If wm is still valid in the presence of changedURIs, it returns // itself unmodified. func (wm *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange) (*workspace, bool) { // Prevent races to wm.modFile or wm.wsDirs below, if wm has not yet been // built. wm.buildMu.Lock() defer wm.buildMu.Unlock() // Any gopls.mod change is processed first, followed by go.mod changes, as // changes to gopls.mod may affect the set of active go.mod files. var ( // New values. We return a new workspace module if and only if modFiles is // non-nil. modFiles map[span.URI]struct{} moduleSource = wm.moduleSource modFile = wm.file err error ) if wm.moduleSource == goplsModWorkspace { // If we are currently reading the modfile from gopls.mod, we default to // preserving it even if module metadata changes (which may be the case if // a go.sum file changes). modFile = wm.file } // First handle changes to the gopls.mod file. if wm.moduleSource != legacyWorkspace { // If gopls.mod has changed we need to either re-read it if it exists or // walk the filesystem if it doesn't exist. gmURI := goplsModURI(wm.root) if change, ok := changes[gmURI]; ok { if change.exists { // Only invalidate if the gopls.mod actually parses. Otherwise, stick with the current gopls.mod parsedFile, parsedModules, err := parseGoplsMod(wm.root, gmURI, change.content) if err == nil { modFile = parsedFile moduleSource = goplsModWorkspace modFiles = parsedModules } else { // Note that modFile is not invalidated here. event.Error(ctx, "parsing gopls.mod", err) } } else { // gopls.mod is deleted. search for modules again. moduleSource = fileSystemWorkspace modFiles, err = findAllModules(ctx, wm.root) // the modFile is no longer valid. if err != nil { event.Error(ctx, "finding file system modules", err) } modFile = nil } } } // Next, handle go.mod changes that could affect our set of tracked modules. // If we're reading our tracked modules from the gopls.mod, there's nothing // to do here. if wm.moduleSource != goplsModWorkspace { for uri, change := range changes { // If a go.mod file has changed, we may need to update the set of active // modules. if !isGoMod(uri) { continue } if wm.moduleSource == legacyWorkspace && !equalURI(modURI(wm.root), uri) { // Legacy mode only considers a module a workspace root. continue } if !source.InDir(wm.root.Filename(), uri.Filename()) { // Otherwise, the module must be contained within the workspace root. continue } if modFiles == nil { modFiles = make(map[span.URI]struct{}) for k := range wm.modFiles { modFiles[k] = struct{}{} } } if change.exists { modFiles[uri] = struct{}{} } else { delete(modFiles, uri) } } } if modFiles != nil { // Any change to modules triggers a new version. return &workspace{ root: wm.root, moduleSource: moduleSource, modFiles: modFiles, file: modFile, wsDirs: wm.wsDirs, }, true } // No change. Just return wm, since it is immutable. return wm, false } func equalURI(left, right span.URI) bool { return span.CompareURI(left, right) == 0 } // goplsModURI returns the URI for the gopls.mod file contained in root. func goplsModURI(root span.URI) span.URI { return span.URIFromPath(filepath.Join(root.Filename(), "gopls.mod")) } // modURI returns the URI for the go.mod file contained in root. func modURI(root span.URI) span.URI { return span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) } // isGoMod reports if uri is a go.mod file. func isGoMod(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.mod" } // isGoMod reports if uri is a go.sum file. func isGoSum(uri span.URI) bool { return filepath.Base(uri.Filename()) == "go.sum" } // fileExists reports if the file uri exists within source. func fileExists(ctx context.Context, uri span.URI, source source.FileSource) (bool, error) { fh, err := source.GetFile(ctx, uri) if err != nil { return false, err } return fileHandleExists(fh) } // fileHandleExists reports if the file underlying fh actually exits. func fileHandleExists(fh source.FileHandle) (bool, error) { _, err := fh.Read() if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } // TODO(rFindley): replace this (and similar) with a uripath package analogous // to filepath. func dirURI(uri span.URI) span.URI { return span.URIFromPath(filepath.Dir(uri.Filename())) } // getLegacyModules returns a module set containing at most the root module. func getLegacyModules(ctx context.Context, root span.URI, fs source.FileSource) (map[span.URI]struct{}, error) { uri := span.URIFromPath(filepath.Join(root.Filename(), "go.mod")) modules := make(map[span.URI]struct{}) exists, err := fileExists(ctx, uri, fs) if err != nil { return nil, err } if exists { modules[uri] = struct{}{} } return modules, nil } func parseGoplsMod(root, uri span.URI, contents []byte) (*modfile.File, map[span.URI]struct{}, error) { modFile, err := modfile.Parse(uri.Filename(), contents, nil) if err != nil { return nil, nil, errors.Errorf("parsing gopls.mod: %w", err) } modFiles := make(map[span.URI]struct{}) for _, replace := range modFile.Replace { if replace.New.Version != "" { return nil, nil, errors.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version) } dirFP := filepath.FromSlash(replace.New.Path) if !filepath.IsAbs(dirFP) { dirFP = filepath.Join(root.Filename(), dirFP) // The resulting modfile must use absolute paths, so that it can be // written to a temp directory. replace.New.Path = dirFP } modURI := span.URIFromPath(filepath.Join(dirFP, "go.mod")) modFiles[modURI] = struct{}{} } return modFile, modFiles, nil } // findAllModules recursively walks the root directory looking for go.mod // files, returning the set of modules it discovers. // TODO(rfindley): consider overlays. func findAllModules(ctx context.Context, root span.URI) (map[span.URI]struct{}, error) { // Walk the view's folder to find all modules in the view. modFiles := make(map[span.URI]struct{}) return modFiles, filepath.Walk(root.Filename(), func(path string, info os.FileInfo, err error) error { if err != nil { // Probably a permission error. Keep looking. return filepath.SkipDir } // For any path that is not the workspace folder, check if the path // would be ignored by the go command. Vendor directories also do not // contain workspace modules. if info.IsDir() && path != root.Filename() { suffix := strings.TrimPrefix(path, root.Filename()) switch { case checkIgnored(suffix), strings.Contains(filepath.ToSlash(suffix), "/vendor/"): return filepath.SkipDir } } // We're only interested in go.mod files. uri := span.URIFromPath(path) if isGoMod(uri) { modFiles[uri] = struct{}{} } return nil }) }