// 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/internal/lsp/source" "golang.org/x/tools/internal/span" ) // A metadataGraph holds information about a transtively closed import graph of // Go packages, as obtained from go/packages. // // Currently a new metadata graph is created for each snapshot. // TODO(rfindley): make this type immutable, so that it may be shared across // snapshots. type metadataGraph struct { // metadata maps package IDs to their associated metadata. metadata map[PackageID]*KnownMetadata // importedBy maps package IDs to the list of packages that import them. importedBy map[PackageID][]PackageID // ids maps file URIs to package IDs. A single file may belong to multiple // packages due to tests packages. ids map[span.URI][]PackageID } // Clone creates a new metadataGraph, applying the given updates to the // receiver. func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { if len(updates) == 0 { // Optimization: since the graph is immutable, we can return the receiver. return g } result := &metadataGraph{metadata: make(map[PackageID]*KnownMetadata, len(g.metadata))} // Copy metadata. for id, m := range g.metadata { result.metadata[id] = m } for id, m := range updates { if m == nil { delete(result.metadata, id) } else { result.metadata[id] = m } } result.build() return result } // build constructs g.importedBy and g.uris from g.metadata. func (g *metadataGraph) build() { // Build the import graph. g.importedBy = make(map[PackageID][]PackageID) for id, m := range g.metadata { for _, importID := range m.Deps { g.importedBy[importID] = append(g.importedBy[importID], id) } } // Collect file associations. g.ids = make(map[span.URI][]PackageID) for id, m := range g.metadata { uris := map[span.URI]struct{}{} for _, uri := range m.CompiledGoFiles { uris[uri] = struct{}{} } for _, uri := range m.GoFiles { uris[uri] = struct{}{} } for uri := range uris { g.ids[uri] = append(g.ids[uri], id) } } // Sort and filter file associations. // // We choose the first non-empty set of package associations out of the // following. For simplicity, call a non-command-line-arguments package a // "real" package. // // 1: valid real packages // 2: a valid command-line-arguments package // 3: invalid real packages // 4: an invalid command-line-arguments package for uri, ids := range g.ids { sort.Slice(ids, func(i, j int) bool { // Sort valid packages first. validi := g.metadata[ids[i]].Valid validj := g.metadata[ids[j]].Valid if validi != validj { return validi } cli := source.IsCommandLineArguments(string(ids[i])) clj := source.IsCommandLineArguments(string(ids[j])) if cli && !clj { return false } if !cli && clj { return true } 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. // - Else, keep using all the invalid metadata. // // TODO(rfindley): it might be better to track all IDs here, and exclude // them later in PackagesForFile, but this is the existing behavior. hasValidMetadata := false for i, id := range ids { m := g.metadata[id] if m.Valid { hasValidMetadata = true } else if hasValidMetadata { g.ids[uri] = ids[:i] break } // 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 && source.IsCommandLineArguments(string(id)) { g.ids[uri] = ids[:i] break } } } } // 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 }