internal/imports: require valid options, move LocalPrefix up

CL 239754 eagerly initialized the environment. This turns out to be a
problem for gopls, which calls ApplyFixes with no ProcessEnv.
Reinitializing it every time seriously harmed the performance of
unimported completions. Move back to lazy initialization.

Working with invalid options has caused a lot of confusion; this is only
the most recent. We have to maintain backwards compatibility in the
externally visible API, but everywhere else we can require fully
populated options. That includes the source byte slice and the options.

LocalPrefix is really more of an Option than an attribute of the
ProcessEnv, and it is needed in ApplyFixes where we really don't want to
have to pass a ProcessEnv. Move it up to Options.

Change-Id: Ib9466c375a640a521721da4587091bf93bbdaa3c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/241159
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/cmd/goimports/goimports.go b/cmd/goimports/goimports.go
index f177b2d..2770897 100644
--- a/cmd/goimports/goimports.go
+++ b/cmd/goimports/goimports.go
@@ -21,6 +21,7 @@
 	"runtime/pprof"
 	"strings"
 
+	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/imports"
 )
 
@@ -42,14 +43,16 @@
 		TabIndent: true,
 		Comments:  true,
 		Fragment:  true,
-		Env: &imports.ProcessEnv{},
+		Env: &imports.ProcessEnv{
+			GocmdRunner: &gocommand.Runner{},
+		},
 	}
 	exitCode = 0
 )
 
 func init() {
 	flag.BoolVar(&options.AllErrors, "e", false, "report all errors (not just the first 10 on different lines)")
-	flag.StringVar(&options.Env.LocalPrefix, "local", "", "put imports beginning with this string after 3rd-party packages; comma-separated list")
+	flag.StringVar(&options.LocalPrefix, "local", "", "put imports beginning with this string after 3rd-party packages; comma-separated list")
 	flag.BoolVar(&options.FormatOnly, "format-only", false, "if true, don't fix imports and only format. In this mode, goimports is effectively gofmt, with the addition that imports are grouped into sections.")
 }
 
diff --git a/imports/forward.go b/imports/forward.go
index 83f4e44..a4e40ad 100644
--- a/imports/forward.go
+++ b/imports/forward.go
@@ -3,8 +3,10 @@
 package imports // import "golang.org/x/tools/imports"
 
 import (
+	"io/ioutil"
 	"log"
 
+	"golang.org/x/tools/internal/gocommand"
 	intimp "golang.org/x/tools/internal/imports"
 )
 
@@ -29,25 +31,34 @@
 var LocalPrefix string
 
 // Process formats and adjusts imports for the provided file.
-// If opt is nil the defaults are used.
+// If opt is nil the defaults are used, and if src is nil the source
+// is read from the filesystem.
 //
 // Note that filename's directory influences which imports can be chosen,
 // so it is important that filename be accurate.
 // To process data ``as if'' it were in filename, pass the data as a non-nil src.
 func Process(filename string, src []byte, opt *Options) ([]byte, error) {
+	var err error
+	if src == nil {
+		src, err = ioutil.ReadFile(filename)
+		if err != nil {
+			return nil, err
+		}
+	}
 	if opt == nil {
 		opt = &Options{Comments: true, TabIndent: true, TabWidth: 8}
 	}
 	intopt := &intimp.Options{
 		Env: &intimp.ProcessEnv{
-			LocalPrefix: LocalPrefix,
+			GocmdRunner: &gocommand.Runner{},
 		},
-		AllErrors:  opt.AllErrors,
-		Comments:   opt.Comments,
-		FormatOnly: opt.FormatOnly,
-		Fragment:   opt.Fragment,
-		TabIndent:  opt.TabIndent,
-		TabWidth:   opt.TabWidth,
+		LocalPrefix: LocalPrefix,
+		AllErrors:   opt.AllErrors,
+		Comments:    opt.Comments,
+		FormatOnly:  opt.FormatOnly,
+		Fragment:    opt.Fragment,
+		TabIndent:   opt.TabIndent,
+		TabWidth:    opt.TabWidth,
 	}
 	if Debug {
 		intopt.Env.Logf = log.Printf
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index 2c70307..ecd13e8 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -32,25 +32,25 @@
 
 // importToGroup is a list of functions which map from an import path to
 // a group number.
-var importToGroup = []func(env *ProcessEnv, importPath string) (num int, ok bool){
-	func(env *ProcessEnv, importPath string) (num int, ok bool) {
-		if env.LocalPrefix == "" {
+var importToGroup = []func(localPrefix, importPath string) (num int, ok bool){
+	func(localPrefix, importPath string) (num int, ok bool) {
+		if localPrefix == "" {
 			return
 		}
-		for _, p := range strings.Split(env.LocalPrefix, ",") {
+		for _, p := range strings.Split(localPrefix, ",") {
 			if strings.HasPrefix(importPath, p) || strings.TrimSuffix(p, "/") == importPath {
 				return 3, true
 			}
 		}
 		return
 	},
-	func(_ *ProcessEnv, importPath string) (num int, ok bool) {
+	func(_, importPath string) (num int, ok bool) {
 		if strings.HasPrefix(importPath, "appengine") {
 			return 2, true
 		}
 		return
 	},
-	func(_ *ProcessEnv, importPath string) (num int, ok bool) {
+	func(_, importPath string) (num int, ok bool) {
 		firstComponent := strings.Split(importPath, "/")[0]
 		if strings.Contains(firstComponent, ".") {
 			return 1, true
@@ -59,9 +59,9 @@
 	},
 }
 
-func importGroup(env *ProcessEnv, importPath string) int {
+func importGroup(localPrefix, importPath string) int {
 	for _, fn := range importToGroup {
-		if n, ok := fn(env, importPath); ok {
+		if n, ok := fn(localPrefix, importPath); ok {
 			return n
 		}
 	}
@@ -278,7 +278,12 @@
 		unknown = append(unknown, imp.ImportPath)
 	}
 
-	names, err := p.env.GetResolver().loadPackageNames(unknown, p.srcDir)
+	resolver, err := p.env.GetResolver()
+	if err != nil {
+		return err
+	}
+
+	names, err := resolver.loadPackageNames(unknown, p.srcDir)
 	if err != nil {
 		return err
 	}
@@ -640,15 +645,23 @@
 			wrappedCallback.exportsLoaded(pkg, exports)
 		},
 	}
-	return env.GetResolver().scan(ctx, scanFilter)
+	resolver, err := env.GetResolver()
+	if err != nil {
+		return err
+	}
+	return resolver.scan(ctx, scanFilter)
 }
 
-func ScoreImportPaths(ctx context.Context, env *ProcessEnv, paths []string) map[string]int {
+func ScoreImportPaths(ctx context.Context, env *ProcessEnv, paths []string) (map[string]int, error) {
 	result := make(map[string]int)
-	for _, path := range paths {
-		result[path] = env.GetResolver().scoreImportPath(ctx, path)
+	resolver, err := env.GetResolver()
+	if err != nil {
+		return nil, err
 	}
-	return result
+	for _, path := range paths {
+		result[path] = resolver.scoreImportPath(ctx, path)
+	}
+	return result, nil
 }
 
 func PrimeCache(ctx context.Context, env *ProcessEnv) error {
@@ -674,8 +687,9 @@
 	return ""
 }
 
-// getAllCandidates gets all of the candidates to be imported, regardless of if they are needed.
-func getAllCandidates(ctx context.Context, wrapped func(ImportFix), searchPrefix, filename, filePkg string, env *ProcessEnv) error {
+// GetAllCandidates gets all of the packages starting with prefix that can be
+// imported by filename, sorted by import path.
+func GetAllCandidates(ctx context.Context, wrapped func(ImportFix), searchPrefix, filename, filePkg string, env *ProcessEnv) error {
 	callback := &scanCallback{
 		rootFound: func(gopathwalk.Root) bool {
 			return true
@@ -714,7 +728,8 @@
 	Exports []string
 }
 
-func getPackageExports(ctx context.Context, wrapped func(PackageExport), searchPkg, filename, filePkg string, env *ProcessEnv) error {
+// GetPackageExports returns all known packages with name pkg and their exports.
+func GetPackageExports(ctx context.Context, wrapped func(PackageExport), searchPkg, filename, filePkg string, env *ProcessEnv) error {
 	callback := &scanCallback{
 		rootFound: func(gopathwalk.Root) bool {
 			return true
@@ -749,8 +764,6 @@
 // ProcessEnv contains environment variables and settings that affect the use of
 // the go command, the go/build package, etc.
 type ProcessEnv struct {
-	LocalPrefix string
-
 	GocmdRunner *gocommand.Runner
 
 	BuildFlags []string
@@ -830,16 +843,19 @@
 	return env
 }
 
-func (e *ProcessEnv) GetResolver() Resolver {
+func (e *ProcessEnv) GetResolver() (Resolver, error) {
 	if e.resolver != nil {
-		return e.resolver
+		return e.resolver, nil
+	}
+	if err := e.init(); err != nil {
+		return nil, err
 	}
 	if len(e.Env["GOMOD"]) == 0 {
 		e.resolver = newGopathResolver(e)
-		return e.resolver
+		return e.resolver, nil
 	}
 	e.resolver = newModuleResolver(e)
-	return e.resolver
+	return e.resolver, nil
 }
 
 func (e *ProcessEnv) buildContext() *build.Context {
@@ -964,10 +980,13 @@
 			return false // We'll do our own loading after we sort.
 		},
 	}
-	err := pass.env.GetResolver().scan(context.Background(), callback)
+	resolver, err := pass.env.GetResolver()
 	if err != nil {
 		return err
 	}
+	if err = resolver.scan(context.Background(), callback); err != nil {
+		return err
+	}
 
 	// Search for imports matching potential package references.
 	type result struct {
@@ -1408,6 +1427,10 @@
 			pass.env.Logf("%s candidate %d/%d: %v in %v", pkgName, i+1, len(candidates), c.pkg.importPathShort, c.pkg.dir)
 		}
 	}
+	resolver, err := pass.env.GetResolver()
+	if err != nil {
+		return nil, err
+	}
 
 	// Collect exports for packages with matching names.
 	rescv := make([]chan *pkg, len(candidates))
@@ -1446,7 +1469,7 @@
 				}
 				// If we're an x_test, load the package under test's test variant.
 				includeTest := strings.HasSuffix(pass.f.Name.Name, "_test") && c.pkg.dir == pass.srcDir
-				_, exports, err := pass.env.GetResolver().loadExports(ctx, c.pkg, includeTest)
+				_, exports, err := resolver.loadExports(ctx, c.pkg, includeTest)
 				if err != nil {
 					if pass.env.Logf != nil {
 						pass.env.Logf("loading exports in dir %s (seeking package %s): %v", c.pkg.dir, pkgName, err)
diff --git a/internal/imports/fix_test.go b/internal/imports/fix_test.go
index 7800cce..170b6d8 100644
--- a/internal/imports/fix_test.go
+++ b/internal/imports/fix_test.go
@@ -8,6 +8,8 @@
 	"context"
 	"flag"
 	"fmt"
+	"go/build"
+	"io/ioutil"
 	"log"
 	"path/filepath"
 	"reflect"
@@ -1157,13 +1159,6 @@
 	const localPrefix = "local.com,github.com/local"
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			options := &Options{
-				TabWidth:   8,
-				TabIndent:  true,
-				Comments:   true,
-				Fragment:   true,
-				FormatOnly: tt.formatOnly,
-			}
 			testConfig{
 				modules: []packagestest.Module{
 					{
@@ -1201,7 +1196,14 @@
 					},
 				},
 			}.test(t, func(t *goimportTest) {
-				t.env.LocalPrefix = localPrefix
+				options := &Options{
+					LocalPrefix: localPrefix,
+					TabWidth:    8,
+					TabIndent:   true,
+					Comments:    true,
+					Fragment:    true,
+					FormatOnly:  tt.formatOnly,
+				}
 				t.assertProcessEquals("golang.org/fake", "x.go", nil, options, tt.out)
 			})
 
@@ -1638,12 +1640,13 @@
 				},
 				exported: exported,
 			}
-			if err := it.env.init(); err != nil {
-				t.Fatal(err)
-			}
 			if *testDebug {
 				it.env.Logf = log.Printf
 			}
+			// packagestest clears out GOROOT to work around golang/go#32849,
+			// which isn't relevant here. Fill it back in so we can find the standard library.
+			it.env.Env["GOROOT"] = build.Default.GOROOT
+
 			fn(it)
 		})
 	}
@@ -1673,6 +1676,13 @@
 }
 
 func (t *goimportTest) processNonModule(file string, contents []byte, opts *Options) ([]byte, error) {
+	if contents == nil {
+		var err error
+		contents, err = ioutil.ReadFile(file)
+		if err != nil {
+			return nil, err
+		}
+	}
 	if opts == nil {
 		opts = &Options{Comments: true, TabIndent: true, TabWidth: 8}
 	}
@@ -1869,8 +1879,14 @@
 					Files: fm{"t.go": tt.src},
 				}}, tt.modules...),
 			}.test(t, func(t *goimportTest) {
-				t.env.LocalPrefix = tt.localPrefix
-				t.assertProcessEquals("test.com", "t.go", nil, nil, tt.want)
+				options := &Options{
+					LocalPrefix: tt.localPrefix,
+					TabWidth:    8,
+					TabIndent:   true,
+					Comments:    true,
+					Fragment:    true,
+				}
+				t.assertProcessEquals("test.com", "t.go", nil, options, tt.want)
 			})
 		})
 	}
@@ -1920,7 +1936,10 @@
 		if strings.Contains(t.Name(), "GoPackages") {
 			t.Skip("go/packages does not ignore package main")
 		}
-		r := t.env.GetResolver()
+		r, err := t.env.GetResolver()
+		if err != nil {
+			t.Fatal(err)
+		}
 		srcDir := filepath.Dir(t.exported.File("example.net/pkg", "z.go"))
 		names, err := r.loadPackageNames([]string{"example.net/pkg"}, srcDir)
 		if err != nil {
@@ -2577,7 +2596,7 @@
 				}
 			}
 		}
-		if err := getAllCandidates(context.Background(), add, "", "x.go", "x", t.env); err != nil {
+		if err := GetAllCandidates(context.Background(), add, "", "x.go", "x", t.env); err != nil {
 			t.Fatalf("GetAllCandidates() = %v", err)
 		}
 		// Sort, then clear out relevance so it doesn't mess up the DeepEqual.
@@ -2628,7 +2647,7 @@
 				}
 			}
 		}
-		if err := getPackageExports(context.Background(), add, "rand", "x.go", "x", t.env); err != nil {
+		if err := GetPackageExports(context.Background(), add, "rand", "x.go", "x", t.env); err != nil {
 			t.Fatalf("getPackageCompletions() = %v", err)
 		}
 		// Sort, then clear out relevance so it doesn't mess up the DeepEqual.
diff --git a/internal/imports/imports.go b/internal/imports/imports.go
index 04ecdfd..2815edc 100644
--- a/internal/imports/imports.go
+++ b/internal/imports/imports.go
@@ -11,7 +11,6 @@
 import (
 	"bufio"
 	"bytes"
-	"context"
 	"fmt"
 	"go/ast"
 	"go/format"
@@ -19,19 +18,22 @@
 	"go/printer"
 	"go/token"
 	"io"
-	"io/ioutil"
 	"regexp"
 	"strconv"
 	"strings"
 
 	"golang.org/x/tools/go/ast/astutil"
-	"golang.org/x/tools/internal/gocommand"
 )
 
 // Options is golang.org/x/tools/imports.Options with extra internal-only options.
 type Options struct {
 	Env *ProcessEnv // The environment to use. Note: this contains the cached module and filesystem state.
 
+	// LocalPrefix is a comma-separated string of import path prefixes, which, if
+	// set, instructs Process to sort the import paths with the given prefixes
+	// into another group after 3rd-party packages.
+	LocalPrefix string
+
 	Fragment  bool // Accept fragment of a source file (no package statement)
 	AllErrors bool // Report all errors (not just the first 10 on different lines)
 
@@ -42,13 +44,8 @@
 	FormatOnly bool // Disable the insertion and deletion of imports
 }
 
-// Process implements golang.org/x/tools/imports.Process with explicit context in env.
+// Process implements golang.org/x/tools/imports.Process with explicit context in opt.Env.
 func Process(filename string, src []byte, opt *Options) (formatted []byte, err error) {
-	src, opt, err = initialize(filename, src, opt)
-	if err != nil {
-		return nil, err
-	}
-
 	fileSet := token.NewFileSet()
 	file, adjust, err := parse(fileSet, filename, src, opt)
 	if err != nil {
@@ -64,16 +61,12 @@
 }
 
 // FixImports returns a list of fixes to the imports that, when applied,
-// will leave the imports in the same state as Process.
+// will leave the imports in the same state as Process. src and opt must
+// be specified.
 //
 // Note that filename's directory influences which imports can be chosen,
 // so it is important that filename be accurate.
 func FixImports(filename string, src []byte, opt *Options) (fixes []*ImportFix, err error) {
-	src, opt, err = initialize(filename, src, opt)
-	if err != nil {
-		return nil, err
-	}
-
 	fileSet := token.NewFileSet()
 	file, _, err := parse(fileSet, filename, src, opt)
 	if err != nil {
@@ -84,13 +77,9 @@
 }
 
 // ApplyFixes applies all of the fixes to the file and formats it. extraMode
-// is added in when parsing the file.
+// is added in when parsing the file. src and opts must be specified, but no
+// env is needed.
 func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options, extraMode parser.Mode) (formatted []byte, err error) {
-	src, opt, err = initialize(filename, src, opt)
-	if err != nil {
-		return nil, err
-	}
-
 	// Don't use parse() -- we don't care about fragments or statement lists
 	// here, and we need to work with unparseable files.
 	fileSet := token.NewFileSet()
@@ -114,60 +103,9 @@
 	return formatFile(fileSet, file, src, nil, opt)
 }
 
-// GetAllCandidates gets all of the packages starting with prefix that can be
-// imported by filename, sorted by import path.
-func GetAllCandidates(ctx context.Context, callback func(ImportFix), searchPrefix, filename, filePkg string, opt *Options) error {
-	_, opt, err := initialize(filename, []byte{}, opt)
-	if err != nil {
-		return err
-	}
-	return getAllCandidates(ctx, callback, searchPrefix, filename, filePkg, opt.Env)
-}
-
-// GetPackageExports returns all known packages with name pkg and their exports.
-func GetPackageExports(ctx context.Context, callback func(PackageExport), searchPkg, filename, filePkg string, opt *Options) error {
-	_, opt, err := initialize(filename, []byte{}, opt)
-	if err != nil {
-		return err
-	}
-	return getPackageExports(ctx, callback, searchPkg, filename, filePkg, opt.Env)
-}
-
-// initialize sets the values for opt and src.
-// If they are provided, they are not changed. Otherwise opt is set to the
-// default values and src is read from the file system.
-func initialize(filename string, src []byte, opt *Options) ([]byte, *Options, error) {
-	// Use defaults if opt is nil.
-	if opt == nil {
-		opt = &Options{Comments: true, TabIndent: true, TabWidth: 8}
-	}
-
-	// Set the env if the user has not provided it.
-	if opt.Env == nil {
-		opt.Env = &ProcessEnv{}
-	}
-	// Set the gocmdRunner if the user has not provided it.
-	if opt.Env.GocmdRunner == nil {
-		opt.Env.GocmdRunner = &gocommand.Runner{}
-	}
-	if err := opt.Env.init(); err != nil {
-		return nil, nil, err
-	}
-
-	if src == nil {
-		b, err := ioutil.ReadFile(filename)
-		if err != nil {
-			return nil, nil, err
-		}
-		src = b
-	}
-
-	return src, opt, nil
-}
-
 func formatFile(fileSet *token.FileSet, file *ast.File, src []byte, adjust func(orig []byte, src []byte) []byte, opt *Options) ([]byte, error) {
-	mergeImports(opt.Env, fileSet, file)
-	sortImports(opt.Env, fileSet, file)
+	mergeImports(fileSet, file)
+	sortImports(opt.LocalPrefix, fileSet, file)
 	imps := astutil.Imports(fileSet, file)
 	var spacesBefore []string // import paths we need spaces before
 	for _, impSection := range imps {
@@ -178,7 +116,7 @@
 		lastGroup := -1
 		for _, importSpec := range impSection {
 			importPath, _ := strconv.Unquote(importSpec.Path.Value)
-			groupNum := importGroup(opt.Env, importPath)
+			groupNum := importGroup(opt.LocalPrefix, importPath)
 			if groupNum != lastGroup && lastGroup != -1 {
 				spacesBefore = append(spacesBefore, importPath)
 			}
diff --git a/internal/imports/imports_test.go b/internal/imports/imports_test.go
index ad16622..6405ab5 100644
--- a/internal/imports/imports_test.go
+++ b/internal/imports/imports_test.go
@@ -4,7 +4,6 @@
 	"os"
 	"testing"
 
-	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/testenv"
 )
 
@@ -12,70 +11,3 @@
 	testenv.ExitIfSmallMachine()
 	os.Exit(m.Run())
 }
-
-// TestNilOpts tests that process does not crash with nil opts.
-func TestNilOpts(t *testing.T) {
-	var testOpts = []struct {
-		name string
-		opt  *Options
-	}{
-		{
-			name: "nil",
-			opt:  nil,
-		},
-		{
-			name: "nil env",
-			opt:  &Options{Comments: true, TabIndent: true, TabWidth: 8},
-		},
-		{
-			name: "default",
-			opt: &Options{
-				Env: &ProcessEnv{
-					GocmdRunner: &gocommand.Runner{},
-				},
-				Comments:  true,
-				TabIndent: true,
-				TabWidth:  8,
-			},
-		},
-	}
-
-	input := `package p
-
-func _() {
-	fmt.Println()
-}
-`
-	want := `package p
-
-import "fmt"
-
-func _() {
-	fmt.Println()
-}
-`
-	for _, test := range testOpts {
-		// Test Process
-		got, err := Process("", []byte(input), test.opt)
-		if err != nil {
-			t.Errorf("%s: %s", test.name, err.Error())
-		}
-		if string(got) != want {
-			t.Errorf("%s: Process: Got:\n%s\nWant:\n%s\n", test.name, string(got), want)
-		}
-
-		// Test FixImports and ApplyFixes
-		fixes, err := FixImports("", []byte(input), test.opt)
-		if err != nil {
-			t.Errorf("%s: %s", test.name, err.Error())
-		}
-
-		got, err = ApplyFixes(fixes, "", []byte(input), test.opt, 0)
-		if err != nil {
-			t.Errorf("%s: %s", test.name, err.Error())
-		}
-		if string(got) != want {
-			t.Errorf("%s: ApplyFix: Got:\n%s\nWant:\n%s\n", test.name, string(got), want)
-		}
-	}
-}
diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go
index 9d26aff..9666fb8 100644
--- a/internal/imports/mod_test.go
+++ b/internal/imports/mod_test.go
@@ -697,10 +697,6 @@
 	if *testDebug {
 		env.Logf = log.Printf
 	}
-	if err := env.init(); err != nil {
-		t.Fatal(err)
-	}
-
 	// go mod download gets mad if we don't have a go.mod, so make sure we do.
 	_, err = os.Stat(filepath.Join(mainDir, "go.mod"))
 	if err != nil && !os.IsNotExist(err) {
@@ -712,10 +708,14 @@
 		}
 	}
 
+	resolver, err := env.GetResolver()
+	if err != nil {
+		t.Fatal(err)
+	}
 	return &modTest{
 		T:        t,
 		env:      env,
-		resolver: newModuleResolver(env),
+		resolver: resolver.(*ModuleResolver),
 		cleanup:  func() { removeDir(dir) },
 	}
 }
@@ -875,10 +875,10 @@
 		GocmdRunner: &gocommand.Runner{},
 		WorkingDir:  dir,
 	}
-	if err := env.init(); err != nil {
+	resolver, err := env.GetResolver()
+	if err != nil {
 		t.Fatal(err)
 	}
-	resolver := newModuleResolver(env)
 	scanToSlice(resolver, nil)
 }
 
@@ -927,7 +927,7 @@
 			}
 		}
 	}
-	if err := getAllCandidates(context.Background(), add, "", "foo.go", "foo", mt.env); err != nil {
+	if err := GetAllCandidates(context.Background(), add, "", "foo.go", "foo", mt.env); err != nil {
 		t.Fatalf("getAllCandidates() = %v", err)
 	}
 	sort.Slice(got, func(i, j int) bool {
@@ -948,14 +948,15 @@
 		GocmdRunner: &gocommand.Runner{},
 		Logf:        log.Printf,
 	}
-	if err := env.init(); err != nil {
+	exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
+	resolver, err := env.GetResolver()
+	if err != nil {
 		b.Fatal(err)
 	}
-	exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
-	scanToSlice(env.GetResolver(), exclude)
+	scanToSlice(resolver, exclude)
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		scanToSlice(env.GetResolver(), exclude)
-		env.GetResolver().(*ModuleResolver).ClearForNewScan()
+		scanToSlice(resolver, exclude)
+		resolver.(*ModuleResolver).ClearForNewScan()
 	}
 }
diff --git a/internal/imports/sortimports.go b/internal/imports/sortimports.go
index 2262794..be8ffa2 100644
--- a/internal/imports/sortimports.go
+++ b/internal/imports/sortimports.go
@@ -15,7 +15,7 @@
 
 // sortImports sorts runs of consecutive import lines in import blocks in f.
 // It also removes duplicate imports when it is possible to do so without data loss.
-func sortImports(env *ProcessEnv, fset *token.FileSet, f *ast.File) {
+func sortImports(localPrefix string, fset *token.FileSet, f *ast.File) {
 	for i, d := range f.Decls {
 		d, ok := d.(*ast.GenDecl)
 		if !ok || d.Tok != token.IMPORT {
@@ -40,11 +40,11 @@
 		for j, s := range d.Specs {
 			if j > i && fset.Position(s.Pos()).Line > 1+fset.Position(d.Specs[j-1].End()).Line {
 				// j begins a new run.  End this one.
-				specs = append(specs, sortSpecs(env, fset, f, d.Specs[i:j])...)
+				specs = append(specs, sortSpecs(localPrefix, fset, f, d.Specs[i:j])...)
 				i = j
 			}
 		}
-		specs = append(specs, sortSpecs(env, fset, f, d.Specs[i:])...)
+		specs = append(specs, sortSpecs(localPrefix, fset, f, d.Specs[i:])...)
 		d.Specs = specs
 
 		// Deduping can leave a blank line before the rparen; clean that up.
@@ -60,7 +60,7 @@
 
 // mergeImports merges all the import declarations into the first one.
 // Taken from golang.org/x/tools/ast/astutil.
-func mergeImports(env *ProcessEnv, fset *token.FileSet, f *ast.File) {
+func mergeImports(fset *token.FileSet, f *ast.File) {
 	if len(f.Decls) <= 1 {
 		return
 	}
@@ -142,7 +142,7 @@
 	End   token.Pos
 }
 
-func sortSpecs(env *ProcessEnv, fset *token.FileSet, f *ast.File, specs []ast.Spec) []ast.Spec {
+func sortSpecs(localPrefix string, fset *token.FileSet, f *ast.File, specs []ast.Spec) []ast.Spec {
 	// Can't short-circuit here even if specs are already sorted,
 	// since they might yet need deduplication.
 	// A lone import, however, may be safely ignored.
@@ -191,7 +191,7 @@
 	// Reassign the import paths to have the same position sequence.
 	// Reassign each comment to abut the end of its spec.
 	// Sort the comments by new position.
-	sort.Sort(byImportSpec{env, specs})
+	sort.Sort(byImportSpec{localPrefix, specs})
 
 	// Dedup. Thanks to our sorting, we can just consider
 	// adjacent pairs of imports.
@@ -245,8 +245,8 @@
 }
 
 type byImportSpec struct {
-	env   *ProcessEnv
-	specs []ast.Spec // slice of *ast.ImportSpec
+	localPrefix string
+	specs       []ast.Spec // slice of *ast.ImportSpec
 }
 
 func (x byImportSpec) Len() int      { return len(x.specs) }
@@ -255,8 +255,8 @@
 	ipath := importPath(x.specs[i])
 	jpath := importPath(x.specs[j])
 
-	igroup := importGroup(x.env, ipath)
-	jgroup := importGroup(x.env, jpath)
+	igroup := importGroup(x.localPrefix, ipath)
+	jgroup := importGroup(x.localPrefix, jpath)
 	if igroup != jgroup {
 		return igroup < jgroup
 	}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index dd2a418..82821e7 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -400,21 +400,27 @@
 			return err
 		}
 		if modFH.Identity() != v.cachedModFileVersion {
-			v.processEnv.GetResolver().(*imports.ModuleResolver).ClearForNewMod()
+			if resolver, err := v.processEnv.GetResolver(); err == nil {
+				resolver.(*imports.ModuleResolver).ClearForNewMod()
+			}
 			v.cachedModFileVersion = modFH.Identity()
 		}
 	}
 
+	v.optionsMu.Lock()
+	localPrefix := v.options.LocalPrefix
+	v.optionsMu.Unlock()
 	// Run the user function.
 	opts := &imports.Options{
 		// Defaults.
-		AllErrors:  true,
-		Comments:   true,
-		Fragment:   true,
-		FormatOnly: false,
-		TabIndent:  true,
-		TabWidth:   8,
-		Env:        v.processEnv,
+		AllErrors:   true,
+		Comments:    true,
+		Fragment:    true,
+		FormatOnly:  false,
+		TabIndent:   true,
+		TabWidth:    8,
+		Env:         v.processEnv,
+		LocalPrefix: localPrefix,
 	}
 
 	if err := fn(opts); err != nil {
@@ -439,13 +445,14 @@
 
 	v.importsMu.Lock()
 	env := v.processEnv
-	env.GetResolver().ClearForNewScan()
+	if resolver, err := v.processEnv.GetResolver(); err == nil {
+		resolver.ClearForNewScan()
+	}
 	v.importsMu.Unlock()
 
 	// We don't have a context handy to use for logging, so use the stdlib for now.
 	event.Log(v.baseCtx, "background imports cache refresh starting")
-	err := imports.PrimeCache(context.Background(), env)
-	if err == nil {
+	if err := imports.PrimeCache(context.Background(), env); err == nil {
 		event.Log(v.baseCtx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
 	} else {
 		event.Log(v.baseCtx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
@@ -464,11 +471,10 @@
 
 	v.optionsMu.Lock()
 	_, buildFlags := v.envLocked()
-	localPrefix, verboseOutput := v.options.LocalPrefix, v.options.VerboseOutput
+	verboseOutput := v.options.VerboseOutput
 	v.optionsMu.Unlock()
 
 	pe := v.processEnv
-	pe.LocalPrefix = localPrefix
 	pe.GocmdRunner = v.session.gocmdRunner
 	pe.BuildFlags = buildFlags
 	pe.Env = v.goEnv
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index f3b8802..cd5757e 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -836,10 +836,13 @@
 
 	var relevances map[string]int
 	if len(paths) != 0 {
-		c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
-			relevances = imports.ScoreImportPaths(ctx, opts.Env, paths)
-			return nil
-		})
+		if err := c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+			var err error
+			relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths)
+			return err
+		}); err != nil {
+			return err
+		}
 	}
 	sort.Slice(paths, func(i, j int) bool {
 		return relevances[paths[i]] > relevances[paths[j]]
@@ -892,7 +895,7 @@
 		}
 	}
 	return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
-		return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.GetTypes().Name(), opts)
+		return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.GetTypes().Name(), opts.Env)
 	})
 }
 
@@ -1124,10 +1127,13 @@
 
 	var relevances map[string]int
 	if len(paths) != 0 {
-		c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
-			relevances = imports.ScoreImportPaths(ctx, opts.Env, paths)
-			return nil
-		})
+		if err := c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
+			var err error
+			relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths)
+			return err
+		}); err != nil {
+			return err
+		}
 	}
 	sort.Slice(paths, func(i, j int) bool {
 		return relevances[paths[i]] > relevances[paths[j]]
@@ -1190,7 +1196,7 @@
 		count++
 	}
 	return c.snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error {
-		return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts)
+		return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env)
 	})
 }
 
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 183fd8e..6999ba3 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -140,6 +140,7 @@
 	}
 
 	options := &imports.Options{
+		LocalPrefix: view.Options().LocalPrefix,
 		// Defaults.
 		AllErrors:  true,
 		Comments:   true,