internal/lsp: use dependencies in cache keys

This change includes dependencies in the cache keys for
CheckPackageHandles. This should fix the issue with propagating results
to reverse dependencies.

Updates golang/go#34410

Change-Id: I025b7f0d6b0dcaa89c3461ebd94eadd35de4955f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/198317
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index 46ade5c..69321cf 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -7,9 +7,11 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
 	"go/ast"
 	"go/scanner"
 	"go/types"
+	"sort"
 	"sync"
 
 	"golang.org/x/tools/go/analysis"
@@ -17,7 +19,6 @@
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/telemetry"
 	"golang.org/x/tools/internal/memoize"
-	"golang.org/x/tools/internal/telemetry/log"
 	"golang.org/x/tools/internal/telemetry/trace"
 	errors "golang.org/x/xerrors"
 )
@@ -25,7 +26,6 @@
 type importer struct {
 	snapshot *snapshot
 	ctx      context.Context
-	config   *packages.Config
 
 	// seen maintains the set of previously imported packages.
 	// If we have seen a package that is already in this map, we have a circular import.
@@ -41,38 +41,31 @@
 	parentCheckPackageHandle *checkPackageHandle
 }
 
-// checkPackageKey uniquely identifies a package and its config.
-type checkPackageKey struct {
-	id     string
-	files  string
-	config string
-
-	// TODO: For now, we don't include dependencies in the key.
-	// This will be necessary when we change the cache invalidation logic.
-}
-
 // checkPackageHandle implements source.CheckPackageHandle.
 type checkPackageHandle struct {
 	handle *memoize.Handle
 
-	files   []source.ParseGoHandle
-	imports map[packagePath]*checkPackageHandle
+	// files are the ParseGoHandles that compose the package.
+	files []source.ParseGoHandle
 
-	m      *metadata
-	config *packages.Config
+	// mode is the mode the the files were parsed in.
+	mode source.ParseMode
+
+	// imports is the map of the package's imports.
+	imports map[packagePath]packageID
+
+	// m is the metadata associated with the package.
+	m *metadata
+
+	// key is the hashed key for the package.
+	key []byte
 }
 
-func (cph *checkPackageHandle) Mode() source.ParseMode {
-	if len(cph.files) == 0 {
-		return -1
+func (cph *checkPackageHandle) packageKey() packageKey {
+	return packageKey{
+		id:   cph.m.id,
+		mode: cph.mode,
 	}
-	mode := cph.files[0].Mode()
-	for _, ph := range cph.files[1:] {
-		if ph.Mode() != mode {
-			return -1
-		}
-	}
-	return mode
 }
 
 // checkPackageData contains the data produced by type-checking a package.
@@ -84,38 +77,82 @@
 }
 
 // checkPackageHandle returns a source.CheckPackageHandle for a given package and config.
-func (imp *importer) checkPackageHandle(ctx context.Context, id packageID, s *snapshot) (*checkPackageHandle, error) {
-	m := s.getMetadata(id)
+func (imp *importer) checkPackageHandle(ctx context.Context, id packageID) (*checkPackageHandle, error) {
+	// Determine the mode that the files should be parsed in.
+	mode := imp.mode(id)
+
+	// Check if we already have this CheckPackageHandle cached.
+	if cph := imp.snapshot.getPackage(id, mode); cph != nil {
+		return cph, nil
+	}
+
+	// Build the CheckPackageHandle for this ID and its dependencies.
+	cph, err := imp.buildKey(ctx, id, mode)
+	if err != nil {
+		return nil, err
+	}
+
+	h := imp.snapshot.view.session.cache.store.Bind(string(cph.key), func(ctx context.Context) interface{} {
+		data := &checkPackageData{}
+		data.pkg, data.err = imp.typeCheck(ctx, cph)
+		return data
+	})
+	cph.handle = h
+
+	return cph, nil
+}
+
+// buildKey computes the checkPackageKey for a given checkPackageHandle.
+func (imp *importer) buildKey(ctx context.Context, id packageID, mode source.ParseMode) (*checkPackageHandle, error) {
+	m := imp.snapshot.getMetadata(id)
 	if m == nil {
 		return nil, errors.Errorf("no metadata for %s", id)
 	}
-	phs, err := imp.parseGoHandles(ctx, m)
+
+	phs, err := imp.parseGoHandles(ctx, m, mode)
 	if err != nil {
-		log.Error(ctx, "no ParseGoHandles", err, telemetry.Package.Of(id))
 		return nil, err
 	}
 	cph := &checkPackageHandle{
 		m:       m,
 		files:   phs,
-		config:  imp.config,
-		imports: make(map[packagePath]*checkPackageHandle),
+		imports: make(map[packagePath]packageID),
+		mode:    mode,
 	}
-	h := imp.snapshot.view.session.cache.store.Bind(cph.key(), func(ctx context.Context) interface{} {
-		data := &checkPackageData{}
-		data.pkg, data.err = imp.typeCheck(ctx, cph, m)
-		return data
+
+	// Make sure all of the deps are sorted.
+	deps := append([]packageID{}, m.deps...)
+	sort.Slice(deps, func(i, j int) bool {
+		return deps[i] < deps[j]
 	})
 
-	cph.handle = h
+	// Create the dep importer for use on the dependency handles.
+	depImporter := &importer{
+		snapshot:          imp.snapshot,
+		topLevelPackageID: imp.topLevelPackageID,
+	}
+	// Begin computing the key by getting the depKeys for all dependencies.
+	var depKeys [][]byte
+	for _, dep := range deps {
+		depHandle, err := depImporter.checkPackageHandle(ctx, dep)
+		if err != nil {
+			return nil, errors.Errorf("no dep handle for %s: %+v", dep, err)
+		}
+		cph.imports[depHandle.m.pkgPath] = depHandle.m.id
+		depKeys = append(depKeys, depHandle.key)
+	}
+	cph.key = checkPackageKey(cph.m.id, cph.files, m.config, depKeys)
 
 	// Cache the CheckPackageHandle in the snapshot.
-	for _, ph := range cph.files {
-		uri := ph.File().Identity().URI
-		s.addPackage(uri, cph)
-	}
+	imp.snapshot.addPackage(cph)
+
 	return cph, nil
 }
 
+func checkPackageKey(id packageID, phs []source.ParseGoHandle, cfg *packages.Config, deps [][]byte) []byte {
+	return []byte(hashContents([]byte(fmt.Sprintf("%s%s%s%s", id, hashParseKeys(phs), hashConfig(cfg), hashContents(bytes.Join(deps, nil))))))
+}
+
 // hashConfig returns the hash for the *packages.Config.
 func hashConfig(config *packages.Config) string {
 	b := bytes.NewBuffer(nil)
@@ -143,16 +180,12 @@
 
 	v := cph.handle.Get(ctx)
 	if v == nil {
-		return nil, ctx.Err()
+		return nil, errors.Errorf("no package for %s", cph.m.id)
 	}
 	data := v.(*checkPackageData)
 	return data.pkg, data.err
 }
 
-func (cph *checkPackageHandle) Config() *packages.Config {
-	return cph.config
-}
-
 func (cph *checkPackageHandle) Files() []source.ParseGoHandle {
 	return cph.files
 }
@@ -182,15 +215,7 @@
 	return data.pkg, data.err
 }
 
-func (cph *checkPackageHandle) key() checkPackageKey {
-	return checkPackageKey{
-		id:     string(cph.m.id),
-		files:  hashParseKeys(cph.files),
-		config: hashConfig(cph.config),
-	}
-}
-
-func (imp *importer) parseGoHandles(ctx context.Context, m *metadata) ([]source.ParseGoHandle, error) {
+func (imp *importer) parseGoHandles(ctx context.Context, m *metadata, mode source.ParseMode) ([]source.ParseGoHandle, error) {
 	phs := make([]source.ParseGoHandle, 0, len(m.files))
 	for _, uri := range m.files {
 		f, err := imp.snapshot.view.GetFile(ctx, uri)
@@ -198,15 +223,18 @@
 			return nil, err
 		}
 		fh := imp.snapshot.Handle(ctx, f)
-		mode := source.ParseExported
-		if imp.topLevelPackageID == m.id {
-			mode = source.ParseFull
-		}
 		phs = append(phs, imp.snapshot.view.session.cache.ParseGoHandle(fh, mode))
 	}
 	return phs, nil
 }
 
+func (imp *importer) mode(id packageID) source.ParseMode {
+	if imp.topLevelPackageID == id {
+		return source.ParseFull
+	}
+	return source.ParseExported
+}
+
 func (imp *importer) Import(pkgPath string) (*types.Package, error) {
 	ctx, done := trace.StartSpan(imp.ctx, "cache.importer.Import", telemetry.PackagePath.Of(pkgPath))
 	defer done()
@@ -215,16 +243,14 @@
 	if imp.parentPkg == nil {
 		return nil, errors.Errorf("no parent package for import %s", pkgPath)
 	}
-
 	// Get the CheckPackageHandle from the importing package.
-	cph, ok := imp.parentCheckPackageHandle.imports[packagePath(pkgPath)]
+	id, ok := imp.parentCheckPackageHandle.imports[packagePath(pkgPath)]
 	if !ok {
 		return nil, errors.Errorf("no package data for import path %s", pkgPath)
 	}
-	for _, ph := range cph.Files() {
-		if ph.Mode() != source.ParseExported {
-			panic("dependency parsed in full mode")
-		}
+	cph := imp.snapshot.getPackage(id, source.ParseExported)
+	if cph == nil {
+		return nil, errors.Errorf("no package for %s", id)
 	}
 	pkg, err := cph.check(ctx)
 	if err != nil {
@@ -234,17 +260,17 @@
 	return pkg.GetTypes(), nil
 }
 
-func (imp *importer) typeCheck(ctx context.Context, cph *checkPackageHandle, m *metadata) (*pkg, error) {
-	ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck", telemetry.Package.Of(m.id))
+func (imp *importer) typeCheck(ctx context.Context, cph *checkPackageHandle) (*pkg, error) {
+	ctx, done := trace.StartSpan(ctx, "cache.importer.typeCheck", telemetry.Package.Of(cph.m.id))
 	defer done()
 
 	pkg := &pkg{
 		view:       imp.snapshot.view,
-		id:         m.id,
-		pkgPath:    m.pkgPath,
+		id:         cph.m.id,
+		pkgPath:    cph.m.pkgPath,
 		files:      cph.Files(),
 		imports:    make(map[packagePath]*pkg),
-		typesSizes: m.typesSizes,
+		typesSizes: cph.m.typesSizes,
 		typesInfo: &types.Info{
 			Types:      make(map[ast.Expr]types.TypeAndValue),
 			Defs:       make(map[*ast.Ident]types.Object),
@@ -257,22 +283,8 @@
 	}
 	// If the package comes back with errors from `go list`,
 	// don't bother type-checking it.
-	for _, err := range m.errors {
-		pkg.errors = append(m.errors, err)
-	}
-	// Set imports of package to correspond to cached packages.
-	cimp := imp.child(ctx, pkg, cph)
-	for _, depID := range m.deps {
-		dep := imp.snapshot.getMetadata(depID)
-		if dep == nil {
-			continue
-		}
-		depHandle, err := cimp.checkPackageHandle(ctx, depID, imp.snapshot)
-		if err != nil {
-			log.Error(ctx, "no check package handle", err, telemetry.Package.Of(depID))
-			continue
-		}
-		cph.imports[dep.pkgPath] = depHandle
+	for _, err := range cph.m.errors {
+		pkg.errors = append(cph.m.errors, err)
 	}
 	var (
 		files       = make([]*ast.File, len(pkg.files))
@@ -305,19 +317,19 @@
 	files = files[:i]
 
 	// Use the default type information for the unsafe package.
-	if m.pkgPath == "unsafe" {
+	if cph.m.pkgPath == "unsafe" {
 		pkg.types = types.Unsafe
 	} else if len(files) == 0 { // not the unsafe package, no parsed files
 		return nil, errors.Errorf("no parsed files for package %s", pkg.pkgPath)
 	} else {
-		pkg.types = types.NewPackage(string(m.pkgPath), m.name)
+		pkg.types = types.NewPackage(string(cph.m.pkgPath), cph.m.name)
 	}
 
 	cfg := &types.Config{
 		Error: func(err error) {
 			imp.snapshot.view.session.cache.appendPkgError(pkg, err)
 		},
-		Importer: cimp,
+		Importer: imp.depImporter(ctx, cph, pkg),
 	}
 	check := types.NewChecker(cfg, imp.snapshot.view.session.cache.FileSet(), pkg.types, pkg.typesInfo)
 
@@ -327,7 +339,7 @@
 	return pkg, nil
 }
 
-func (imp *importer) child(ctx context.Context, pkg *pkg, cph *checkPackageHandle) *importer {
+func (imp *importer) depImporter(ctx context.Context, cph *checkPackageHandle, pkg *pkg) *importer {
 	// Handle circular imports by copying previously seen imports.
 	seen := make(map[packageID]struct{})
 	for k, v := range imp.seen {
@@ -336,12 +348,11 @@
 	seen[pkg.id] = struct{}{}
 	return &importer{
 		snapshot:                 imp.snapshot,
-		ctx:                      ctx,
-		config:                   imp.config,
-		seen:                     seen,
 		topLevelPackageID:        imp.topLevelPackageID,
 		parentPkg:                pkg,
 		parentCheckPackageHandle: cph,
+		seen:                     seen,
+		ctx:                      ctx,
 	}
 }
 
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index e3ed8ec..1435ffa 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -21,6 +21,9 @@
 	if err != nil {
 		return nil, nil, err
 	}
+	if len(cphs) == 0 {
+		return nil, nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
+	}
 	return s, cphs, nil
 }
 
@@ -31,13 +34,12 @@
 
 	// Determine if we need to type-check the package.
 	m, cphs, load, check := s.shouldCheck(fh)
-	cfg := s.view.Config(ctx)
 
 	// We may need to re-load package metadata.
 	// We only need to this if it has been invalidated, and is therefore unvailable.
 	if load {
 		var err error
-		m, err = s.load(ctx, f.URI(), cfg)
+		m, err = s.load(ctx, f.URI())
 		if err != nil {
 			return nil, err
 		}
@@ -51,12 +53,11 @@
 		var results []source.CheckPackageHandle
 		for _, m := range m {
 			imp := &importer{
-				config:            cfg,
-				seen:              make(map[packageID]struct{}),
-				topLevelPackageID: m.id,
 				snapshot:          s,
+				topLevelPackageID: m.id,
+				seen:              make(map[packageID]struct{}),
 			}
-			cph, err := imp.checkPackageHandle(ctx, m.id, s)
+			cph, err := imp.checkPackageHandle(ctx, m.id)
 			if err != nil {
 				return nil, err
 			}
@@ -64,9 +65,6 @@
 		}
 		cphs = results
 	}
-	if len(cphs) == 0 {
-		return nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
-	}
 	return cphs, nil
 }
 
@@ -90,26 +88,8 @@
 	}
 	// We expect to see a checked package for each package ID,
 	// and it should be parsed in full mode.
-	var (
-		expected   = len(m)
-		cachedCPHs = s.getPackages(fh.Identity().URI)
-	)
-	if len(cachedCPHs) < expected {
-		return m, nil, load, true
-	}
-	for _, cph := range cachedCPHs {
-		// The package may have been checked in the exported mode.
-		if cph.Mode() != source.ParseFull {
-			continue
-		}
-		// Confirm that the file belongs to this package.
-		for _, file := range cph.Files() {
-			if file.File().Identity() == fh.Identity() {
-				cphs = append(cphs, cph)
-			}
-		}
-	}
-	if len(cphs) < expected {
+	cphs = s.getPackages(fh.Identity().URI, source.ParseFull)
+	if len(cphs) < len(m) {
 		return m, nil, load, true
 	}
 	return m, cphs, load, check
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index f175b5f..cdbe970 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -33,12 +33,16 @@
 	errors      []packages.Error
 	deps        []packageID
 	missingDeps map[packagePath]struct{}
+
+	// config is the *packages.Config associated with the loaded package.
+	config *packages.Config
 }
 
-func (s *snapshot) load(ctx context.Context, uri span.URI, cfg *packages.Config) ([]*metadata, error) {
+func (s *snapshot) load(ctx context.Context, uri span.URI) ([]*metadata, error) {
 	ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.URI.Of(uri))
 	defer done()
 
+	cfg := s.view.Config(ctx)
 	pkgs, err := packages.Load(cfg, fmt.Sprintf("file=%s", uri.Filename()))
 	log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
 
@@ -49,7 +53,7 @@
 		// Return this error as a diagnostic to the user.
 		return nil, err
 	}
-	m, prevMissingImports, err := s.updateMetadata(ctx, uri, pkgs)
+	m, prevMissingImports, err := s.updateMetadata(ctx, uri, pkgs, cfg)
 	if err != nil {
 		return nil, err
 	}
@@ -103,12 +107,14 @@
 	// Get the original parsed file in order to check package name and imports.
 	original, _, _, err := c.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx)
 	if err != nil {
+		log.Error(ctx, "no ParseGoHandle for original FileHandle", err, telemetry.URI.Of(originalFH.Identity().URI))
 		return false
 	}
 
 	// Get the current parsed file in order to check package name and imports.
 	current, _, _, err := c.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx)
 	if err != nil {
+		log.Error(ctx, "no ParseGoHandle for original FileHandle", err, telemetry.URI.Of(currentFH.Identity().URI))
 		return false
 	}
 
@@ -133,7 +139,7 @@
 	return false
 }
 
-func (s *snapshot) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package) ([]*metadata, map[packageID]map[packagePath]struct{}, error) {
+func (s *snapshot) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package, cfg *packages.Config) ([]*metadata, map[packageID]map[packagePath]struct{}, error) {
 	// Clear metadata since we are re-running go/packages.
 	prevMissingImports := make(map[packageID]map[packagePath]struct{})
 	m := s.getMetadataForURI(uri)
@@ -149,7 +155,7 @@
 		log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
 
 		// Set the metadata for this package.
-		if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg); err != nil {
+		if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg); err != nil {
 			return nil, nil, err
 		}
 		m := s.getMetadata(packageID(pkg.ID))
@@ -167,7 +173,7 @@
 	return results, prevMissingImports, nil
 }
 
-func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package) error {
+func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config) error {
 	// Recreate the metadata rather than reusing it to avoid locking.
 	m := &metadata{
 		id:         packageID(pkg.ID),
@@ -175,6 +181,7 @@
 		name:       pkg.Name,
 		typesSizes: pkg.TypesSizes,
 		errors:     pkg.Errors,
+		config:     cfg,
 	}
 	for _, filename := range pkg.CompiledGoFiles {
 		uri := span.FileURI(filename)
@@ -205,7 +212,7 @@
 		}
 		dep := s.getMetadata(importID)
 		if dep == nil {
-			if err := s.updateImports(ctx, importPkgPath, importPkg); err != nil {
+			if err := s.updateImports(ctx, importPkgPath, importPkg, cfg); err != nil {
 				log.Error(ctx, "error in dependency", err)
 			}
 		}
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 724746a..37b7818 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -75,7 +75,7 @@
 func (h *parseGoHandle) Parse(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error, error) {
 	v := h.handle.Get(ctx)
 	if v == nil {
-		return nil, nil, nil, ctx.Err()
+		return nil, nil, nil, errors.Errorf("no parsed file for %s", h.File().Identity().URI)
 	}
 	data := v.(*parseGoData)
 	return data.ast, data.mapper, data.parseError, data.err
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index 4ba27a2..513e66f 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -208,11 +208,11 @@
 	p.diagnostics[a] = diags
 }
 
-func (p *pkg) FindDiagnostic(pdiag protocol.Diagnostic) (*source.Diagnostic, error) {
-	p.diagMu.Lock()
-	defer p.diagMu.Unlock()
+func (pkg *pkg) FindDiagnostic(pdiag protocol.Diagnostic) (*source.Diagnostic, error) {
+	pkg.diagMu.Lock()
+	defer pkg.diagMu.Unlock()
 
-	for a, diagnostics := range p.diagnostics {
+	for a, diagnostics := range pkg.diagnostics {
 		if a.Name != pdiag.Source {
 			continue
 		}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 4e175b5..5a41423 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -99,15 +99,17 @@
 		filesByURI:    make(map[span.URI]viewFile),
 		filesByBase:   make(map[string][]viewFile),
 		snapshot: &snapshot{
-			packages: make(map[span.URI]map[packageKey]*checkPackageHandle),
-			ids:      make(map[span.URI][]packageID),
-			metadata: make(map[packageID]*metadata),
-			files:    make(map[span.URI]source.FileHandle),
+			packages:   make(map[packageKey]*checkPackageHandle),
+			ids:        make(map[span.URI][]packageID),
+			metadata:   make(map[packageID]*metadata),
+			files:      make(map[span.URI]source.FileHandle),
+			importedBy: make(map[packageID][]packageID),
 		},
 		ignoredURIs: make(map[span.URI]struct{}),
 		builtin:     &builtinPkg{},
 	}
 	v.snapshot.view = v
+
 	v.analyzers = UpdateAnalyzers(v, defaultAnalyzers)
 	// Preemptively build the builtin package,
 	// so we immediately add builtin.go to the list of ignored files.
@@ -236,7 +238,7 @@
 	// A file may be in multiple views.
 	for _, view := range s.views {
 		if strings.HasPrefix(string(uri), string(view.Folder())) {
-			view.invalidateMetadata(uri)
+			view.invalidateMetadata(ctx, uri)
 		}
 	}
 }
@@ -350,7 +352,7 @@
 		// force a go/packages invocation to refresh the package's file list.
 		views := s.viewsOf(uri)
 		for _, v := range views {
-			v.invalidateMetadata(uri)
+			v.invalidateMetadata(ctx, uri)
 		}
 	}
 	s.filesWatchMap.Notify(uri)
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 972cad1..ab76e40 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -31,7 +31,7 @@
 
 	// packages maps a file URI to a set of CheckPackageHandles to which that file belongs.
 	// It may be invalidated when a file's content changes.
-	packages map[span.URI]map[packageKey]*checkPackageHandle
+	packages map[packageKey]*checkPackageHandle
 }
 
 func (s *snapshot) getImportedBy(id packageID) []packageID {
@@ -46,33 +46,49 @@
 	return s.importedBy[id]
 }
 
-func (s *snapshot) addPackage(uri span.URI, cph *checkPackageHandle) {
+func (s *snapshot) addPackage(cph *checkPackageHandle) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	pkgs, ok := s.packages[uri]
-	if !ok {
-		pkgs = make(map[packageKey]*checkPackageHandle)
-		s.packages[uri] = pkgs
+	// TODO: We should make sure not to compute duplicate CheckPackageHandles,
+	// and instead panic here. This will be hard to do because we may encounter
+	// the same package multiple times in the dependency tree.
+	if _, ok := s.packages[cph.packageKey()]; ok {
+		return
 	}
-	// TODO: Check that there isn't already something set here.
-	// This can't be done until we fix the cache keys for CheckPackageHandles.
-	pkgs[packageKey{
-		id:   cph.m.id,
-		mode: cph.Mode(),
-	}] = cph
+	s.packages[cph.packageKey()] = cph
 }
 
-func (s *snapshot) getPackages(uri span.URI) (cphs []source.CheckPackageHandle) {
+func (s *snapshot) getPackages(uri span.URI, m source.ParseMode) (cphs []source.CheckPackageHandle) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	for _, cph := range s.packages[uri] {
-		cphs = append(cphs, cph)
+	if ids, ok := s.ids[uri]; ok {
+		for _, id := range ids {
+			key := packageKey{
+				id:   id,
+				mode: m,
+			}
+			cph, ok := s.packages[key]
+			if ok {
+				cphs = append(cphs, cph)
+			}
+		}
 	}
 	return cphs
 }
 
+func (s *snapshot) getPackage(id packageID, m source.ParseMode) *checkPackageHandle {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	key := packageKey{
+		id:   id,
+		mode: m,
+	}
+	return s.packages[key]
+}
+
 func (s *snapshot) getMetadataForURI(uri span.URI) (metadata []*metadata) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
@@ -89,6 +105,12 @@
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	// TODO: We should make sure not to set duplicate metadata,
+	// and instead panic here. This can be done by making sure not to
+	// reset metadata information for packages we've already seen.
+	if _, ok := s.metadata[m.id]; ok {
+		return
+	}
 	s.metadata[m.id] = m
 }
 
@@ -103,6 +125,14 @@
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	for _, existingID := range s.ids[uri] {
+		if existingID == id {
+			// TODO: We should make sure not to set duplicate IDs,
+			// and instead panic here. This can be done by making sure not to
+			// reset metadata information for packages we've already seen.
+			return
+		}
+	}
 	s.ids[uri] = append(s.ids[uri], id)
 }
 
@@ -130,52 +160,67 @@
 	return s.files[f.URI()]
 }
 
-func (s *snapshot) clone(withoutURI span.URI, withoutTypes, withoutMetadata map[span.URI]struct{}) *snapshot {
+func (s *snapshot) clone(ctx context.Context, withoutURI *span.URI, withoutTypes, withoutMetadata map[span.URI]struct{}) *snapshot {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
 	result := &snapshot{
 		id:         s.id + 1,
 		view:       s.view,
-		packages:   make(map[span.URI]map[packageKey]*checkPackageHandle),
 		ids:        make(map[span.URI][]packageID),
-		metadata:   make(map[packageID]*metadata),
 		importedBy: make(map[packageID][]packageID),
+		metadata:   make(map[packageID]*metadata),
+		packages:   make(map[packageKey]*checkPackageHandle),
 		files:      make(map[span.URI]source.FileHandle),
 	}
 	// Copy all of the FileHandles except for the one that was invalidated.
 	for k, v := range s.files {
-		if k == withoutURI {
+		if withoutURI != nil && k == *withoutURI {
 			continue
 		}
 		result.files[k] = v
 	}
-	for k, v := range s.packages {
+	// Collect the IDs for the packages associated with the excluded URIs.
+	withoutMetadataIDs := make(map[packageID]struct{})
+	withoutTypesIDs := make(map[packageID]struct{})
+	for k, ids := range s.ids {
+		// Map URIs to IDs for exclusion.
 		if withoutTypes != nil {
 			if _, ok := withoutTypes[k]; ok {
-				continue
+				for _, id := range ids {
+					withoutTypesIDs[id] = struct{}{}
+				}
 			}
 		}
-		result.packages[k] = v
-	}
-	withoutIDs := make(map[packageID]struct{})
-	for k, ids := range s.ids {
 		if withoutMetadata != nil {
 			if _, ok := withoutMetadata[k]; ok {
 				for _, id := range ids {
-					withoutIDs[id] = struct{}{}
+					withoutMetadataIDs[id] = struct{}{}
 				}
 				continue
 			}
 		}
 		result.ids[k] = ids
 	}
+	// Copy the package type information.
+	for k, v := range s.packages {
+		if _, ok := withoutTypesIDs[k.id]; ok {
+			continue
+		}
+		if _, ok := withoutMetadataIDs[k.id]; ok {
+			continue
+		}
+		result.packages[k] = v
+	}
+	// Copy the package metadata.
 	for k, v := range s.metadata {
-		if _, ok := withoutIDs[k]; ok {
+		if _, ok := withoutMetadataIDs[k]; ok {
 			continue
 		}
 		result.metadata[k] = v
 	}
+	// Don't bother copying the importedBy graph,
+	// as it changes each time we update metadata.
 	return result
 }
 
@@ -210,8 +255,7 @@
 		// TODO: If a package's name has changed,
 		// we should invalidate the metadata for the new package name (if it exists).
 	}
-
-	v.snapshot = v.snapshot.clone(uri, withoutTypes, withoutMetadata)
+	v.snapshot = v.snapshot.clone(ctx, &uri, withoutTypes, withoutMetadata)
 }
 
 // invalidateMetadata invalidates package metadata for all files in f's
@@ -220,15 +264,16 @@
 //
 // TODO: This function shouldn't be necessary.
 // We should be able to handle its use cases more efficiently.
-func (v *view) invalidateMetadata(uri span.URI) {
+func (v *view) invalidateMetadata(ctx context.Context, uri span.URI) {
 	v.snapshotMu.Lock()
 	defer v.snapshotMu.Unlock()
 
 	withoutMetadata := make(map[span.URI]struct{})
+
 	for _, id := range v.snapshot.getIDs(uri) {
 		v.snapshot.reverseDependencies(id, withoutMetadata, map[packageID]struct{}{})
 	}
-	v.snapshot = v.snapshot.clone(uri, nil, withoutMetadata)
+	v.snapshot = v.snapshot.clone(ctx, nil, withoutMetadata, withoutMetadata)
 }
 
 // reverseDependencies populates the uris map with file URIs belonging to the
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index f9279dc..c840a81 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -22,6 +22,7 @@
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/telemetry/log"
+	"golang.org/x/tools/internal/xcontext"
 	errors "golang.org/x/xerrors"
 )
 
@@ -353,6 +354,7 @@
 		kind:  source.Go,
 	}
 	v.session.filesWatchMap.Watch(uri, func() {
+		ctx := xcontext.Detach(ctx)
 		v.invalidateContent(ctx, uri, kind)
 	})
 	v.mapFile(uri, f)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 16b0d9d..fa85bc0 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -105,13 +105,6 @@
 	// ParseGoHandle returns a ParseGoHandle for which to get the package.
 	Files() []ParseGoHandle
 
-	// Config is the *packages.Config that the package metadata was loaded with.
-	Config() *packages.Config
-
-	// Mode returns the ParseMode for all of the files in the CheckPackageHandle.
-	// The files should always have the same parse mode.
-	Mode() ParseMode
-
 	// Check returns the type-checked Package for the CheckPackageHandle.
 	Check(ctx context.Context) (Package, error)