| // 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" |
| "errors" |
| "fmt" |
| "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" |
| ) |
| |
| // workspaceSource reports how the set of active modules has been derived. |
| type workspaceSource int |
| |
| const ( |
| legacyWorkspace = iota // non-module or single module mode |
| goplsModWorkspace // modules provided by a gopls.mod file |
| goWorkWorkspace // modules provided by a go.work file |
| fileSystemWorkspace // modules scanned from the filesystem |
| ) |
| |
| func (s workspaceSource) String() string { |
| switch s { |
| case legacyWorkspace: |
| return "legacy" |
| case goplsModWorkspace: |
| return "gopls.mod" |
| case goWorkWorkspace: |
| return "go.work" |
| 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 |
| excludePath func(string) bool |
| moduleSource workspaceSource |
| |
| // activeModFiles holds the active go.mod files. |
| activeModFiles map[span.URI]struct{} |
| |
| // knownModFiles holds the set of all go.mod files in the workspace. |
| // In all modes except for legacy, this is equivalent to modFiles. |
| knownModFiles map[span.URI]struct{} |
| |
| // workFile, if nonEmpty, is the go.work file for the workspace. |
| workFile span.URI |
| |
| // 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 |
| mod *modfile.File |
| sum []byte |
| wsDirs map[span.URI]struct{} |
| } |
| |
| // newWorkspace creates a new workspace at the given root directory, |
| // determining its module source based on the presence of a gopls.mod or |
| // go.work file, and the go111moduleOff and useWsModule settings. |
| // |
| // If useWsModule is set, the workspace may use a synthetic mod file replacing |
| // all modules in the root. |
| // |
| // If there is no active workspace file (a gopls.mod or go.work), newWorkspace |
| // scans the filesystem to find modules. |
| func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, useWsModule bool) (*workspace, error) { |
| ws := &workspace{ |
| root: root, |
| excludePath: excludePath, |
| } |
| |
| // The user may have a gopls.mod or go.work file that defines their |
| // workspace. |
| if err := loadExplicitWorkspaceFile(ctx, ws, fs); err == nil { |
| return ws, nil |
| } |
| |
| // Otherwise, in all other modes, search for all of the go.mod files in the |
| // workspace. |
| knownModFiles, err := findModules(root, excludePath, 0) |
| if err != nil { |
| return nil, err |
| } |
| ws.knownModFiles = knownModFiles |
| |
| switch { |
| case go111moduleOff: |
| ws.moduleSource = legacyWorkspace |
| case useWsModule: |
| ws.activeModFiles = knownModFiles |
| ws.moduleSource = fileSystemWorkspace |
| default: |
| ws.moduleSource = legacyWorkspace |
| activeModFiles, err := getLegacyModules(ctx, root, fs) |
| if err != nil { |
| return nil, err |
| } |
| ws.activeModFiles = activeModFiles |
| } |
| return ws, nil |
| } |
| |
| // loadExplicitWorkspaceFile loads workspace information from go.work or |
| // gopls.mod files, setting the active modules, mod file, and module source |
| // accordingly. |
| func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.FileSource) error { |
| for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { |
| fh, err := fs.GetFile(ctx, uriForSource(ws.root, src)) |
| if err != nil { |
| return err |
| } |
| contents, err := fh.Read() |
| if err != nil { |
| continue |
| } |
| var file *modfile.File |
| var activeModFiles map[span.URI]struct{} |
| switch src { |
| case goWorkWorkspace: |
| file, activeModFiles, err = parseGoWork(ctx, ws.root, fh.URI(), contents, fs) |
| ws.workFile = fh.URI() |
| case goplsModWorkspace: |
| file, activeModFiles, err = parseGoplsMod(ws.root, fh.URI(), contents) |
| } |
| if err != nil { |
| ws.buildMu.Lock() |
| ws.built = true |
| ws.buildErr = err |
| ws.buildMu.Unlock() |
| } |
| ws.mod = file |
| ws.activeModFiles = activeModFiles |
| ws.moduleSource = src |
| return nil |
| } |
| return noHardcodedWorkspace |
| } |
| |
| var noHardcodedWorkspace = errors.New("no hardcoded workspace") |
| |
| func (w *workspace) getKnownModFiles() map[span.URI]struct{} { |
| return w.knownModFiles |
| } |
| |
| func (w *workspace) getActiveModFiles() map[span.URI]struct{} { |
| return w.activeModFiles |
| } |
| |
| // 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 (w *workspace) modFile(ctx context.Context, fs source.FileSource) (*modfile.File, error) { |
| w.build(ctx, fs) |
| return w.mod, w.buildErr |
| } |
| |
| func (w *workspace) sumFile(ctx context.Context, fs source.FileSource) ([]byte, error) { |
| w.build(ctx, fs) |
| return w.sum, w.buildErr |
| } |
| |
| func (w *workspace) build(ctx context.Context, fs source.FileSource) { |
| w.buildMu.Lock() |
| defer w.buildMu.Unlock() |
| |
| if w.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 w.moduleSource != goplsModWorkspace { |
| file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs) |
| switch { |
| case err == nil: |
| w.mod = file |
| case w.mod != 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. |
| w.buildErr = err |
| } |
| } |
| if w.mod != nil { |
| w.wsDirs = map[span.URI]struct{}{ |
| w.root: {}, |
| } |
| for _, r := range w.mod.Replace { |
| // We may be replacing a module with a different version, not a path |
| // on disk. |
| if r.New.Version != "" { |
| continue |
| } |
| w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} |
| } |
| } |
| // Ensure that there is always at least the root dir. |
| if len(w.wsDirs) == 0 { |
| w.wsDirs = map[span.URI]struct{}{ |
| w.root: {}, |
| } |
| } |
| sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs) |
| if err == nil { |
| w.sum = sum |
| } else { |
| event.Error(ctx, "building workspace sum file", err) |
| } |
| w.built = true |
| } |
| |
| // dirs returns the workspace directories for the loaded modules. |
| func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { |
| w.build(ctx, fs) |
| var dirs []span.URI |
| for d := range w.wsDirs { |
| dirs = append(dirs, d) |
| } |
| sort.Slice(dirs, func(i, j int) bool { |
| return source.CompareURI(dirs[i], dirs[j]) < 0 |
| }) |
| return dirs |
| } |
| |
| // invalidate returns a (possibly) new workspace after invalidating the changed |
| // files. If w is still valid in the presence of changedURIs, it returns itself |
| // unmodified. |
| // |
| // The returned changed and reload flags control the level of invalidation. |
| // Some workspace changes may affect workspace contents without requiring a |
| // reload of metadata (for example, unsaved changes to a go.mod or go.sum |
| // file). |
| func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { |
| // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. |
| w.buildMu.Lock() |
| defer w.buildMu.Unlock() |
| |
| // Clone the workspace. This may be discarded if nothing changed. |
| result := &workspace{ |
| root: w.root, |
| moduleSource: w.moduleSource, |
| knownModFiles: make(map[span.URI]struct{}), |
| activeModFiles: make(map[span.URI]struct{}), |
| workFile: w.workFile, |
| mod: w.mod, |
| sum: w.sum, |
| wsDirs: w.wsDirs, |
| excludePath: w.excludePath, |
| } |
| for k, v := range w.knownModFiles { |
| result.knownModFiles[k] = v |
| } |
| for k, v := range w.activeModFiles { |
| result.activeModFiles[k] = v |
| } |
| |
| // First handle changes to the go.work or gopls.mod file. This must be |
| // considered before any changes to go.mod or go.sum files, as these files |
| // determine which modules we care about. If go.work/gopls.mod has changed |
| // we need to either re-read it if it exists or walk the filesystem if it |
| // has been deleted. go.work should override the gopls.mod if both exist. |
| changed, reload = handleWorkspaceFileChanges(ctx, result, changes, fs) |
| // Next, handle go.mod changes that could affect our workspace. |
| for uri, change := range changes { |
| // Otherwise, we only care about go.mod files in the workspace directory. |
| if change.isUnchanged || !isGoMod(uri) || !source.InDir(result.root.Filename(), uri.Filename()) { |
| continue |
| } |
| changed = true |
| active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0 |
| reload = reload || (active && change.fileHandle.Saved()) |
| // Don't mess with the list of mod files if using go.work or gopls.mod. |
| if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { |
| continue |
| } |
| if change.exists { |
| result.knownModFiles[uri] = struct{}{} |
| if active { |
| result.activeModFiles[uri] = struct{}{} |
| } |
| } else { |
| delete(result.knownModFiles, uri) |
| delete(result.activeModFiles, uri) |
| } |
| } |
| |
| // Finally, process go.sum changes for any modules that are now active. |
| for uri, change := range changes { |
| if !isGoSum(uri) { |
| continue |
| } |
| // TODO(rFindley) factor out this URI mangling. |
| dir := filepath.Dir(uri.Filename()) |
| modURI := span.URIFromPath(filepath.Join(dir, "go.mod")) |
| if _, active := result.activeModFiles[modURI]; !active { |
| continue |
| } |
| // Only changes to active go.sum files actually cause the workspace to |
| // change. |
| changed = true |
| reload = reload || change.fileHandle.Saved() |
| } |
| |
| if !changed { |
| return w, false, false |
| } |
| |
| return result, changed, reload |
| } |
| |
| // handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod |
| // file, updating ws accordingly. ws.root must be set. |
| func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[span.URI]*fileChange, fs source.FileSource) (changed, reload bool) { |
| // If go.work/gopls.mod has changed we need to either re-read it if it |
| // exists or walk the filesystem if it has been deleted. |
| // go.work should override the gopls.mod if both exist. |
| for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { |
| uri := uriForSource(ws.root, src) |
| // File opens/closes are just no-ops. |
| change, ok := changes[uri] |
| if !ok { |
| continue |
| } |
| if change.isUnchanged { |
| break |
| } |
| if change.exists { |
| // Only invalidate if the file if it actually parses. |
| // Otherwise, stick with the current file. |
| var parsedFile *modfile.File |
| var parsedModules map[span.URI]struct{} |
| var err error |
| switch src { |
| case goWorkWorkspace: |
| parsedFile, parsedModules, err = parseGoWork(ctx, ws.root, uri, change.content, fs) |
| case goplsModWorkspace: |
| parsedFile, parsedModules, err = parseGoplsMod(ws.root, uri, change.content) |
| } |
| if err != nil { |
| // An unparseable file should not invalidate the workspace: |
| // nothing good could come from changing the workspace in |
| // this case. |
| event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) |
| } else { |
| // only update the modfile if it parsed. |
| changed = true |
| reload = change.fileHandle.Saved() |
| ws.mod = parsedFile |
| ws.moduleSource = src |
| ws.knownModFiles = parsedModules |
| ws.activeModFiles = make(map[span.URI]struct{}) |
| for k, v := range parsedModules { |
| ws.activeModFiles[k] = v |
| } |
| } |
| break // We've found an explicit workspace file, so can stop looking. |
| } else { |
| // go.work/gopls.mod is deleted. search for modules again. |
| changed = true |
| reload = true |
| ws.moduleSource = fileSystemWorkspace |
| // The parsed file is no longer valid. |
| ws.mod = nil |
| knownModFiles, err := findModules(ws.root, ws.excludePath, 0) |
| if err != nil { |
| ws.knownModFiles = nil |
| ws.activeModFiles = nil |
| event.Error(ctx, "finding file system modules", err) |
| } else { |
| ws.knownModFiles = knownModFiles |
| ws.activeModFiles = make(map[span.URI]struct{}) |
| for k, v := range ws.knownModFiles { |
| ws.activeModFiles[k] = v |
| } |
| } |
| } |
| } |
| return changed, reload |
| } |
| |
| // goplsModURI returns the URI for the gopls.mod file contained in root. |
| func uriForSource(root span.URI, src workspaceSource) span.URI { |
| var basename string |
| switch src { |
| case goplsModWorkspace: |
| basename = "gopls.mod" |
| case goWorkWorkspace: |
| basename = "go.work" |
| default: |
| return "" |
| } |
| return span.URIFromPath(filepath.Join(root.Filename(), basename)) |
| } |
| |
| // 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" |
| } |
| |
| func isGoSum(uri span.URI) bool { |
| return filepath.Base(uri.Filename()) == "go.sum" || filepath.Base(uri.Filename()) == "go.work.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 parseGoWork(ctx context.Context, root, uri span.URI, contents []byte, fs source.FileSource) (*modfile.File, map[span.URI]struct{}, error) { |
| workFile, err := modfile.ParseWork(uri.Filename(), contents, nil) |
| if err != nil { |
| return nil, nil, fmt.Errorf("parsing go.work: %w", err) |
| } |
| modFiles := make(map[span.URI]struct{}) |
| for _, dir := range workFile.Use { |
| // The resulting modfile must use absolute paths, so that it can be |
| // written to a temp directory. |
| dir.Path = absolutePath(root, dir.Path) |
| modURI := span.URIFromPath(filepath.Join(dir.Path, "go.mod")) |
| modFiles[modURI] = struct{}{} |
| } |
| modFile, err := buildWorkspaceModFile(ctx, modFiles, fs) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| // Require a go directive, per the spec. |
| if workFile.Go == nil || workFile.Go.Version == "" { |
| return nil, nil, fmt.Errorf("go.work has missing or incomplete go directive") |
| } |
| if err := modFile.AddGoStmt(workFile.Go.Version); err != nil { |
| return nil, nil, err |
| } |
| |
| return modFile, modFiles, 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, fmt.Errorf("parsing gopls.mod: %w", err) |
| } |
| modFiles := make(map[span.URI]struct{}) |
| for _, replace := range modFile.Replace { |
| if replace.New.Version != "" { |
| return nil, nil, fmt.Errorf("gopls.mod: replaced module %q@%q must not have version", replace.New.Path, replace.New.Version) |
| } |
| // The resulting modfile must use absolute paths, so that it can be |
| // written to a temp directory. |
| replace.New.Path = absolutePath(root, replace.New.Path) |
| modURI := span.URIFromPath(filepath.Join(replace.New.Path, "go.mod")) |
| modFiles[modURI] = struct{}{} |
| } |
| return modFile, modFiles, nil |
| } |
| |
| func absolutePath(root span.URI, path string) string { |
| dirFP := filepath.FromSlash(path) |
| if !filepath.IsAbs(dirFP) { |
| dirFP = filepath.Join(root.Filename(), dirFP) |
| } |
| return dirFP |
| } |
| |
| // errExhausted is returned by findModules if the file scan limit is reached. |
| var errExhausted = errors.New("exhausted") |
| |
| // Limit go.mod search to 1 million files. As a point of reference, |
| // Kubernetes has 22K files (as of 2020-11-24). |
| const fileLimit = 1000000 |
| |
| // findModules recursively walks the root directory looking for go.mod files, |
| // returning the set of modules it discovers. If modLimit is non-zero, |
| // searching stops once modLimit modules have been found. |
| // |
| // TODO(rfindley): consider overlays. |
| func findModules(root span.URI, excludePath func(string) bool, modLimit int) (map[span.URI]struct{}, error) { |
| // Walk the view's folder to find all modules in the view. |
| modFiles := make(map[span.URI]struct{}) |
| searched := 0 |
| errDone := errors.New("done") |
| err := 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/"), |
| excludePath(suffix): |
| return filepath.SkipDir |
| } |
| } |
| // We're only interested in go.mod files. |
| uri := span.URIFromPath(path) |
| if isGoMod(uri) { |
| modFiles[uri] = struct{}{} |
| } |
| if modLimit > 0 && len(modFiles) >= modLimit { |
| return errDone |
| } |
| searched++ |
| if fileLimit > 0 && searched >= fileLimit { |
| return errExhausted |
| } |
| return nil |
| }) |
| if err == errDone { |
| return modFiles, nil |
| } |
| return modFiles, err |
| } |