--- /dev/null
+// 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
+ })
+}