| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "sort" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/gopls/internal/bug" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| ) |
| |
| // A metadataGraph is an immutable and transitively closed import |
| // graph of Go packages, as obtained from go/packages. |
| type metadataGraph struct { |
| // metadata maps package IDs to their associated metadata. |
| metadata map[PackageID]*Metadata |
| |
| // importedBy maps package IDs to the list of packages that import them. |
| importedBy map[PackageID][]PackageID |
| |
| // ids maps file URIs to package IDs, sorted by (!valid, cli, packageID). |
| // A single file may belong to multiple packages due to tests packages. |
| // |
| // Invariant: all IDs present in the ids map exist in the metadata map. |
| ids map[protocol.DocumentURI][]PackageID |
| } |
| |
| // Metadata implements the MetadataSource interface. |
| func (g *metadataGraph) Metadata(id PackageID) *Metadata { |
| return g.metadata[id] |
| } |
| |
| // Clone creates a new metadataGraph, applying the given updates to the |
| // receiver. A nil map value represents a deletion. |
| func (g *metadataGraph) Clone(updates map[PackageID]*Metadata) *metadataGraph { |
| if len(updates) == 0 { |
| // Optimization: since the graph is immutable, we can return the receiver. |
| return g |
| } |
| |
| // Copy metadata map then apply updates. |
| metadata := make(map[PackageID]*Metadata, len(g.metadata)) |
| for id, m := range g.metadata { |
| metadata[id] = m |
| } |
| for id, m := range updates { |
| if m == nil { |
| delete(metadata, id) |
| } else { |
| metadata[id] = m |
| } |
| } |
| |
| // Break import cycles involving updated nodes. |
| breakImportCycles(metadata, updates) |
| |
| return newMetadataGraph(metadata) |
| } |
| |
| // newMetadataGraph returns a new metadataGraph, |
| // deriving relations from the specified metadata. |
| func newMetadataGraph(metadata map[PackageID]*Metadata) *metadataGraph { |
| // Build the import graph. |
| importedBy := make(map[PackageID][]PackageID) |
| for id, m := range metadata { |
| for _, depID := range m.DepsByPkgPath { |
| importedBy[depID] = append(importedBy[depID], id) |
| } |
| } |
| |
| // Collect file associations. |
| uriIDs := make(map[protocol.DocumentURI][]PackageID) |
| for id, m := range metadata { |
| uris := map[protocol.DocumentURI]struct{}{} |
| for _, uri := range m.CompiledGoFiles { |
| uris[uri] = struct{}{} |
| } |
| for _, uri := range m.GoFiles { |
| uris[uri] = struct{}{} |
| } |
| for uri := range uris { |
| uriIDs[uri] = append(uriIDs[uri], id) |
| } |
| } |
| |
| // Sort and filter file associations. |
| for uri, ids := range uriIDs { |
| sort.Slice(ids, func(i, j int) bool { |
| cli := IsCommandLineArguments(ids[i]) |
| clj := IsCommandLineArguments(ids[j]) |
| if cli != clj { |
| return clj |
| } |
| |
| // 2. packages appear in name order. |
| return ids[i] < ids[j] |
| }) |
| |
| // Choose the best IDs for each URI, according to the following rules: |
| // - If there are any valid real packages, choose them. |
| // - Else, choose the first valid command-line-argument package, if it exists. |
| // |
| // TODO(rfindley): it might be better to track all IDs here, and exclude |
| // them later when type checking, but this is the existing behavior. |
| for i, id := range ids { |
| // If we've seen *anything* prior to command-line arguments package, take |
| // it. Note that ids[0] may itself be command-line-arguments. |
| if i > 0 && IsCommandLineArguments(id) { |
| uriIDs[uri] = ids[:i] |
| break |
| } |
| } |
| } |
| |
| return &metadataGraph{ |
| metadata: metadata, |
| importedBy: importedBy, |
| ids: uriIDs, |
| } |
| } |
| |
| // reverseReflexiveTransitiveClosure returns a new mapping containing the |
| // metadata for the specified packages along with any package that |
| // transitively imports one of them, keyed by ID, including all the initial packages. |
| func (g *metadataGraph) reverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageID]*Metadata { |
| seen := make(map[PackageID]*Metadata) |
| var visitAll func([]PackageID) |
| visitAll = func(ids []PackageID) { |
| for _, id := range ids { |
| if seen[id] == nil { |
| if m := g.metadata[id]; m != nil { |
| seen[id] = m |
| visitAll(g.importedBy[id]) |
| } |
| } |
| } |
| } |
| visitAll(ids) |
| return seen |
| } |
| |
| // breakImportCycles breaks import cycles in the metadata by deleting |
| // Deps* edges. It modifies only metadata present in the 'updates' |
| // subset. This function has an internal test. |
| func breakImportCycles(metadata, updates map[PackageID]*Metadata) { |
| // 'go list' should never report a cycle without flagging it |
| // as such, but we're extra cautious since we're combining |
| // information from multiple runs of 'go list'. Also, Bazel |
| // may silently report cycles. |
| cycles := detectImportCycles(metadata, updates) |
| if len(cycles) > 0 { |
| // There were cycles (uncommon). Break them. |
| // |
| // The naive way to break cycles would be to perform a |
| // depth-first traversal and to detect and delete |
| // cycle-forming edges as we encounter them. |
| // However, we're not allowed to modify the existing |
| // Metadata records, so we can only break edges out of |
| // the 'updates' subset. |
| // |
| // Another possibility would be to delete not the |
| // cycle forming edge but the topmost edge on the |
| // stack whose tail is an updated node. |
| // However, this would require that we retroactively |
| // undo all the effects of the traversals that |
| // occurred since that edge was pushed on the stack. |
| // |
| // We use a simpler scheme: we compute the set of cycles. |
| // All cyclic paths necessarily involve at least one |
| // updated node, so it is sufficient to break all |
| // edges from each updated node to other members of |
| // the strong component. |
| // |
| // This may result in the deletion of dominating |
| // edges, causing some dependencies to appear |
| // spuriously unreachable. Consider A <-> B -> C |
| // where updates={A,B}. The cycle is {A,B} so the |
| // algorithm will break both A->B and B->A, causing |
| // A to no longer depend on B or C. |
| // |
| // But that's ok: any error in Metadata.Errors is |
| // conservatively assumed by snapshot.clone to be a |
| // potential import cycle error, and causes special |
| // invalidation so that if B later drops its |
| // cycle-forming import of A, both A and B will be |
| // invalidated. |
| for _, cycle := range cycles { |
| cyclic := make(map[PackageID]bool) |
| for _, m := range cycle { |
| cyclic[m.ID] = true |
| } |
| for id := range cyclic { |
| if m := updates[id]; m != nil { |
| for path, depID := range m.DepsByImpPath { |
| if cyclic[depID] { |
| delete(m.DepsByImpPath, path) |
| } |
| } |
| for path, depID := range m.DepsByPkgPath { |
| if cyclic[depID] { |
| delete(m.DepsByPkgPath, path) |
| } |
| } |
| |
| // Set m.Errors to enable special |
| // invalidation logic in snapshot.clone. |
| if len(m.Errors) == 0 { |
| m.Errors = []packages.Error{{ |
| Msg: "detected import cycle", |
| Kind: packages.ListError, |
| }} |
| } |
| } |
| } |
| } |
| |
| // double-check when debugging |
| if false { |
| if cycles := detectImportCycles(metadata, updates); len(cycles) > 0 { |
| bug.Reportf("unbroken cycle: %v", cycles) |
| } |
| } |
| } |
| } |
| |
| // detectImportCycles reports cycles in the metadata graph. It returns a new |
| // unordered array of all cycles (nontrivial strong components) in the |
| // metadata graph reachable from a non-nil 'updates' value. |
| func detectImportCycles(metadata, updates map[PackageID]*Metadata) [][]*Metadata { |
| // We use the depth-first algorithm of Tarjan. |
| // https://doi.org/10.1137/0201010 |
| // |
| // TODO(adonovan): when we can use generics, consider factoring |
| // in common with the other implementation of Tarjan (in typerefs), |
| // abstracting over the node and edge representation. |
| |
| // A node wraps a Metadata with its working state. |
| // (Unfortunately we can't intrude on shared Metadata.) |
| type node struct { |
| rep *node |
| m *Metadata |
| index, lowlink int32 |
| scc int8 // TODO(adonovan): opt: cram these 1.5 bits into previous word |
| } |
| nodes := make(map[PackageID]*node, len(metadata)) |
| nodeOf := func(id PackageID) *node { |
| n, ok := nodes[id] |
| if !ok { |
| m := metadata[id] |
| if m == nil { |
| // Dangling import edge. |
| // Not sure whether a go/packages driver ever |
| // emits this, but create a dummy node in case. |
| // Obviously it won't be part of any cycle. |
| m = &Metadata{ID: id} |
| } |
| n = &node{m: m} |
| n.rep = n |
| nodes[id] = n |
| } |
| return n |
| } |
| |
| // find returns the canonical node decl. |
| // (The nodes form a disjoint set forest.) |
| var find func(*node) *node |
| find = func(n *node) *node { |
| rep := n.rep |
| if rep != n { |
| rep = find(rep) |
| n.rep = rep // simple path compression (no union-by-rank) |
| } |
| return rep |
| } |
| |
| // global state |
| var ( |
| index int32 = 1 |
| stack []*node |
| sccs [][]*Metadata // set of nontrivial strongly connected components |
| ) |
| |
| // visit implements the depth-first search of Tarjan's SCC algorithm |
| // Precondition: x is canonical. |
| var visit func(*node) |
| visit = func(x *node) { |
| x.index = index |
| x.lowlink = index |
| index++ |
| |
| stack = append(stack, x) // push |
| x.scc = -1 |
| |
| for _, yid := range x.m.DepsByPkgPath { |
| y := nodeOf(yid) |
| // Loop invariant: x is canonical. |
| y = find(y) |
| if x == y { |
| continue // nodes already combined (self-edges are impossible) |
| } |
| |
| switch { |
| case y.scc > 0: |
| // y is already a collapsed SCC |
| |
| case y.scc < 0: |
| // y is on the stack, and thus in the current SCC. |
| if y.index < x.lowlink { |
| x.lowlink = y.index |
| } |
| |
| default: |
| // y is unvisited; visit it now. |
| visit(y) |
| // Note: x and y are now non-canonical. |
| x = find(x) |
| if y.lowlink < x.lowlink { |
| x.lowlink = y.lowlink |
| } |
| } |
| } |
| |
| // Is x the root of an SCC? |
| if x.lowlink == x.index { |
| // Gather all metadata in the SCC (if nontrivial). |
| var scc []*Metadata |
| for { |
| // Pop y from stack. |
| i := len(stack) - 1 |
| y := stack[i] |
| stack = stack[:i] |
| if x != y || scc != nil { |
| scc = append(scc, y.m) |
| } |
| if x == y { |
| break // complete |
| } |
| // x becomes y's canonical representative. |
| y.rep = x |
| } |
| if scc != nil { |
| sccs = append(sccs, scc) |
| } |
| x.scc = 1 |
| } |
| } |
| |
| // Visit only the updated nodes: |
| // the existing metadata graph has no cycles, |
| // so any new cycle must involve an updated node. |
| for id, m := range updates { |
| if m != nil { |
| if n := nodeOf(id); n.index == 0 { // unvisited |
| visit(n) |
| } |
| } |
| } |
| |
| return sccs |
| } |