gopls/internal/lsp/cache: invalidate metadata after vendor change
This change causes the snapshot to watch the vendor/modules.txt
file and to invalidate the metadata if it changes, since it
could cause modules to move.
Also, add a regression test for the bug in which running
go mod vendor causes a module to move, but the Definitions
operation reports the old location. (And the reverse case
of deleting the vendor tree.)
Also, add persistent.Map.String method, for debugging.
Fixes golang/go#55995
Change-Id: I48302416586c763b4a4de7d67aaa88fde52ea400
Reviewed-on: https://go-review.googlesource.com/c/tools/+/454315
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 3b0958e..51c8bb7 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -581,7 +581,6 @@
// the directory.
func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification {
var snapshots []*snapshot
-
s.viewMu.Lock()
for _, v := range s.views {
snapshot, release := v.getSnapshot()
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 5a15641..80de11a 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -917,7 +917,7 @@
dirName := dir.Filename()
// If the directory is within the view's folder, we're already watching
- // it with the pattern above.
+ // it with the first pattern above.
if source.InDir(s.view.folder.Filename(), dirName) {
continue
}
@@ -1654,16 +1654,18 @@
// Most likely, each call site of inVendor needs to be reconsidered to
// understand and correctly implement the desired behavior.
func inVendor(uri span.URI) bool {
- if !strings.Contains(string(uri), "/vendor/") {
- return false
- }
- // Only packages in _subdirectories_ of /vendor/ are considered vendored
+ _, after, found := cut(string(uri), "/vendor/")
+ // Only subdirectories of /vendor/ are considered vendored
// (/vendor/a/foo.go is vendored, /vendor/foo.go is not).
- split := strings.Split(string(uri), "/vendor/")
- if len(split) < 2 {
- return false
+ return found && strings.Contains(after, "/")
+}
+
+// TODO(adonovan): replace with strings.Cut when we can assume go1.18.
+func cut(s, sep string) (before, after string, found bool) {
+ if i := strings.Index(s, sep); i >= 0 {
+ return s[:i], s[i+len(sep):], true
}
- return strings.Contains(split[1], "/")
+ return s, "", false
}
// unappliedChanges is a file source that handles an uncloned snapshot.
@@ -1691,14 +1693,16 @@
s.mu.Lock()
defer s.mu.Unlock()
- // If there is an initialization error and a vendor directory changed, try to
- // reinit.
- if s.initializedErr != nil {
- for uri := range changes {
- if inVendor(uri) {
- reinit = true
- break
- }
+ // Changes to vendor tree may require reinitialization,
+ // either because of an initialization error
+ // (e.g. "inconsistent vendoring detected"), or because
+ // one or more modules may have moved into or out of the
+ // vendor tree after 'go mod vendor' or 'rm -fr vendor/'.
+ for uri := range changes {
+ if inVendor(uri) && s.initializedErr != nil ||
+ strings.HasSuffix(string(uri), "/vendor/modules.txt") {
+ reinit = true
+ break
}
}
@@ -1782,7 +1786,7 @@
}
// directIDs keeps track of package IDs that have directly changed.
- // It maps id->invalidateMetadata.
+ // Note: this is not a set, it's a map from id to invalidateMetadata.
directIDs := map[PackageID]bool{}
// Invalidate all package metadata if the workspace module has changed.
@@ -1821,7 +1825,7 @@
// Mark all of the package IDs containing the given file.
filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged)
for id := range filePackageIDs {
- directIDs[id] = directIDs[id] || invalidateMetadata
+ directIDs[id] = directIDs[id] || invalidateMetadata // may insert 'false'
}
// Invalidate the previous modTidyHandle if any of the files have been
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index 54b4122..bcb6f3a 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -1128,28 +1128,34 @@
// FileHandle from the cache for temporary files is problematic, since we
// cannot delete it.
func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modContent []byte) (bool, error) {
+ // Legacy GOPATH workspace?
if s.workspaceMode()&moduleMode == 0 {
return false, nil
}
+
+ // Explicit -mod flag?
matches := modFlagRegexp.FindStringSubmatch(s.view.goEnv["GOFLAGS"])
- var modFlag string
if len(matches) != 0 {
- modFlag = matches[1]
- }
- if modFlag != "" {
- // Don't override an explicit '-mod=vendor' argument.
- // We do want to override '-mod=readonly': it would break various module code lenses,
- // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway.
- return modFlag == "vendor", nil
+ modFlag := matches[1]
+ if modFlag != "" {
+ // Don't override an explicit '-mod=vendor' argument.
+ // We do want to override '-mod=readonly': it would break various module code lenses,
+ // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway.
+ return modFlag == "vendor", nil
+ }
}
modFile, err := modfile.Parse(modURI.Filename(), modContent, nil)
if err != nil {
return false, err
}
+
+ // No vendor directory?
if fi, err := os.Stat(filepath.Join(s.view.rootURI.Filename(), "vendor")); err != nil || !fi.IsDir() {
return false, nil
}
+
+ // Vendoring enabled by default by go declaration in go.mod?
vendorEnabled := modFile.Go != nil && modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0
return vendorEnabled, nil
}
diff --git a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go
index 99daff1..f57fc80 100644
--- a/gopls/internal/lsp/cache/view_test.go
+++ b/gopls/internal/lsp/cache/view_test.go
@@ -111,18 +111,11 @@
path string
inVendor bool
}{
- {
- path: "foo/vendor/x.go",
- inVendor: false,
- },
- {
- path: "foo/vendor/x/x.go",
- inVendor: true,
- },
- {
- path: "foo/x.go",
- inVendor: false,
- },
+ {"foo/vendor/x.go", false},
+ {"foo/vendor/x/x.go", true},
+ {"foo/x.go", false},
+ {"foo/vendor/foo.txt", false},
+ {"foo/vendor/modules.txt", false},
} {
if got := inVendor(span.URIFromPath(tt.path)); got != tt.inVendor {
t.Errorf("expected %s inVendor %v, got %v", tt.path, tt.inVendor, got)
diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go
index a5030d7..8f8619e 100644
--- a/gopls/internal/regtest/misc/definition_test.go
+++ b/gopls/internal/regtest/misc/definition_test.go
@@ -5,7 +5,9 @@
package misc
import (
+ "os"
"path"
+ "path/filepath"
"strings"
"testing"
@@ -286,3 +288,87 @@
env.Editor.Server.Definition(env.Ctx, params)
})
}
+
+// TestVendoringInvalidatesMetadata ensures that gopls uses the
+// correct metadata even after an external 'go mod vendor' command
+// causes packages to move; see issue #55995.
+func TestVendoringInvalidatesMetadata(t *testing.T) {
+ const proxy = `
+-- other.com/b@v1.0.0/go.mod --
+module other.com/b
+go 1.14
+
+-- other.com/b@v1.0.0/b.go --
+package b
+const K = 0
+`
+ const src = `
+-- go.mod --
+module example.com/a
+go 1.14
+require other.com/b v1.0.0
+
+-- go.sum --
+other.com/b v1.0.0 h1:1wb3PMGdet5ojzrKl+0iNksRLnOM9Jw+7amBNqmYwqk=
+other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g=
+
+-- a.go --
+package a
+import "other.com/b"
+const _ = b.K
+
+`
+ WithOptions(
+ ProxyFiles(proxy),
+ Modes(Default), // fails in 'experimental' mode
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ // Enable to debug go.sum mismatch, which may appear as
+ // "module lookup disabled by GOPROXY=off", confusingly.
+ if false {
+ env.DumpGoSum(".")
+ }
+
+ env.OpenFile("a.go")
+ refPos := env.RegexpSearch("a.go", "K") // find "b.K" reference
+
+ // Initially, b.K is defined in the module cache.
+ gotFile, _ := env.GoToDefinition("a.go", refPos)
+ wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go"
+ if gotFile != wantCache {
+ t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache)
+ }
+
+ // Run 'go mod vendor' outside the editor.
+ if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, true); err != nil {
+ t.Fatalf("go mod vendor: %v", err)
+ }
+
+ // Synchronize changes to watched files.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // Now, b.K is defined in the vendor tree.
+ gotFile, _ = env.GoToDefinition("a.go", refPos)
+ wantVendor := "vendor/other.com/b/b.go"
+ if gotFile != wantVendor {
+ t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor)
+ }
+
+ // Delete the vendor tree.
+ if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil {
+ t.Fatal(err)
+ }
+ // Notify the server of the deletion.
+ if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil {
+ t.Fatal(err)
+ }
+
+ // Synchronize again.
+ env.Await(env.DoneWithChangeWatchedFiles())
+
+ // b.K is once again defined in the module cache.
+ gotFile, _ = env.GoToDefinition("a.go", refPos)
+ if gotFile != wantCache {
+ t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache)
+ }
+ })
+}
diff --git a/internal/persistent/map.go b/internal/persistent/map.go
index f5dd102..b29cfe4 100644
--- a/internal/persistent/map.go
+++ b/internal/persistent/map.go
@@ -8,7 +8,9 @@
package persistent
import (
+ "fmt"
"math/rand"
+ "strings"
"sync/atomic"
)
@@ -41,6 +43,18 @@
root *mapNode
}
+func (m *Map) String() string {
+ var buf strings.Builder
+ buf.WriteByte('{')
+ var sep string
+ m.Range(func(k, v interface{}) {
+ fmt.Fprintf(&buf, "%s%v: %v", sep, k, v)
+ sep = ", "
+ })
+ buf.WriteByte('}')
+ return buf.String()
+}
+
type mapNode struct {
key interface{}
value *refValue