internal/lsp: push initialization tasks into one function

This change moves as much view initialization code into the
initialization function, instead of having it happen on view create.
Also, the `go env` variables that are collected at inconsistent times
are all collected on view creation. That is sufficient, since the view
is recreated if the environment changes.

I had originally hoped that the initial call to `go env` and the
-modfile detection could become part of this parallel initialization as
well, but you can't create a *packages.Config until the temporary
modfile has been set up, so it still makes sense to do that on view
create. This is, however, the reasoning behind the refactorings in
the -modfile detection in this CL. The main changes are a few renamings
and a split between snapshot.ModFiles and view.modFiles to maximize the
amount of work done in the view. I changed view.modfiles to moduleInformation
because I thought we might want to store additional information there at some
point. Rohan, please let me know if you disagree with any of the changes I made,
and I can revert them.

Fixes golang/go#36487

Change-Id: I504db5a4f41b79bee99ebd391e32e7b520a19569
Reviewed-on: https://go-review.googlesource.com/c/tools/+/214417
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rohan Challa <rohan@golang.org>
diff --git a/internal/lsp/cache/builtin.go b/internal/lsp/cache/builtin.go
index eee8297..42a1a06 100644
--- a/internal/lsp/cache/builtin.go
+++ b/internal/lsp/cache/builtin.go
@@ -12,6 +12,7 @@
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
+	errors "golang.org/x/xerrors"
 )
 
 type builtinPkg struct {
@@ -19,11 +20,15 @@
 	files []source.ParseGoHandle
 }
 
-func (b *builtinPkg) Lookup(name string) *ast.Object {
-	if b == nil || b.pkg == nil || b.pkg.Scope == nil {
-		return nil
+func (v *view) LookupBuiltin(name string) (*ast.Object, error) {
+	if v.builtin == nil || v.builtin.pkg == nil || v.builtin.pkg.Scope == nil {
+		return nil, errors.Errorf("no builtin package")
 	}
-	return b.pkg.Scope.Lookup(name)
+	astObj := v.builtin.pkg.Scope.Lookup(name)
+	if astObj == nil {
+		return nil, errors.Errorf("no builtin object for %s", name)
+	}
+	return astObj, nil
 }
 
 func (b *builtinPkg) CompiledGoFiles() []source.ParseGoHandle {
@@ -34,6 +39,9 @@
 // It assumes that the view is not active yet,
 // i.e. it has not been added to the session's list of views.
 func (v *view) buildBuiltinPackage(ctx context.Context) error {
+	if v.builtin != nil {
+		return errors.Errorf("rebuilding builtin package")
+	}
 	cfg := v.Config(ctx)
 	pkgs, err := packages.Load(cfg, "builtin")
 	// If the error is related to a go.mod parse error, we want to continue loading.
@@ -44,15 +52,15 @@
 		return err
 	}
 	if len(pkgs) != 1 {
-		return err
+		return errors.Errorf("expected 1 (got %v) packages for builtin", len(pkgs))
 	}
-	pkg := pkgs[0]
 	files := make(map[string]*ast.File)
-	for _, filename := range pkg.GoFiles {
+	var pghs []source.ParseGoHandle
+	for _, filename := range pkgs[0].GoFiles {
 		fh := v.session.GetFile(span.FileURI(filename))
-		ph := v.session.cache.ParseGoHandle(fh, source.ParseFull)
-		v.builtin.files = append(v.builtin.files, ph)
-		file, _, _, err := ph.Parse(ctx)
+		pgh := v.session.cache.ParseGoHandle(fh, source.ParseFull)
+		pghs = append(pghs, pgh)
+		file, _, _, err := pgh.Parse(ctx)
 		if err != nil {
 			return err
 		}
@@ -62,6 +70,13 @@
 		v.ignoredURIs[span.NewURI(filename)] = struct{}{}
 		v.ignoredURIsMu.Unlock()
 	}
-	v.builtin.pkg, err = ast.NewPackage(cfg.Fset, files, nil, nil)
-	return err
+	pkg, err := ast.NewPackage(cfg.Fset, files, nil, nil)
+	if err != nil {
+		return err
+	}
+	v.builtin = &builtinPkg{
+		files: pghs,
+		pkg:   pkg,
+	}
+	return nil
 }
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index ee0681b..92ca3eb 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -228,11 +228,7 @@
 	if len(pkg.GoFiles) > 1 {
 		return false
 	}
-	buildCachePath, err := v.getBuildCachePath(ctx)
-	if err != nil {
-		return false
-	}
-	if !strings.HasPrefix(pkg.GoFiles[0], buildCachePath) {
+	if !strings.HasPrefix(pkg.GoFiles[0], v.gocache) {
 		return false
 	}
 	return true
diff --git a/internal/lsp/cache/modfiles.go b/internal/lsp/cache/modfiles.go
index d45a5ca..4797ba3 100644
--- a/internal/lsp/cache/modfiles.go
+++ b/internal/lsp/cache/modfiles.go
@@ -9,21 +9,47 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/telemetry"
+	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/telemetry/log"
 	errors "golang.org/x/xerrors"
 )
 
+func (v *view) modFiles(ctx context.Context) (span.URI, span.URI, error) {
+	// Don't return errors if the view is not a module.
+	if v.mod == nil {
+		return "", "", nil
+	}
+	// Copy the real go.mod file content into the temp go.mod file.
+	origFile, err := os.Open(v.mod.realMod.Filename())
+	if err != nil {
+		return "", "", err
+	}
+	defer origFile.Close()
+
+	tempFile, err := os.OpenFile(v.mod.tempMod.Filename(), os.O_WRONLY, os.ModePerm)
+	if err != nil {
+		return "", "", err
+	}
+	defer tempFile.Close()
+
+	if _, err := io.Copy(tempFile, origFile); err != nil {
+		return "", "", err
+	}
+	return v.mod.realMod, v.mod.tempMod, nil
+}
+
 // 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 modfileFlagExists(ctx context.Context, folder string, env []string) (string, bool, error) {
-	// Check the Go version by running go list with GO111MODULE=off.
-	// If the output is anything other than "go1.14\n", assume -modfile is not supported.
-	// Borrowed from internal/imports/mod.go:620
+func (v *view) modfileFlagExists(ctx context.Context, env []string) (string, bool, 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()
 	stdout, err := source.InvokeGo(ctx, folder, append(env, "GO111MODULE=off"), "list", "-e", "-f", format)
 	if err != nil {
 		return "", false, err
@@ -34,62 +60,71 @@
 		log.Error(ctx, "unexpected stdout when checking for go1.14", errors.Errorf("%q", stdout), telemetry.Directory.Of(folder))
 		return "", false, nil
 	}
-	// Get the go.mod file associated with this module.
-	b, err := source.InvokeGo(ctx, folder, env, "env", "GOMOD")
-	if err != nil {
-		return "", false, err
-	}
-	modfile := strings.TrimSpace(b.String())
+	modfile := strings.TrimSpace(v.gomod)
 	if modfile == os.DevNull {
-		return "", false, errors.Errorf("go env GOMOD did not detect a go.mod file in this folder")
+		return "", false, errors.Errorf("unable to detect a go.mod file in %s", v.folder)
 	}
 	return modfile, lines[0] == "go1.14", nil
 }
 
-// The function getModfiles will return the go.mod files associated with the directory that is passed in.
-func getModfiles(ctx context.Context, folder string, options source.Options) (*modfiles, error) {
-	if !options.TempModfile {
-		log.Print(ctx, "using the -modfile flag is disabled", telemetry.Directory.Of(folder))
-		return nil, nil
+func (v *view) setModuleInformation(ctx context.Context, enabled bool) error {
+	// The user has disabled the use of the -modfile flag.
+	if !enabled {
+		log.Print(ctx, "using the -modfile flag is disabled", telemetry.Directory.Of(v.folder))
+		return nil
 	}
-	modfile, flagExists, err := modfileFlagExists(ctx, folder, options.Env)
+	modFile, flagExists, err := v.modfileFlagExists(ctx, v.Options().Env)
 	if err != nil {
-		return nil, err
+		return err
 	}
+	// The user's version of Go does not support the -modfile flag.
 	if !flagExists {
-		return nil, nil
+		return nil
 	}
-	if modfile == "" || modfile == os.DevNull {
-		return nil, errors.Errorf("go env GOMOD cannot detect a go.mod file in this folder")
+	if modFile == "" || modFile == os.DevNull {
+		return errors.Errorf("unable to detect a go.mod file in %s", v.folder)
 	}
 	// Copy the current go.mod file into the temporary go.mod file.
-	tempFile, err := ioutil.TempFile("", "go.*.mod")
+	// The file's name will be of the format go.1234.mod.
+	// It's temporary go.sum file should have the corresponding format of go.1234.sum.
+	tempModFile, err := ioutil.TempFile("", "go.*.mod")
 	if err != nil {
-		return nil, err
+		return err
 	}
-	defer tempFile.Close()
-	origFile, err := os.Open(modfile)
+	defer tempModFile.Close()
+
+	origFile, err := os.Open(modFile)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	defer origFile.Close()
-	if _, err := io.Copy(tempFile, origFile); err != nil {
-		return nil, err
+
+	if _, err := io.Copy(tempModFile, origFile); err != nil {
+		return err
 	}
-	copySumFile(modfile, tempFile.Name())
-	return &modfiles{real: modfile, temp: tempFile.Name()}, nil
+	v.mod = &moduleInformation{
+		realMod: span.FileURI(modFile),
+		tempMod: span.FileURI(tempModFile.Name()),
+	}
+	// Copy go.sum file as well (if there is one).
+	sumFile := filepath.Join(filepath.Dir(modFile), "go.sum")
+	stat, err := os.Stat(sumFile)
+	if err != nil || !stat.Mode().IsRegular() {
+		return nil
+	}
+	contents, err := ioutil.ReadFile(sumFile)
+	if err != nil {
+		return err
+	}
+	if err := ioutil.WriteFile(v.mod.tempSumFile(), contents, stat.Mode()); err != nil {
+		return err
+	}
+	return nil
 }
 
-func copySumFile(realFile, tempFile string) {
-	realSum := realFile[0:len(realFile)-3] + "sum"
-	tempSum := tempFile[0:len(tempFile)-3] + "sum"
-	stat, err := os.Stat(realSum)
-	if err != nil || !stat.Mode().IsRegular() {
-		return
-	}
-	contents, err := ioutil.ReadFile(realSum)
-	if err != nil {
-		return
-	}
-	ioutil.WriteFile(tempSum, contents, stat.Mode())
+// tempSumFile returns the path to the copied temporary go.sum file.
+// It simply replaces the extension of the temporary go.mod file with "sum".
+func (mod *moduleInformation) tempSumFile() string {
+	tmp := mod.tempMod.Filename()
+	return tmp[:len(tmp)-len("mod")] + "sum"
 }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 3af6e89..aece595 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -15,7 +15,6 @@
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/telemetry"
 	"golang.org/x/tools/internal/span"
-	"golang.org/x/tools/internal/telemetry/log"
 	"golang.org/x/tools/internal/telemetry/trace"
 	"golang.org/x/tools/internal/xcontext"
 	errors "golang.org/x/xerrors"
@@ -78,10 +77,6 @@
 	baseCtx := trace.Detach(xcontext.Detach(ctx))
 	backgroundCtx, cancel := context.WithCancel(baseCtx)
 
-	modfiles, err := getModfiles(ctx, folder.Filename(), options)
-	if err != nil {
-		log.Error(ctx, "error getting modfiles", err, telemetry.Directory.Of(folder))
-	}
 	v := &view{
 		session:       s,
 		initialized:   make(chan struct{}),
@@ -91,7 +86,6 @@
 		backgroundCtx: backgroundCtx,
 		cancel:        cancel,
 		name:          name,
-		modfiles:      modfiles,
 		folder:        folder,
 		filesByURI:    make(map[span.URI]*fileBase),
 		filesByBase:   make(map[string][]*fileBase),
@@ -105,7 +99,6 @@
 			workspacePackages: make(map[packageID]packagePath),
 		},
 		ignoredURIs: make(map[span.URI]struct{}),
-		builtin:     &builtinPkg{},
 	}
 	v.snapshot.view = v
 
@@ -113,10 +106,12 @@
 		v.session.cache.options(&v.options)
 	}
 
-	// Preemptively build the builtin package,
-	// so we immediately add builtin.go to the list of ignored files.
-	// TODO(rstambler): This could be part of the initialization process.
-	if err := v.buildBuiltinPackage(ctx); err != nil {
+	// Make sure to get the `go env` before continuing with initialization.
+	if err := v.setGoEnv(ctx, folder.Filename(), options.Env); err != nil {
+		return nil, nil, err
+	}
+	// Set the module-specific information.
+	if err := v.setModuleInformation(ctx, v.options.TempModfile); err != nil {
 		return nil, nil, err
 	}
 
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 4f7f3df..3785fcc 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -6,7 +6,6 @@
 
 import (
 	"context"
-	"io"
 	"os"
 	"path/filepath"
 	"strings"
@@ -68,32 +67,22 @@
 }
 
 func (s *snapshot) ModFiles(ctx context.Context) (source.FileHandle, source.FileHandle, error) {
-	if s.view.modfiles == nil {
+	r, t, err := s.view.modFiles(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	if r == "" || t == "" {
 		return nil, nil, nil
 	}
-	realfh, err := s.GetFile(span.FileURI(s.view.modfiles.real))
+	// Get the real mod file's content through the snapshot,
+	// as it may be open in an overlay.
+	realfh, err := s.GetFile(r)
 	if err != nil {
 		return nil, nil, err
 	}
-	if realfh == nil {
-		return nil, nil, errors.Errorf("go.mod filehandle is nil")
-	}
-	// Copy the real go.mod file content into the temp go.mod file.
-	origFile, err := os.Open(s.view.modfiles.real)
-	if err != nil {
-		return nil, nil, err
-	}
-	defer origFile.Close()
-	tempFile, err := os.OpenFile(s.view.modfiles.temp, os.O_WRONLY, os.ModePerm)
-	if err != nil {
-		return nil, nil, err
-	}
-	defer tempFile.Close()
-	if _, err := io.Copy(tempFile, origFile); err != nil {
-		return nil, nil, err
-	}
-	// Go directly to disk to get the correct FileHandle, since we just copied the file without invalidating the snapshot.
-	tempfh := s.view.Session().Cache().GetFile(span.FileURI(s.view.modfiles.temp))
+	// Go directly to disk to get the temporary mod file,
+	// since it is always on disk.
+	tempfh := s.view.session.cache.GetFile(t)
 	if tempfh == nil {
 		return nil, nil, errors.Errorf("temporary go.mod filehandle is nil")
 	}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index bd3204a..765b05d 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -7,17 +7,18 @@
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"go/ast"
 	"go/token"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"reflect"
 	"strings"
 	"sync"
 	"time"
 
+	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/lsp/debug"
@@ -53,12 +54,12 @@
 	// Name is the user visible name of this view.
 	name string
 
-	// modfiles are the go.mod files attributed to this view.
-	modfiles *modfiles
-
 	// Folder is the root of this view.
 	folder span.URI
 
+	// mod is the module information for this view.
+	mod *moduleInformation
+
 	// process is the process env for this view.
 	// Note: this contains cached module and filesystem state.
 	//
@@ -81,21 +82,23 @@
 	snapshotMu sync.Mutex
 	snapshot   *snapshot
 
-	// builtin is used to resolve builtin types.
-	builtin *builtinPkg
-
 	// ignoredURIs is the set of URIs of files that we ignore.
 	ignoredURIsMu sync.Mutex
 	ignoredURIs   map[span.URI]struct{}
 
-	// initialized is closed when we have attempted to load the view's workspace packages.
-	// If we failed to load initially, we don't re-try to avoid too many go/packages calls.
+	// `go env` variables that need to be tracked by the view.
+	gopath, gomod, gocache string
+
+	// initialized is closed when the view has been fully initialized.
+	// On initialization, the view's workspace packages are loaded.
+	// All of the fields below are set as part of initialization.
+	// If we failed to load, we don't re-try to avoid too many go/packages calls.
 	initializeOnce      sync.Once
 	initialized         chan struct{}
 	initializationError error
 
-	// buildCachePath is the value of `go env GOCACHE`.
-	buildCachePath string
+	// builtin is used to resolve builtin types.
+	builtin *builtinPkg
 }
 
 // fileBase holds the common functionality for all files.
@@ -120,9 +123,10 @@
 	return len(f.uris)
 }
 
-// modfiles holds the real and temporary go.mod files that are attributed to a view.
-type modfiles struct {
-	real, temp string
+// moduleInformation holds the real and temporary go.mod files
+// that are attributed to a view.
+type moduleInformation struct {
+	realMod, tempMod span.URI
 }
 
 func (v *view) Session() source.Session {
@@ -173,13 +177,12 @@
 	// We want to run the go commands with the -modfile flag if the version of go
 	// that we are using supports it.
 	buildFlags := v.options.BuildFlags
-	if v.modfiles != nil {
-		buildFlags = append(buildFlags, fmt.Sprintf("-modfile=%s", v.modfiles.temp))
+	if v.mod != nil {
+		buildFlags = append(buildFlags, fmt.Sprintf("-modfile=%s", v.mod.tempMod.Filename()))
 	}
-	return &packages.Config{
+	cfg := &packages.Config{
 		Dir:        v.folder.Filename(),
 		Context:    ctx,
-		Env:        v.options.Env,
 		BuildFlags: buildFlags,
 		Mode: packages.NeedName |
 			packages.NeedFiles |
@@ -199,6 +202,9 @@
 		},
 		Tests: true,
 	}
+	cfg.Env = append(cfg.Env, fmt.Sprintf("GOPATH=%s", v.gopath))
+	cfg.Env = append(cfg.Env, v.options.Env...)
+	return cfg
 }
 
 func (v *view) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error {
@@ -284,14 +290,6 @@
 			env.GOSUMDB = split[1]
 		}
 	}
-
-	if env.GOPATH == "" {
-		gopath, err := getGoEnvVar(ctx, cfg, "GOPATH")
-		if err != nil {
-			return nil, err
-		}
-		env.GOPATH = gopath
-	}
 	return env, nil
 }
 
@@ -423,8 +421,9 @@
 		v.cancel()
 		v.cancel = nil
 	}
-	if v.modfiles != nil {
-		os.Remove(v.modfiles.temp)
+	if v.mod != nil {
+		os.Remove(v.mod.tempMod.Filename())
+		os.Remove(v.mod.tempSumFile())
 	}
 	debug.DropView(debugView{v})
 }
@@ -453,10 +452,6 @@
 	return v.backgroundCtx
 }
 
-func (v *view) BuiltinPackage() source.BuiltinPackage {
-	return v.builtin
-}
-
 func (v *view) Snapshot() source.Snapshot {
 	return v.getSnapshot()
 }
@@ -472,16 +467,32 @@
 	v.initializeOnce.Do(func() {
 		defer close(v.initialized)
 
-		// Do not cancel the call to go/packages.Load for the entire workspace.
-		meta, err := s.load(ctx, directoryURI(v.folder))
-		if err != nil {
+		g, ctx := errgroup.WithContext(ctx)
+
+		// Load all of the packages in the workspace.
+		g.Go(func() error {
+			// Do not cancel the call to go/packages.Load for the entire workspace.
+			meta, err := s.load(ctx, directoryURI(v.folder))
+			if err != nil {
+				return err
+			}
+			// A test variant of a package can only be loaded directly by loading
+			// the non-test variant with -test. Track the import path of the non-test variant.
+			for _, m := range meta {
+				s.setWorkspacePackage(m.id, m.pkgPath)
+			}
+			return nil
+		})
+
+		// Build the builtin package on initialization.
+		g.Go(func() error {
+			return v.buildBuiltinPackage(ctx)
+		})
+
+		// Wait for all initialization tasks to complete.
+		if err := g.Wait(); err != nil {
 			v.initializationError = err
 		}
-		// A test variant of a package can only be loaded directly by loading
-		// the non-test variant with -test. Track the import path of the non-test variant.
-		for _, m := range meta {
-			s.setWorkspacePackage(m.id, m.pkgPath)
-		}
 	})
 }
 
@@ -532,42 +543,47 @@
 	v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
 }
 
-func (v *view) getBuildCachePath(ctx context.Context) (string, error) {
-	v.mu.Lock()
-	defer v.mu.Unlock()
-
-	if v.buildCachePath == "" {
-		path, err := getGoEnvVar(ctx, v.Config(ctx), "GOCACHE")
-		if err != nil {
-			return "", err
-		}
-		v.buildCachePath = path
-	}
-	return v.buildCachePath, nil
-}
-
-func getGoEnvVar(ctx context.Context, cfg *packages.Config, value string) (string, error) {
-	var result string
-	for _, kv := range cfg.Env {
-		split := strings.Split(kv, "=")
-		if len(split) < 2 {
+func (v *view) setGoEnv(ctx context.Context, dir string, env []string) error {
+	var gocache, gopath bool
+	for _, e := range env {
+		split := strings.Split(e, "=")
+		if len(split) != 2 {
 			continue
 		}
-		if split[0] == value {
-			result = split[1]
+		switch split[0] {
+		case "GOCACHE":
+			v.gocache = split[1]
+			gocache = true
+		case "GOPATH":
+			v.gopath = split[1]
+			gopath = true
 		}
 	}
-	if result == "" {
-		cmd := exec.CommandContext(ctx, "go", "env", value)
-		cmd.Env = cfg.Env
-		out, err := cmd.CombinedOutput()
-		if err != nil {
-			return "", err
-		}
-		result = strings.TrimSpace(string(out))
-		if result == "" {
-			return "", errors.Errorf("no value for %s", value)
+	b, err := source.InvokeGo(ctx, dir, env, "env", "-json")
+	if err != nil {
+		return err
+	}
+	envMap := make(map[string]string)
+	decoder := json.NewDecoder(b)
+	if err := decoder.Decode(&envMap); err != nil {
+		return err
+	}
+	if !gopath {
+		if gopath, ok := envMap["GOPATH"]; ok {
+			v.gopath = gopath
+		} else {
+			return errors.New("unable to determine GOPATH")
 		}
 	}
-	return result, nil
+	if !gocache {
+		if gocache, ok := envMap["GOCACHE"]; ok {
+			v.gocache = gocache
+		} else {
+			return errors.New("unable to determine GOCACHE")
+		}
+	}
+	if gomod, ok := envMap["GOMOD"]; ok {
+		v.gomod = gomod
+	}
+	return nil
 }
diff --git a/internal/lsp/source/completion_builtin.go b/internal/lsp/source/completion_builtin.go
index 4f40023..ddead70 100644
--- a/internal/lsp/source/completion_builtin.go
+++ b/internal/lsp/source/completion_builtin.go
@@ -13,11 +13,10 @@
 // argument. It attempts to use the AST hints from builtin.go where
 // possible.
 func (c *completer) builtinArgKind(obj types.Object, call *ast.CallExpr) objKind {
-	astObj := c.snapshot.View().BuiltinPackage().Lookup(obj.Name())
-	if astObj == nil {
+	astObj, err := c.snapshot.View().LookupBuiltin(obj.Name())
+	if err != nil {
 		return 0
 	}
-
 	exprIdx := indexExprAtPos(c.pos, call.Args)
 
 	decl, ok := astObj.Decl.(*ast.FuncDecl)
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 168308f..f9a5aa4 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -245,11 +245,12 @@
 		item.Kind = protocol.ConstantCompletion
 	case *types.Builtin:
 		item.Kind = protocol.FunctionCompletion
-		builtin := c.snapshot.View().BuiltinPackage().Lookup(obj.Name())
-		if obj == nil {
+		astObj, err := c.snapshot.View().LookupBuiltin(obj.Name())
+		if err != nil {
+			log.Error(c.ctx, "no builtin package", err)
 			break
 		}
-		decl, ok := builtin.Decl.(*ast.FuncDecl)
+		decl, ok := astObj.Decl.(*ast.FuncDecl)
 		if !ok {
 			break
 		}
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index e0761cb..d4c7c95 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -153,11 +153,11 @@
 
 	// Handle builtins separately.
 	if result.Declaration.obj.Parent() == types.Universe {
-		obj := view.BuiltinPackage().Lookup(result.Name)
-		if obj == nil {
-			return result, nil
+		astObj, err := view.LookupBuiltin(result.Name)
+		if err != nil {
+			return nil, err
 		}
-		decl, ok := obj.Decl.(ast.Node)
+		decl, ok := astObj.Decl.(ast.Node)
 		if !ok {
 			return nil, errors.Errorf("no declaration for %s", result.Name)
 		}
diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go
index 2e00d57..ac7141b 100644
--- a/internal/lsp/source/signature_help.go
+++ b/internal/lsp/source/signature_help.go
@@ -136,11 +136,11 @@
 }
 
 func builtinSignature(ctx context.Context, v View, callExpr *ast.CallExpr, name string, pos token.Pos) (*SignatureInformation, error) {
-	obj := v.BuiltinPackage().Lookup(name)
-	if obj == nil {
-		return nil, errors.Errorf("no object for %s", name)
+	astObj, err := v.LookupBuiltin(name)
+	if err != nil {
+		return nil, err
 	}
-	decl, ok := obj.Decl.(*ast.FuncDecl)
+	decl, ok := astObj.Decl.(*ast.FuncDecl)
 	if !ok {
 		return nil, errors.Errorf("no function declaration for builtin: %s", name)
 	}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index c946d23..e8da7bc 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -92,8 +92,8 @@
 	// Folder returns the root folder for this view.
 	Folder() span.URI
 
-	// BuiltinPackage returns the type information for the special "builtin" package.
-	BuiltinPackage() BuiltinPackage
+	// LookupBuiltin returns the go/ast.Object for the given name in the builtin package.
+	LookupBuiltin(name string) (*ast.Object, error)
 
 	// BackgroundContext returns a context used for all background processing
 	// on behalf of this view.
@@ -358,8 +358,3 @@
 func (e *Error) Error() string {
 	return fmt.Sprintf("%s:%s: %s", e.URI, e.Range, e.Message)
 }
-
-type BuiltinPackage interface {
-	Lookup(name string) *ast.Object
-	CompiledGoFiles() []ParseGoHandle
-}