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
 }