internal/lsp/cache: consider gopls.mod when finding workspace root
gopls.mod files should take precedence over go.mod files when finding a
workspace root. To allow this, implement our own algorithm for expanding
the workspace, rather than using the go command.
For golang/go#41837
Change-Id: I943c08bdbdbdd164f108e44bae95d2c659a6e21e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/263897
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Trust: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 448273a..4b97ee6 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -126,7 +126,7 @@
}
type environmentVariables struct {
- gocache, gopath, goprivate, gomodcache, gomod string
+ gocache, gopath, goprivate, gomodcache string
}
type workspaceMode int
@@ -651,13 +651,15 @@
tool, _ := exec.LookPath("gopackagesdriver")
hasGopackagesDriver := gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "")
- var modURI span.URI
- if envVars.gomod != os.DevNull && envVars.gomod != "" {
- modURI = span.URIFromPath(envVars.gomod)
- }
root := folder
- if options.ExpandWorkspaceToModule && modURI != "" {
- root = span.URIFromPath(filepath.Dir(modURI.Filename()))
+ if options.ExpandWorkspaceToModule {
+ wsRoot, err := findWorkspaceRoot(ctx, root, s)
+ if err != nil {
+ return nil, err
+ }
+ if wsRoot != "" {
+ root = wsRoot
+ }
}
return &workspaceInformation{
hasGopackagesDriver: hasGopackagesDriver,
@@ -669,6 +671,39 @@
}, nil
}
+func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource) (span.URI, error) {
+ for _, basename := range []string{"gopls.mod", "go.mod"} {
+ dir, err := findRootPattern(ctx, folder, basename, fs)
+ if err != nil {
+ return "", errors.Errorf("finding %s: %w", basename, err)
+ }
+ if dir != "" {
+ return dir, nil
+ }
+ }
+ return "", nil
+}
+
+func findRootPattern(ctx context.Context, folder span.URI, basename string, fs source.FileSource) (span.URI, error) {
+ dir := folder.Filename()
+ for dir != "" {
+ target := filepath.Join(dir, basename)
+ exists, err := fileExists(ctx, span.URIFromPath(target), fs)
+ if err != nil {
+ return "", err
+ }
+ if exists {
+ return span.URIFromPath(dir), nil
+ }
+ next, _ := filepath.Split(dir)
+ if next == dir {
+ break
+ }
+ dir = next
+ }
+ return "", nil
+}
+
// OS-specific path case check, for case-insensitive filesystems.
var checkPathCase = defaultCheckPathCase
@@ -710,7 +745,6 @@
"GOPATH": &envVars.gopath,
"GOPRIVATE": &envVars.goprivate,
"GOMODCACHE": &envVars.gomodcache,
- "GOMOD": &envVars.gomod,
}
// We can save ~200 ms by requesting only the variables we care about.
args := append([]string{"-json"}, imports.RequiredGoEnvVars...)
diff --git a/internal/lsp/cache/view_test.go b/internal/lsp/cache/view_test.go
index 0dad594..87774e6 100644
--- a/internal/lsp/cache/view_test.go
+++ b/internal/lsp/cache/view_test.go
@@ -4,10 +4,14 @@
package cache
import (
+ "context"
"io/ioutil"
"os"
"path/filepath"
"testing"
+
+ "golang.org/x/tools/internal/lsp/fake"
+ "golang.org/x/tools/internal/span"
)
func TestCaseInsensitiveFilesystem(t *testing.T) {
@@ -43,3 +47,50 @@
}
}
}
+
+func TestFindWorkspaceRoot(t *testing.T) {
+ workspace := `
+-- a/go.mod --
+module a
+-- a/x/x.go
+package x
+-- b/go.mod --
+module b
+-- b/c/go.mod --
+module bc
+-- d/gopls.mod --
+module d-goplsworkspace
+-- d/e/go.mod
+module de
+`
+ dir, err := fake.Tempdir(workspace)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ tests := []struct {
+ folder, want string
+ }{
+ // no module at root.
+ {"", ""},
+ {"a", "a"},
+ {"a/x", "a"},
+ {"b/c", "b/c"},
+ {"d", "d"},
+ {"d/e", "d"},
+ }
+
+ for _, test := range tests {
+ ctx := context.Background()
+ rel := fake.RelativeTo(dir)
+ folderURI := span.URIFromPath(rel.AbsPath(test.folder))
+ got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rel.RelPath(got.Filename()) != test.want {
+ t.Errorf("fileWorkspaceRoot(%q) = %q, want %q", test.folder, got, test.want)
+ }
+ }
+}