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