internal/lsp: use -modfile for import organization

Also add a regression test.

Fixes golang/go#39681.

Change-Id: I685fbe4255b9e8e067dacd84a6d464d0a2bbe1f1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/238737
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 99c3910..6b4d847 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -84,7 +84,7 @@
 	defer done()
 
 	cfg := s.config(ctx)
-	var cleanup func()
+	cleanup := func() {}
 	if s.view.tmpMod {
 		modFH, err := s.GetFile(ctx, s.view.modURI)
 		if err != nil {
@@ -97,17 +97,15 @@
 				return err
 			}
 		}
-		var uri span.URI
-		uri, cleanup, err = tempModFile(modFH, sumFH)
+		var tmpURI span.URI
+		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
 		if err != nil {
 			return err
 		}
-		cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", uri.Filename()))
+		cfg.BuildFlags = append(cfg.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
 	}
 	pkgs, err := packages.Load(cfg, query...)
-	if cleanup != nil {
-		cleanup()
-	}
+	cleanup()
 
 	// If the context was canceled, return early. Otherwise, we might be
 	// type-checking an incomplete result. Check the context directly,
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index fef1c31..4c27c0c 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -62,7 +62,10 @@
 
 	// importsMu guards imports-related state, particularly the ProcessEnv.
 	importsMu sync.Mutex
-	// process is the process env for this view.
+
+	// processEnv is the process env for this view.
+	// Some of its fields can be changed dynamically by modifications to
+	// the view's options. These fields are repopulated for every use.
 	// Note: this contains cached module and filesystem state.
 	//
 	// TODO(suzmue): the state cached in the process env is specific to each view,
@@ -361,22 +364,45 @@
 	v.importsMu.Lock()
 	defer v.importsMu.Unlock()
 
+	// The resolver cached in the process env is reused, but some fields need
+	// to be repopulated for each use.
 	if v.processEnv == nil {
-		var err error
-		if v.processEnv, err = v.buildProcessEnv(ctx); err != nil {
-			return err
-		}
+		v.processEnv = &imports.ProcessEnv{}
 	}
 
-	// In module mode, check if the mod file has changed.
-	if v.modURI != "" {
-		mod, err := v.session.cache.GetFile(ctx, v.modURI)
+	var modFH, sumFH source.FileHandle
+	if v.tmpMod {
+		var err error
+		// Use temporary go.mod files, but always go to disk for the contents.
+		// Rebuilding the cache is expensive, and we don't want to do it for
+		// transient changes.
+		modFH, err = v.session.cache.GetFile(ctx, v.modURI)
 		if err != nil {
 			return err
 		}
-		if mod.Identity() != v.cachedModFileVersion {
+		if v.sumURI != "" {
+			sumFH, err = v.session.cache.GetFile(ctx, v.sumURI)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	cleanup, err := v.populateProcessEnv(ctx, modFH, sumFH)
+	if err != nil {
+		return err
+	}
+	defer cleanup()
+
+	// If the go.mod file has changed, clear the cache.
+	if v.modURI != "" {
+		modFH, err := v.session.cache.GetFile(ctx, v.modURI)
+		if err != nil {
+			return err
+		}
+		if modFH.Identity() != v.cachedModFileVersion {
 			v.processEnv.GetResolver().(*imports.ModuleResolver).ClearForNewMod()
-			v.cachedModFileVersion = mod.Identity()
+			v.cachedModFileVersion = modFH.Identity()
 		}
 	}
 
@@ -431,20 +457,37 @@
 	v.importsMu.Unlock()
 }
 
-func (v *View) buildProcessEnv(ctx context.Context) (*imports.ProcessEnv, error) {
+// populateProcessEnv sets the dynamically configurable fields for the view's
+// process environment. It operates on a snapshot because it needs to access
+// file contents. Assumes that the caller is holding the s.view.importsMu.
+func (v *View) populateProcessEnv(ctx context.Context, modFH, sumFH source.FileHandle) (cleanup func(), err error) {
+	cleanup = func() {}
+
 	v.optionsMu.Lock()
 	env, buildFlags := v.envLocked()
 	localPrefix, verboseOutput := v.options.LocalPrefix, v.options.VerboseOutput
 	v.optionsMu.Unlock()
 
-	processEnv := &imports.ProcessEnv{
-		WorkingDir:  v.folder.Filename(),
-		BuildFlags:  buildFlags,
-		LocalPrefix: localPrefix,
-		GocmdRunner: v.gocmdRunner,
+	pe := v.processEnv
+
+	pe.BuildFlags = buildFlags
+
+	// Add -modfile to the build flags, if we are using it.
+	if modFH != nil {
+		var tmpURI span.URI
+		tmpURI, cleanup, err = tempModFile(modFH, sumFH)
+		if err != nil {
+			return nil, err
+		}
+		pe.BuildFlags = append(pe.BuildFlags, fmt.Sprintf("-modfile=%s", tmpURI.Filename()))
 	}
+
+	pe.WorkingDir = v.folder.Filename()
+	pe.LocalPrefix = localPrefix
+	pe.GocmdRunner = v.gocmdRunner
+
 	if verboseOutput {
-		processEnv.Logf = func(format string, args ...interface{}) {
+		pe.Logf = func(format string, args ...interface{}) {
 			event.Log(ctx, fmt.Sprintf(format, args...))
 		}
 	}
@@ -455,23 +498,23 @@
 		}
 		switch split[0] {
 		case "GOPATH":
-			processEnv.GOPATH = split[1]
+			pe.GOPATH = split[1]
 		case "GOROOT":
-			processEnv.GOROOT = split[1]
+			pe.GOROOT = split[1]
 		case "GO111MODULE":
-			processEnv.GO111MODULE = split[1]
+			pe.GO111MODULE = split[1]
 		case "GOPROXY":
-			processEnv.GOPROXY = split[1]
+			pe.GOPROXY = split[1]
 		case "GOFLAGS":
-			processEnv.GOFLAGS = split[1]
+			pe.GOFLAGS = split[1]
 		case "GOSUMDB":
-			processEnv.GOSUMDB = split[1]
+			pe.GOSUMDB = split[1]
 		}
 	}
-	if processEnv.GOPATH == "" {
+	if pe.GOPATH == "" {
 		return nil, fmt.Errorf("no GOPATH for view %s", v.folder)
 	}
-	return processEnv, nil
+	return cleanup, nil
 }
 
 // envLocked returns the environment and build flags for the current view.
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index a2285be..6dc9aed 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -70,12 +70,12 @@
 	// Ensure that the reports returned from mod.Diagnostics are only related
 	// to the go.mod file for the module.
 	if len(reports) > 1 {
-		panic("unexpected reports from mod.Diagnostics")
+		panic(fmt.Sprintf("expected 1 report from mod.Diagnostics, got %v: %v", len(reports), reports))
 	}
 	modURI := snapshot.View().ModFile()
 	for id, diags := range reports {
 		if id.URI != modURI {
-			panic("unexpected reports from mod.Diagnostics")
+			panic(fmt.Sprintf("expected module diagnostics report for %q, got %q", modURI, id.URI))
 		}
 		key := diagnosticKey{
 			id: id,
diff --git a/internal/lsp/regtest/modfile_test.go b/internal/lsp/regtest/modfile_test.go
new file mode 100644
index 0000000..47a0ed2
--- /dev/null
+++ b/internal/lsp/regtest/modfile_test.go
@@ -0,0 +1,60 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package regtest
+
+import (
+	"testing"
+
+	"golang.org/x/tools/internal/lsp/tests"
+	"golang.org/x/tools/internal/testenv"
+)
+
+func TestModFileModification(t *testing.T) {
+	testenv.NeedsGo1Point(t, 14)
+
+	const proxy = `
+-- example.com@v1.2.3/go.mod --
+module example.com
+
+go 1.12
+-- example.com@v1.2.3/blah/blah.go --
+package blah
+
+const Name = "Blah"
+`
+	const untidyModule = `
+-- go.mod --
+module mod.com
+
+-- main.go --
+package main
+
+import "example.com/blah"
+
+func main() {
+	fmt.Println(blah.Name)
+}`
+	runner.Run(t, untidyModule, func(t *testing.T, env *Env) {
+		// Open the file and make sure that the initial workspace load does not
+		// modify the go.mod file.
+		goModContent := env.ReadWorkspaceFile("go.mod")
+		env.OpenFile("main.go")
+		env.Await(
+			env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
+		)
+		if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
+			t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(want, got))
+		}
+		// Save the buffer, which will format and organize imports.
+		// Confirm that the go.mod file still does not change.
+		env.SaveBuffer("main.go")
+		env.Await(
+			env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
+		)
+		if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
+			t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(want, got))
+		}
+	}, WithProxy(proxy))
+}
diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go
index f1c7502..827dcdb 100644
--- a/internal/lsp/regtest/wrappers.go
+++ b/internal/lsp/regtest/wrappers.go
@@ -34,8 +34,8 @@
 	return content
 }
 
-// RemoveFileFromWorkspace deletes a file on disk but does nothing in the
-// editor. It calls t.Fatal on any error.
+// WriteWorkspaceFile writes a file to disk but does nothing in the editor.
+// It calls t.Fatal on any error.
 func (e *Env) WriteWorkspaceFile(name, content string) {
 	e.T.Helper()
 	if err := e.Sandbox.Workdir.WriteFile(e.Ctx, name, content); err != nil {