internal/lsp/cache: use gopls.mod for the workspace module if it exists
When building the workspace module, prefer a gopls.mod file located at
the root of the view if it exists.
Also do some minor documenting/cleanup along the way.
For golang/go#32394
Change-Id: If87729a766d37e6c834fefe40cfb47b67a36a60c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/256582
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Trust: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/workspace_test.go b/gopls/internal/regtest/workspace_test.go
index 8629917..72c3ec1 100644
--- a/gopls/internal/regtest/workspace_test.go
+++ b/gopls/internal/regtest/workspace_test.go
@@ -358,3 +358,73 @@
)
})
}
+
+func TestUseGoplsMod(t *testing.T) {
+ const multiModule = `
+-- moda/a/go.mod --
+module a.com
+
+require b.com v1.2.3
+
+-- moda/a/a.go --
+package a
+
+import (
+ "b.com/b"
+)
+
+func main() {
+ var x int
+ _ = b.Hello()
+}
+-- modb/go.mod --
+module b.com
+
+-- modb/b/b.go --
+package b
+
+func Hello() int {
+ var x int
+}
+-- gopls.mod --
+module gopls-workspace
+
+require (
+ a.com v0.0.0-goplsworkspace
+ b.com v1.2.3
+)
+
+replace a.com => $SANDBOX_WORKDIR/moda/a
+`
+ withOptions(
+ WithProxyFiles(workspaceModuleProxy),
+ ).run(t, multiModule, func(t *testing.T, env *Env) {
+ env.Await(InitialWorkspaceLoad)
+ env.OpenFile("moda/a/a.go")
+ original, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) {
+ t.Errorf("expected %s, got %v", want, original)
+ }
+ workdir := env.Sandbox.Workdir.RootURI().SpanURI().Filename()
+ env.WriteWorkspaceFile("gopls.mod", fmt.Sprintf(`module gopls-workspace
+
+require (
+ a.com v0.0.0-goplsworkspace
+ b.com v0.0.0-goplsworkspace
+)
+
+replace a.com => %s/moda/a
+replace b.com => %s/modb
+`, workdir, workdir))
+ env.Await(
+ OnceMet(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1),
+ env.DiagnosticAtRegexp("modb/b/b.go", "x"),
+ ),
+ )
+ newLocation, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "modb/b/b.go"; !strings.HasSuffix(newLocation, want) {
+ t.Errorf("expected %s, got %v", want, newLocation)
+ }
+ })
+}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 86ba0f0..c80cd4c 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -23,6 +23,7 @@
errors "golang.org/x/xerrors"
)
+// metadata holds package metadata extracted from a call to packages.Load.
type metadata struct {
id packageID
pkgPath packagePath
@@ -40,6 +41,8 @@
config *packages.Config
}
+// load calls packages.Load for the given scopes, updating package metadata,
+// import graph, and mapped files with the result.
func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
var query []string
var containsDir bool // for logging
@@ -234,6 +237,9 @@
return span.URIFromPath(filepath.Dir(filename)), cleanup, nil
}
+// setMetadata extracts metadata from pkg and records it in s. It
+// recurses through pkg.Imports to ensure that metadata exists for all
+// dependencies.
func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) {
id := packageID(pkg.ID)
if _, ok := seen[id]; ok {
@@ -262,6 +268,7 @@
s.addID(uri, m.id)
}
+ // TODO(rstambler): is this still necessary?
copied := map[packageID]struct{}{
id: {},
}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 368be08..5cc18bc 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -733,8 +733,11 @@
return s.files[f.URI()]
}
-// GetFile returns a File for the given URI. It will always succeed because it
-// adds the file to the managed set if needed.
+// GetFile returns a File for the given URI. If the file is unknown it is added
+// to the managed set.
+//
+// GetFile succeeds even if the file does not exist. A non-nil error return
+// indicates some type of internal error, for example if ctx is cancelled.
func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.VersionedFileHandle, error) {
f, err := s.view.getFile(uri)
if err != nil {
@@ -1426,6 +1429,25 @@
}
fhs = append(fhs, fh)
}
+ goplsModURI := span.URIFromPath(filepath.Join(s.view.Folder().Filename(), "gopls.mod"))
+ goplsModFH, err := s.GetFile(ctx, goplsModURI)
+ if err != nil {
+ return nil, err
+ }
+ _, err = goplsModFH.Read()
+ switch {
+ case err == nil:
+ // We have a gopls.mod. Our handle only depends on it.
+ fhs = []source.FileHandle{goplsModFH}
+ case os.IsNotExist(err):
+ // No gopls.mod, so we must build the workspace mod file automatically.
+ // Defensively ensure that the goplsModFH is nil as this controls automatic
+ // building of the workspace mod file.
+ goplsModFH = nil
+ default:
+ return nil, errors.Errorf("error getting gopls.mod: %w", err)
+ }
+
sort.Slice(fhs, func(i, j int) bool {
return fhs[i].URI() < fhs[j].URI()
})
@@ -1435,6 +1457,13 @@
}
key := workspaceModuleKey(hashContents([]byte(k)))
h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
+ if goplsModFH != nil {
+ parsed, err := s.ParseMod(ctx, goplsModFH)
+ if err != nil {
+ return &workspaceModuleData{err: err}
+ }
+ return &workspaceModuleData{file: parsed.File}
+ }
s := arg.(*snapshot)
data := &workspaceModuleData{}
data.file, data.err = s.BuildWorkspaceModFile(ctx)
@@ -1450,7 +1479,7 @@
}
// BuildWorkspaceModFile generates a workspace module given the modules in the
-// the workspace.
+// the workspace. It does not read gopls.mod.
func (s *snapshot) BuildWorkspaceModFile(ctx context.Context) (*modfile.File, error) {
file := &modfile.File{}
file.AddModuleStmt("gopls-workspace")
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index a8f3b07..abf6f4c 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -314,11 +314,8 @@
copy(bBuildFlags, b.BuildFlags)
sort.Strings(aBuildFlags)
sort.Strings(bBuildFlags)
- if !reflect.DeepEqual(aBuildFlags, bBuildFlags) {
- return false
- }
// the rest of the options are benign
- return true
+ return reflect.DeepEqual(aBuildFlags, bBuildFlags)
}
func (v *View) SetOptions(ctx context.Context, options *source.Options) (source.View, error) {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index c4731fc..f098921 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -407,9 +407,8 @@
URI() span.URI
Kind() FileKind
- // Identity returns a FileIdentity for the file, even if there was an error
- // reading it.
- // It is a fatal error to call Identity on a file that has not yet been read.
+ // FileIdentity returns a FileIdentity for the file, even if there was an
+ // error reading it.
FileIdentity() FileIdentity
// Read reads the contents of a file.
// If the file is not available, returns a nil slice and an error.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 2b9a77a..4606e25 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -215,7 +215,7 @@
for _, s := range snapshots {
if s.View() == view {
if snapshot != nil {
- return errors.Errorf("duplicate snapshots for the same view")
+ return errors.New("duplicate snapshots for the same view")
}
snapshot = s
}