internal/lsp/cache: fix for default -mod=readonly
Go 1.16 may set -mod=readonly by default. To maintain current behavior,
gopls needs to override that by passing -mod=mod to all its go
invocations.
While this behavior should be safe on all modern versions of Go, I gated
it on 1.16 just for safety's sake.
Change-Id: Ic8088213d1ab9ab3a3ed0b51f47b2c222974d613
Reviewed-on: https://go-review.googlesource.com/c/tools/+/253799
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 52ff7ee..ff5a208 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -91,6 +91,22 @@
cfg := s.config(ctx)
cleanup := func() {}
+
+ var modFH, sumFH source.FileHandle
+ var err error
+ if s.view.modURI != "" {
+ modFH, err = s.GetFile(ctx, s.view.modURI)
+ if err != nil {
+ return err
+ }
+ }
+ if s.view.sumURI != "" {
+ sumFH, err = s.GetFile(ctx, s.view.sumURI)
+ if err != nil {
+ return err
+ }
+ }
+
switch {
case s.view.workspaceMode&workspaceModule != 0:
var (
@@ -103,17 +119,6 @@
}
cfg.Dir = tmpDir.Filename()
case s.view.workspaceMode&tempModfile != 0:
- modFH, err := s.GetFile(ctx, s.view.modURI)
- if err != nil {
- return err
- }
- var sumFH source.FileHandle
- if s.view.sumURI != "" {
- sumFH, err = s.GetFile(ctx, s.view.sumURI)
- if err != nil {
- return err
- }
- }
var tmpURI span.URI
tmpURI, cleanup, err = tempModFile(modFH, sumFH)
if err != nil {
@@ -121,6 +126,15 @@
}
cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
}
+
+ modMod, err := s.view.needsModEqualsMod(ctx, modFH)
+ if err != nil {
+ return err
+ }
+ if modMod {
+ cfg.BuildFlags = append([]string{"-mod=mod"}, cfg.BuildFlags...)
+ }
+
pkgs, err := packages.Load(cfg, query...)
cleanup()
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index a0aa93f..01d6c6f 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -201,7 +201,7 @@
}
// Set the module-specific information.
- if err := v.setBuildInformation(ctx, folder, options); err != nil {
+ if err := v.setBuildInformation(ctx, options); err != nil {
return nil, nil, func() {}, err
}
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index b72b62c..4d336f2 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -164,7 +164,6 @@
cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo)
}
packagesinternal.SetGoCmdRunner(cfg, s.view.session.gocmdRunner)
-
return cfg
}
@@ -218,6 +217,19 @@
}
cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
}
+ if s.view.modURI != "" && verb != "mod" && verb != "get" {
+ modFH, err := s.GetFile(ctx, s.view.modURI)
+ if err != nil {
+ return "", nil, nil, cleanup, err
+ }
+ modMod, err := s.view.needsModEqualsMod(ctx, modFH)
+ if err != nil {
+ return "", nil, nil, cleanup, err
+ }
+ if modMod {
+ cfg.BuildFlags = append([]string{"-mod=mod"}, cfg.BuildFlags...)
+ }
+ }
runner = packagesinternal.GetGoCmdRunner(cfg)
return tmpURI, runner, &gocommand.Invocation{
Verb: verb,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 40baa94..945cdd4 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -9,6 +9,7 @@
"context"
"encoding/json"
"fmt"
+ "go/build"
"io"
"io/ioutil"
"os"
@@ -16,16 +17,17 @@
"path"
"path/filepath"
"reflect"
+ "regexp"
"strings"
"sync"
"time"
"golang.org/x/mod/modfile"
+ "golang.org/x/mod/semver"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/keys"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
- "golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/span"
@@ -133,6 +135,9 @@
// The real go.mod and go.sum files that are attributed to a view.
modURI, sumURI span.URI
+ // The Go version in use: X in Go 1.X.
+ goversion int
+
// workspaceMode describes the way in which the view's workspace should be
// loaded.
workspaceMode workspaceMode
@@ -463,6 +468,20 @@
}
v.optionsMu.Unlock()
+ pe.Env = map[string]string{}
+ for k, v := range v.goEnv {
+ pe.Env[k] = v
+ }
+ modmod, err := v.needsModEqualsMod(ctx, modFH)
+ if err != nil {
+ return cleanup, err
+ }
+ if modmod {
+ // -mod isn't really a build flag, but we can get away with it given
+ // the set of commands that goimports wants to run.
+ pe.BuildFlags = append([]string{"-mod=mod"}, pe.BuildFlags...)
+ }
+
// Add -modfile to the build flags, if we are using it.
if v.workspaceMode&tempModfile != 0 && modFH != nil {
var tmpURI span.URI
@@ -765,10 +784,15 @@
v.initializeOnce = &once
}
-func (v *View) setBuildInformation(ctx context.Context, folder span.URI, options source.Options) error {
- if err := checkPathCase(folder.Filename()); err != nil {
+func (v *View) setBuildInformation(ctx context.Context, options source.Options) error {
+ if err := checkPathCase(v.Folder().Filename()); err != nil {
return errors.Errorf("invalid workspace configuration: %w", err)
}
+ var err error
+ v.goversion, err = v.goVersion(ctx, v.Options().Env)
+ if err != nil {
+ return err
+ }
// Make sure to get the `go env` before continuing with initialization.
modFile, err := v.setGoEnv(ctx, options.Env)
if err != nil {
@@ -793,9 +817,7 @@
return nil
}
v.workspaceMode = standard
- if modfileFlag, err := v.modfileFlagExists(ctx, v.Options().Env); err != nil {
- return err
- } else if modfileFlag {
+ if v.goversion >= 14 {
v.workspaceMode |= tempModfile
}
return nil
@@ -945,26 +967,66 @@
// This function will return the main go.mod file for this folder if it exists
// and whether the -modfile flag exists for this version of go.
-func (v *View) modfileFlagExists(ctx context.Context, env []string) (bool, error) {
+func (v *View) goVersion(ctx context.Context, env []string) (int, error) {
// Check the go version by running "go list" with modules off.
// Borrowed from internal/imports/mod.go:620.
- const format = `{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}`
- folder := v.folder.Filename()
+ const format = `{{context.ReleaseTags}}`
inv := gocommand.Invocation{
Verb: "list",
Args: []string{"-e", "-f", format},
Env: append(env, "GO111MODULE=off"),
WorkingDir: v.root.Filename(),
}
- stdout, err := v.session.gocmdRunner.Run(ctx, inv)
+ stdoutBytes, err := v.session.gocmdRunner.Run(ctx, inv)
+ if err != nil {
+ return 0, err
+ }
+ stdout := stdoutBytes.String()
+ if len(stdout) < 3 {
+ return 0, fmt.Errorf("bad ReleaseTags output: %q", stdout)
+ }
+ // Split up "[go1.1 go1.15]"
+ tags := strings.Fields(stdout[1 : len(stdout)-2])
+ for i := len(tags) - 1; i >= 0; i-- {
+ var version int
+ if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil {
+ continue
+ }
+ return version, nil
+ }
+ return 0, fmt.Errorf("no parseable ReleaseTags in %v", tags)
+}
+
+var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)
+
+func (v *View) needsModEqualsMod(ctx context.Context, modFH source.FileHandle) (bool, error) {
+ if v.goversion < 16 || modFH == nil {
+ return false, nil
+ }
+
+ matches := modFlagRegexp.FindStringSubmatch(v.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
+ }
+
+ modBytes, err := modFH.Read()
if err != nil {
return false, err
}
- // If the output is not go1.14 or an empty string, then it could be an error.
- lines := strings.Split(stdout.String(), "\n")
- if len(lines) < 2 && stdout.String() != "" {
- event.Error(ctx, "unexpected stdout when checking for go1.14", errors.Errorf("%q", stdout), tag.Directory.Of(folder))
- return false, nil
+ modFile, err := modfile.Parse(modFH.URI().Filename(), modBytes, nil)
+ if err != nil {
+ return false, err
}
- return lines[0] == "go1.14", nil
+ if fi, err := os.Stat(filepath.Join(filepath.Dir(v.modURI.Filename()), "vendor")); err != nil || !fi.IsDir() {
+ return true, nil
+ }
+ vendorEnabled := modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0
+ return !vendorEnabled, nil
}