gopls/internal/lsp/cache: eliminate obsolete invalidation step

This change removes the call to invalidatePackagesLocked after
buildMetadata in the load method; now that we no longer permit
invalid metadata, it is unnecessary.
(The sole remaining call to invalidatePackagesLocked was
inlined into Clone.)

Also:
- check the invalidation invariant in load before Clone.
- merge MetadataForFile and getOrLoadIDsForURI since all
  callers want the Metadata (not just PackageID) and the
  defensive check is no longer needed.
- simplify awaitLoadedAllErrors by calling getInitializationError.
- reduce allocation and critical sections in various
  snapshot methods that retrieve the metadataGraph.
- flag various places where we can avoid type-checking.
- various other doc comments.

Updates golang/go#54180

Change-Id: I82dca203b2520259630b2fd9d08e120030d44a96
Reviewed-on: https://go-review.googlesource.com/c/tools/+/452057
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 5f31958..321ff5f 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -21,6 +21,7 @@
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/span"
+	"golang.org/x/tools/internal/bug"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/event/tag"
 	"golang.org/x/tools/internal/gocommand"
@@ -213,48 +214,30 @@
 
 	s.mu.Lock()
 
-	// Only update metadata where we don't already have valid metadata.
-	//
-	// We want to preserve an invariant that s.packages.Get(id).m.Metadata
-	// matches s.meta.metadata[id].Metadata. By avoiding overwriting valid
-	// metadata, we minimize the amount of invalidation required to preserve this
-	// invariant.
-	//
-	// TODO(rfindley): perform a sanity check that metadata matches here. If not,
-	// we have an invalidation bug elsewhere.
+	// Compute the minimal metadata updates (for Clone)
+	// required to preserve this invariant:
+	// for all id, s.packages.Get(id).m == s.meta.metadata[id].
 	updates := make(map[PackageID]*source.Metadata)
-	var updatedIDs []PackageID
 	for _, m := range newMetadata {
 		if existing := s.meta.metadata[m.ID]; existing == nil {
 			updates[m.ID] = m
-			updatedIDs = append(updatedIDs, m.ID)
 			delete(s.shouldLoad, m.ID)
 		}
 	}
+	// Assert the invariant.
+	s.packages.Range(func(k, v interface{}) {
+		pk, ph := k.(packageKey), v.(*packageHandle)
+		if s.meta.metadata[pk.id] != ph.m {
+			// TODO(adonovan): upgrade to unconditional panic after Jan 2023.
+			bug.Reportf("inconsistent metadata")
+		}
+	})
 
 	event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates)))
 
-	// Invalidate the reverse transitive closure of packages that have changed.
-	//
-	// Note that the original metadata is being invalidated here, so we use the
-	// original metadata graph to compute the reverse closure.
-	invalidatedPackages := s.meta.reverseTransitiveClosure(updatedIDs...)
-
 	s.meta = s.meta.Clone(updates)
 	s.resetIsActivePackageLocked()
 
-	// Invalidate any packages and analysis results we may have associated with
-	// this metadata.
-	//
-	// Generally speaking we should have already invalidated these results in
-	// snapshot.clone, but with experimentalUseInvalidMetadata is may be possible
-	// that we have re-computed stale results before the reload completes. In
-	// this case, we must re-invalidate here.
-	//
-	// TODO(golang/go#54180): if we decide to make experimentalUseInvalidMetadata
-	// obsolete, we should avoid this invalidation.
-	s.invalidatePackagesLocked(invalidatedPackages)
-
 	s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta)
 	s.dumpWorkspace("load")
 	s.mu.Unlock()
diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go
index 6466b8e..5b927a5 100644
--- a/gopls/internal/lsp/cache/pkg.go
+++ b/gopls/internal/lsp/cache/pkg.go
@@ -25,7 +25,7 @@
 	ImportPath  = source.ImportPath
 )
 
-// pkg contains the type information needed by the source package.
+// pkg contains parse trees and type information for a package.
 type pkg struct {
 	m               *source.Metadata
 	mode            source.ParseMode
@@ -34,7 +34,7 @@
 	compiledGoFiles []*source.ParsedGoFile
 	diagnostics     []*source.Diagnostic
 	deps            map[PackageID]*pkg // use m.DepsBy{Pkg,Imp}Path to look up ID
-	version         *module.Version
+	version         *module.Version    // may be nil; may differ from m.Module.Version
 	parseErrors     []scanner.ErrorList
 	typeErrors      []types.Error
 	types           *types.Package
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 369235f..1f738c6 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -100,10 +100,10 @@
 	// It may be invalidated when a file's content changes.
 	//
 	// Invariants to preserve:
-	//  - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids
+	//  - packages.Get(id).meta == meta.metadata[id] for all ids
 	//  - if a package is in packages, then all of its dependencies should also
 	//    be in packages, unless there is a missing import
-	packages *persistent.Map // from packageKey to *memoize.Promise[*packageHandle]
+	packages *persistent.Map // from packageKey to *packageHandle
 
 	// isActivePackageCache maps package ID to the cached value if it is active or not.
 	// It may be invalidated when metadata changes or a new file is opened or closed.
@@ -655,6 +655,8 @@
 func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, pkgPolicy source.PackageFilter) (source.Package, error) {
 	ctx = event.Label(ctx, tag.URI.Of(uri))
 
+	// TODO(adonovan): opt: apply pkgPolicy to the list of
+	// Metadatas before initiating loading of any package.
 	phs, err := s.packageHandlesForFile(ctx, uri, mode, false)
 	if err != nil {
 		return nil, err
@@ -699,24 +701,24 @@
 	if kind := s.view.FileKind(fh); kind != source.Go {
 		return nil, fmt.Errorf("no packages for non-Go file %s (%v)", uri, kind)
 	}
-	knownIDs, err := s.getOrLoadIDsForURI(ctx, uri)
+	metas, err := s.MetadataForFile(ctx, uri)
 	if err != nil {
 		return nil, err
 	}
 
 	var phs []*packageHandle
-	for _, id := range knownIDs {
+	for _, m := range metas {
 		// Filter out any intermediate test variants. We typically aren't
 		// interested in these packages for file= style queries.
-		if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants {
+		if m.IsIntermediateTestVariant() && !withIntermediateTestVariants {
 			continue
 		}
 		parseMode := source.ParseFull
 		if mode == source.TypecheckWorkspace {
-			parseMode = s.workspaceParseMode(id)
+			parseMode = s.workspaceParseMode(m.ID)
 		}
 
-		ph, err := s.buildPackageHandle(ctx, id, parseMode)
+		ph, err := s.buildPackageHandle(ctx, m.ID, parseMode)
 		if err != nil {
 			return nil, err
 		}
@@ -725,12 +727,10 @@
 	return phs, nil
 }
 
-// getOrLoadIDsForURI returns package IDs associated with the file uri. If no
-// such packages exist or if they are known to be stale, it reloads the file.
-//
-// If experimentalUseInvalidMetadata is set, this function may return package
-// IDs with invalid metadata.
-func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) {
+// MetadataForFile returns the Metadata for each package associated
+// with the file uri. If no such package exists or if they are known
+// to be stale, it reloads metadata for the file.
+func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) {
 	s.mu.Lock()
 
 	// Start with the set of package associations derived from the last load.
@@ -772,23 +772,31 @@
 		s.clearShouldLoad(scope)
 
 		// Don't return an error here, as we may still return stale IDs.
-		// Furthermore, the result of getOrLoadIDsForURI should be consistent upon
+		// Furthermore, the result of MetadataForFile should be consistent upon
 		// subsequent calls, even if the file is marked as unloadable.
 		if err != nil && !errors.Is(err, errNoPackages) {
-			event.Error(ctx, "getOrLoadIDsForURI", err)
+			event.Error(ctx, "MetadataForFile", err)
 		}
 	}
 
+	// Retrieve the metadata.
 	s.mu.Lock()
+	defer s.mu.Unlock()
 	ids = s.meta.ids[uri]
-	// metadata is only ever added by loading, so if we get here and still have
-	// no ids, uri is unloadable.
+	metas := make([]*source.Metadata, len(ids))
+	for i, id := range ids {
+		metas[i] = s.meta.metadata[id]
+		if metas[i] == nil {
+			panic("nil metadata")
+		}
+	}
+	// Metadata is only ever added by loading,
+	// so if we get here and still have
+	// no IDs, uri is unloadable.
 	if !unloadable && len(ids) == 0 {
 		s.unloadableFiles[uri] = struct{}{}
 	}
-	s.mu.Unlock()
-
-	return ids, nil
+	return metas, nil
 }
 
 func (s *snapshot) GetReverseDependencies(ctx context.Context, id PackageID) ([]source.Package, error) {
@@ -1135,37 +1143,17 @@
 	return result
 }
 
-func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) {
-	knownIDs, err := s.getOrLoadIDsForURI(ctx, uri)
-	if err != nil {
-		return nil, err
-	}
-	var mds []*source.Metadata
-	for _, id := range knownIDs {
-		md := s.getMetadata(id)
-		// TODO(rfindley): knownIDs and metadata should be in sync, but existing
-		// code is defensive of nil metadata.
-		if md != nil {
-			mds = append(mds, md)
-		}
-	}
-	return mds, nil
-}
-
 func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) {
 	if err := s.awaitLoaded(ctx); err != nil {
 		return nil, err
 	}
 
-	ids := make([]source.PackageID, 0, len(s.meta.metadata))
 	s.mu.Lock()
-	for id := range s.meta.metadata {
-		ids = append(ids, id)
-	}
+	g := s.meta
 	s.mu.Unlock()
 
-	pkgs := make([]source.Package, 0, len(ids))
-	for _, id := range ids {
+	pkgs := make([]source.Package, 0, len(g.metadata))
+	for id := range g.metadata {
 		pkg, err := s.checkedPackage(ctx, id, s.workspaceParseMode(id))
 		if err != nil {
 			return nil, err
@@ -1181,10 +1169,11 @@
 	}
 
 	s.mu.Lock()
-	defer s.mu.Unlock()
+	g := s.meta
+	s.mu.Unlock()
 
-	var meta []*source.Metadata
-	for _, m := range s.meta.metadata {
+	meta := make([]*source.Metadata, 0, len(g.metadata))
+	for _, m := range g.metadata {
 		meta = append(meta, m)
 	}
 	return meta, nil
@@ -1281,11 +1270,7 @@
 // noValidMetadataForURILocked reports whether there is any valid metadata for
 // the given URI.
 func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool {
-	ids, ok := s.meta.ids[uri]
-	if !ok {
-		return true
-	}
-	for _, id := range ids {
+	for _, id := range s.meta.ids[uri] {
 		if _, ok := s.meta.metadata[id]; ok {
 			return false
 		}
@@ -1483,12 +1468,8 @@
 	// TODO(rfindley): Should we be more careful about returning the
 	// initialization error? Is it possible for the initialization error to be
 	// corrected without a successful reinitialization?
-	s.mu.Lock()
-	initializedErr := s.initializedErr
-	s.mu.Unlock()
-
-	if initializedErr != nil {
-		return initializedErr
+	if err := s.getInitializationError(); err != nil {
+		return err
 	}
 
 	// TODO(rfindley): revisit this handling. Calling reloadWorkspace with a
@@ -1928,7 +1909,25 @@
 		addRevDeps(id, invalidateMetadata)
 	}
 
-	result.invalidatePackagesLocked(idsToInvalidate)
+	// Delete invalidated package type information.
+	for id := range idsToInvalidate {
+		for _, mode := range source.AllParseModes {
+			key := packageKey{mode, id}
+			result.packages.Delete(key)
+		}
+	}
+
+	// Delete invalidated analysis actions.
+	var actionsToDelete []actionKey
+	result.actions.Range(func(k, _ interface{}) {
+		key := k.(actionKey)
+		if _, ok := idsToInvalidate[key.pkgid]; ok {
+			actionsToDelete = append(actionsToDelete, key)
+		}
+	})
+	for _, key := range actionsToDelete {
+		result.actions.Delete(key)
+	}
 
 	// If a file has been deleted, we must delete metadata for all packages
 	// containing that file.
@@ -2080,35 +2079,6 @@
 	return invalidated
 }
 
-// invalidatePackagesLocked deletes data associated with the given package IDs.
-//
-// Note: all keys in the ids map are invalidated, regardless of the
-// corresponding value.
-//
-// s.mu must be held while calling this function.
-func (s *snapshot) invalidatePackagesLocked(ids map[PackageID]bool) {
-	// Delete invalidated package type information.
-	for id := range ids {
-		for _, mode := range source.AllParseModes {
-			key := packageKey{mode, id}
-			s.packages.Delete(key)
-		}
-	}
-
-	// Copy actions.
-	// TODO(adonovan): opt: avoid iteration over s.actions.
-	var actionsToDelete []actionKey
-	s.actions.Range(func(k, _ interface{}) {
-		key := k.(actionKey)
-		if _, ok := ids[key.pkgid]; ok {
-			actionsToDelete = append(actionsToDelete, key)
-		}
-	})
-	for _, key := range actionsToDelete {
-		s.actions.Delete(key)
-	}
-}
-
 // fileWasSaved reports whether the FileHandle passed in has been saved. It
 // accomplishes this by checking to see if the original and current FileHandles
 // are both overlays, and if the current FileHandle is saved while the original
diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go
index d19cafc..275348b 100644
--- a/gopls/internal/lsp/code_action.go
+++ b/gopls/internal/lsp/code_action.go
@@ -212,7 +212,7 @@
 		}
 
 		if wanted[protocol.RefactorExtract] {
-			fixes, err := extractionFixes(ctx, snapshot, pkg, uri, params.Range)
+			fixes, err := extractionFixes(ctx, snapshot, uri, params.Range)
 			if err != nil {
 				return nil, err
 			}
@@ -305,7 +305,7 @@
 	return results
 }
 
-func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
+func extractionFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
 	if rng.Start == rng.End {
 		return nil, nil
 	}
@@ -313,6 +313,7 @@
 	if err != nil {
 		return nil, err
 	}
+	// TODO(adonovan): opt: avoid package loading; only parsing is needed.
 	_, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage)
 	if err != nil {
 		return nil, fmt.Errorf("getting file for Identifier: %w", err)
diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go
index 7f3652d..81e29bd 100644
--- a/gopls/internal/lsp/command.go
+++ b/gopls/internal/lsp/command.go
@@ -690,6 +690,7 @@
 		progress:    "Toggling GC Details",
 		forURI:      args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
+		// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
 		pkg, err := deps.snapshot.PackageForFile(ctx, deps.fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage)
 		if err != nil {
 			return err
@@ -756,6 +757,7 @@
 	err := c.run(ctx, commandConfig{
 		forURI: args.URI,
 	}, func(ctx context.Context, deps commandDeps) error {
+		// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
 		pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage)
 		if err != nil {
 			return err
diff --git a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go
index 7eb561f..ea1ff5a 100644
--- a/gopls/internal/lsp/link.go
+++ b/gopls/internal/lsp/link.go
@@ -103,6 +103,7 @@
 func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
 	view := snapshot.View()
 	// We don't actually need type information, so any typecheck mode is fine.
+	// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
 	pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckWorkspace, source.WidestPackage)
 	if err != nil {
 		return nil, err
@@ -174,6 +175,7 @@
 }
 
 func moduleAtVersion(targetImportPath source.ImportPath, pkg source.Package) (string, string, bool) {
+	// TODO(adonovan): opt: avoid need for package; use Metadata.DepsByImportPath only.
 	impPkg, err := pkg.ResolveImportPath(targetImportPath)
 	if err != nil {
 		return "", "", false
diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go
index 2fc03e5..29be148 100644
--- a/gopls/internal/lsp/source/add_import.go
+++ b/gopls/internal/lsp/source/add_import.go
@@ -7,12 +7,13 @@
 import (
 	"context"
 
-	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/gopls/internal/lsp/protocol"
+	"golang.org/x/tools/internal/imports"
 )
 
 // AddImport adds a single import statement to the given file
 func AddImport(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, importPath string) ([]protocol.TextEdit, error) {
+	// TODO(adonovan): opt: avoid loading type checked package; only parsing is needed.
 	_, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go
index 83ffcc2..afca98d 100644
--- a/gopls/internal/lsp/source/code_lens.go
+++ b/gopls/internal/lsp/source/code_lens.go
@@ -62,6 +62,7 @@
 	}
 
 	if len(fns.Benchmarks) > 0 {
+		// TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed.
 		_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
 		if err != nil {
 			return nil, err
@@ -100,6 +101,7 @@
 	if !strings.HasSuffix(fh.URI().Filename(), "_test.go") {
 		return out, nil
 	}
+	// TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed.
 	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
 	if err != nil {
 		return out, err
@@ -227,6 +229,7 @@
 }
 
 func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+	// TODO(adonovan): avoid loading type-checked package; only Metadata is needed.
 	_, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage)
 	if err != nil {
 		return nil, err
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go
index 64498e2..b220a6b 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/lsp/source/fix.go
@@ -109,6 +109,7 @@
 			}
 			editsPerFile[fh.URI()] = te
 		}
+		// TODO(adonovan): opt: avoid loading type-checked package; only Metadata is needed.
 		_, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
 		if err != nil {
 			return nil, err
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go
index 3d09e10..c252229 100644
--- a/gopls/internal/lsp/source/hover.go
+++ b/gopls/internal/lsp/source/hover.go
@@ -139,6 +139,7 @@
 
 // findRune returns rune information for a position in a file.
 func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (rune, MappedRange, error) {
+	// TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed.
 	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
 	if err != nil {
 		return 0, MappedRange{}, err
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go
index d031056..f147108 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/lsp/source/references.go
@@ -78,6 +78,10 @@
 		for _, dep := range rdeps {
 			for _, f := range dep.CompiledGoFiles() {
 				for _, imp := range f.File.Imports {
+					// TODO(adonovan): using UnquoteImport() here would
+					// reveal that there's an ImportPath==PackagePath
+					// comparison that doesn't account for vendoring.
+					// Use dep.Metadata().DepsByPkgPath instead.
 					if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == string(renamingPkg.PkgPath()) {
 						refs = append(refs, &ReferenceInfo{
 							Name:        packageName,
diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go
index 9e4b704..4cdfc71 100644
--- a/gopls/internal/lsp/source/stub.go
+++ b/gopls/internal/lsp/source/stub.go
@@ -280,6 +280,7 @@
 	objPos := snapshot.FileSet().Position(obj.Pos())
 	objFile := span.URIFromPath(objPos.Filename)
 	objectFH := snapshot.FindFile(objFile)
+	// TODO(adonovan): opt: avoid loading type-checked package; only parsing is needed.
 	_, goFile, err := GetParsedFile(ctx, snapshot, objectFH, WidestPackage)
 	if err != nil {
 		return nil, nil, fmt.Errorf("GetParsedFile: %w", err)
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go
index d7ab5c5..55267c9 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/lsp/source/util.go
@@ -94,6 +94,9 @@
 // GetParsedFile is a convenience function that extracts the Package and
 // ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/
 // WidestPackage.
+//
+// Type-checking is expensive. Call cheaper methods of Snapshot if all
+// you need is Metadata or parse trees.
 func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) {
 	pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy)
 	if err != nil {
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 9991850..0e9dcd2 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -210,7 +210,7 @@
 	// Symbols returns all symbols in the snapshot.
 	Symbols(ctx context.Context) map[span.URI][]Symbol
 
-	// Metadata returns package metadata associated with the given file URI.
+	// Metadata returns metadata for each package associated with the given file URI.
 	MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error)
 
 	// GetCriticalError returns any critical errors in the workspace.
@@ -693,7 +693,7 @@
 	PkgPath() PackagePath
 	GetTypesSizes() types.Sizes
 	ForTest() string
-	Version() *module.Version
+	Version() *module.Version // may differ from Metadata.Module.Version
 
 	// Results of parsing:
 	FileSet() *token.FileSet
diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go
index d29318e..a6f0f1c 100644
--- a/gopls/internal/vulncheck/command.go
+++ b/gopls/internal/vulncheck/command.go
@@ -369,17 +369,10 @@
 	// Group packages by modules since vuln db is keyed by module.
 	metadataByModule := map[source.PackagePath][]*source.Metadata{}
 	for _, md := range metadata {
-		// TODO(hyangah): delete after go.dev/cl/452057 is merged.
-		// After the cl, this becomes an impossible condition.
-		if md == nil {
-			continue
+		if md.Module != nil {
+			modulePath := source.PackagePath(md.Module.Path)
+			metadataByModule[modulePath] = append(metadataByModule[modulePath], md)
 		}
-		mi := md.Module
-		if mi == nil {
-			continue
-		}
-		modulePath := source.PackagePath(mi.Path)
-		metadataByModule[modulePath] = append(metadataByModule[modulePath], md)
 	}
 
 	// Request vuln entries from remote service.