internal/lsp/cache: invalidate reverse closure when loading packages
When setting new metadata for a package ID, we invalidate the
corresponding type-checked packages. Whenever we invalidate a package,
we must also invalidate its reverse transitive closure. Otherwise we may
end up in a scenario where the go/types import graph does not match
gopls' view of the import graph.
Change-Id: I8db6ff3e4a722656e6dde7907e7c0470375c4847
Reviewed-on: https://go-review.googlesource.com/c/tools/+/413683
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go
index 3f24773..dc7d4fa 100644
--- a/internal/lsp/cache/graph.go
+++ b/internal/lsp/cache/graph.go
@@ -128,3 +128,44 @@
}
}
}
+
+// reverseTransitiveClosure calculates the set of packages that transitively
+// reach an id in ids via their Deps. The result also includes given ids.
+//
+// If includeInvalid is false, the algorithm ignores packages with invalid
+// metadata (including those in the given list of ids).
+func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]struct{} {
+ seen := make(map[PackageID]struct{})
+ var visitAll func([]PackageID)
+ visitAll = func(ids []PackageID) {
+ for _, id := range ids {
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ m := g.metadata[id]
+ // Only use invalid metadata if we support it.
+ if m == nil || !(m.Valid || includeInvalid) {
+ continue
+ }
+ seen[id] = struct{}{}
+ visitAll(g.importedBy[id])
+ }
+ }
+ visitAll(ids)
+ return seen
+}
+
+func collectReverseTransitiveClosure(g *metadataGraph, includeInvalid bool, ids []PackageID, seen map[PackageID]struct{}) {
+ for _, id := range ids {
+ if _, ok := seen[id]; ok {
+ continue
+ }
+ m := g.metadata[id]
+ // Only use invalid metadata if we support it.
+ if m == nil || !(m.Valid || includeInvalid) {
+ continue
+ }
+ seen[id] = struct{}{}
+ collectReverseTransitiveClosure(g, includeInvalid, g.importedBy[id], seen)
+ }
+}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index bdafdcd..db9a06d 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -204,18 +204,28 @@
}
}
+ var loadedIDs []PackageID
+ for id := range updates {
+ loadedIDs = append(loadedIDs, id)
+ }
+
s.mu.Lock()
+
+ // invalidate the reverse transitive closure of packages that have changed.
+ invalidatedPackages := s.meta.reverseTransitiveClosure(true, loadedIDs...)
s.meta = s.meta.Clone(updates)
+
// Invalidate any packages we may have associated with this metadata.
//
- // TODO(rfindley): if we didn't already invalidate these in snapshot.clone,
- // shouldn't we invalidate the reverse transitive closure?
- for _, m := range updates {
+ // TODO(rfindley): this should not be necessary, as we should have already
+ // invalidated in snapshot.clone.
+ for id := range invalidatedPackages {
for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} {
- key := packageKey{mode, m.ID}
+ key := packageKey{mode, id}
delete(s.packages, key)
}
}
+
s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta)
s.dumpWorkspace("load")
s.mu.Unlock()
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 05f8928..8194750 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -70,6 +70,10 @@
builtin span.URI
// meta holds loaded metadata.
+ //
+ // meta is guarded by mu, but the metadataGraph itself is immutable.
+ // TODO(rfindley): in many places we hold mu while operating on meta, even
+ // though we only need to hold mu while reading the pointer.
meta *metadataGraph
// files maps file URIs to their corresponding FileHandles.
@@ -627,8 +631,10 @@
if err := s.awaitLoaded(ctx); err != nil {
return nil, err
}
- ids := make(map[PackageID]struct{})
- s.transitiveReverseDependencies(PackageID(id), ids)
+ s.mu.Lock()
+ meta := s.meta
+ s.mu.Unlock()
+ ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), PackageID(id))
// Make sure to delete the original package ID from the map.
delete(ids, PackageID(id))
@@ -652,24 +658,6 @@
return ph.check(ctx, s)
}
-// transitiveReverseDependencies populates the ids map with package IDs
-// belonging to the provided package and its transitive reverse dependencies.
-func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID]struct{}) {
- if _, ok := ids[id]; ok {
- return
- }
- m := s.getMetadata(id)
- // Only use invalid metadata if we support it.
- if m == nil || !(m.Valid || s.useInvalidMetadata()) {
- return
- }
- ids[id] = struct{}{}
- importedBy := s.getImportedBy(id)
- for _, parentID := range importedBy {
- s.transitiveReverseDependencies(parentID, ids)
- }
-}
-
func (s *snapshot) getGoFile(key parseKey) *parseGoHandle {
s.mu.Lock()
defer s.mu.Unlock()
@@ -1879,7 +1867,6 @@
}
// Invalidate reverse dependencies too.
- // TODO(heschi): figure out the locking model and use transitiveReverseDeps?
// idsToInvalidate keeps track of transitive reverse dependencies.
// If an ID is present in the map, invalidate its types.
// If an ID's value is true, invalidate its metadata too.