| // Copyright 2022 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.bazel.bzlmod.modcommand; |
| |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap; |
| import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; |
| import static java.util.Comparator.reverseOrder; |
| import static java.util.stream.Collectors.joining; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSetMultimap; |
| import com.google.common.collect.ImmutableSortedSet; |
| import com.google.common.collect.ImmutableTable; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule; |
| import com.google.devtools.build.lib.bazel.bzlmod.BzlmodRepoRuleValue; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionId; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionUsage; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey; |
| import com.google.devtools.build.lib.bazel.bzlmod.Tag; |
| import com.google.devtools.build.lib.bazel.bzlmod.Version; |
| import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsExpanded; |
| import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsIndirect; |
| import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.NodeMetadata; |
| import com.google.devtools.build.lib.packages.LabelPrinter; |
| import com.google.devtools.build.lib.packages.RawAttributeMapper; |
| import com.google.devtools.build.lib.packages.Rule; |
| import com.google.devtools.build.lib.query2.query.output.BuildOutputFormatter.AttributeReader; |
| import com.google.devtools.build.lib.query2.query.output.BuildOutputFormatter.TargetOutputter; |
| import com.google.devtools.build.lib.query2.query.output.PossibleAttributeValues; |
| import com.google.devtools.build.lib.util.MaybeCompleteSet; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.Writer; |
| import java.util.ArrayDeque; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Deque; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| import net.starlark.java.eval.Starlark; |
| |
| /** |
| * Executes inspection queries for {@link com.google.devtools.build.lib.bazel.commands.ModCommand} |
| * and prints the resulted output to the reporter's output stream using the different defined {@link |
| * OutputFormatters}. |
| */ |
| public class ModExecutor { |
| |
| private final ImmutableMap<ModuleKey, AugmentedModule> depGraph; |
| private final ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsages; |
| private final ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos; |
| private final Optional<MaybeCompleteSet<ModuleExtensionId>> extensionFilter; |
| private final ModOptions options; |
| private final PrintWriter printer; |
| private ImmutableMap<ModuleExtensionId, ImmutableSetMultimap<String, ModuleKey>> |
| extensionRepoImports; |
| |
| public ModExecutor( |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, ModOptions options, Writer writer) { |
| this( |
| depGraph, |
| ImmutableTable.of(), |
| ImmutableSetMultimap.of(), |
| Optional.of(MaybeCompleteSet.completeSet()), |
| options, |
| writer); |
| } |
| |
| public ModExecutor( |
| ImmutableMap<ModuleKey, AugmentedModule> depGraph, |
| ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsages, |
| ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos, |
| Optional<MaybeCompleteSet<ModuleExtensionId>> extensionFilter, |
| ModOptions options, |
| Writer writer) { |
| this.depGraph = depGraph; |
| this.extensionUsages = extensionUsages; |
| this.extensionRepos = extensionRepos; |
| this.extensionFilter = extensionFilter; |
| this.options = options; |
| this.printer = new PrintWriter(writer); |
| // Easier lookup table for repo imports by module. |
| // It is updated after pruneByDepthAndLink to filter out pruned modules. |
| this.extensionRepoImports = computeRepoImportsTable(depGraph.keySet()); |
| } |
| |
| public void graph(ImmutableSet<ModuleKey> from) { |
| ImmutableMap<ModuleKey, ResultNode> result = |
| expandAndPrune(from, computeExtensionFilterTargets(), false); |
| OutputFormatters.getFormatter(options.outputFormat) |
| .output(result, depGraph, extensionRepos, extensionRepoImports, printer, options); |
| } |
| |
| public void path(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) { |
| MaybeCompleteSet<ModuleKey> targets = |
| MaybeCompleteSet.unionElements(computeExtensionFilterTargets(), to); |
| ImmutableMap<ModuleKey, ResultNode> result = expandAndPrune(from, targets, true); |
| OutputFormatters.getFormatter(options.outputFormat) |
| .output(result, depGraph, extensionRepos, extensionRepoImports, printer, options); |
| } |
| |
| public void allPaths(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) { |
| MaybeCompleteSet<ModuleKey> targets = |
| MaybeCompleteSet.unionElements(computeExtensionFilterTargets(), to); |
| ImmutableMap<ModuleKey, ResultNode> result = expandAndPrune(from, targets, false); |
| OutputFormatters.getFormatter(options.outputFormat) |
| .output(result, depGraph, extensionRepos, extensionRepoImports, printer, options); |
| } |
| |
| public void showRepo(ImmutableMap<String, BzlmodRepoRuleValue> targetRepoRuleValues) { |
| RuleDisplayOutputter outputter = new RuleDisplayOutputter(printer); |
| for (Entry<String, BzlmodRepoRuleValue> e : targetRepoRuleValues.entrySet()) { |
| printer.printf("## %s:\n", e.getKey()); |
| outputter.outputRule(e.getValue().getRule()); |
| } |
| printer.flush(); |
| } |
| |
| public void showExtension( |
| ImmutableSet<ModuleExtensionId> extensions, ImmutableSet<ModuleKey> fromUsages) { |
| for (ModuleExtensionId extension : extensions) { |
| displayExtension(extension, fromUsages); |
| } |
| printer.flush(); |
| } |
| |
| /** |
| * The core function which produces the {@link ResultNode} graph for all the graph-generating |
| * queries above. First, it expands the result graph starting from the {@code from} modules, up |
| * until the {@code to} target modules if they are specified. If {@code singlePath} is set, it |
| * will only contain a single path to one of the targets. <br> |
| * Then it calls {@link ResultGraphPruner#pruneByDepth()} to prune nodes after the specified |
| * {@code depth} (root is at depth 0). If the query specifies any {@code to} targets, even if they |
| * are below the specified depth, they will still be included in the graph using some indirect |
| * (dotted) edges. If {@code from} nodes other than the root are specified, they will be pinned |
| * (connected directly under the root - using indirect edges if necessary). |
| */ |
| @VisibleForTesting |
| ImmutableMap<ModuleKey, ResultNode> expandAndPrune( |
| ImmutableSet<ModuleKey> from, MaybeCompleteSet<ModuleKey> targets, boolean singlePath) { |
| final MaybeCompleteSet<ModuleKey> coloredPaths = colorReversePathsToRoot(targets); |
| ImmutableMap.Builder<ModuleKey, ResultNode> resultBuilder = new ImmutableMap.Builder<>(); |
| ResultNode.Builder rootBuilder = ResultNode.builder(); |
| |
| ImmutableSet<ModuleKey> rootDirectChildren = |
| depGraph.get(ModuleKey.ROOT).getAllDeps(options.includeUnused).keySet(); |
| ImmutableSet<ModuleKey> rootPinnedChildren = |
| getPinnedChildrenOfRootInTheResultGraph(rootDirectChildren, from).stream() |
| .filter(coloredPaths::contains) |
| .filter(this::filterBuiltin) |
| .collect(toImmutableSortedSet(ModuleKey.LEXICOGRAPHIC_COMPARATOR)); |
| rootPinnedChildren.forEach( |
| moduleKey -> |
| rootBuilder.addChild( |
| moduleKey, |
| IsExpanded.TRUE, |
| rootDirectChildren.contains(moduleKey) ? IsIndirect.FALSE : IsIndirect.TRUE)); |
| resultBuilder.put(ModuleKey.ROOT, rootBuilder.build()); |
| |
| Set<ModuleKey> seen = new HashSet<>(rootPinnedChildren); |
| Deque<ModuleKey> toVisit = new ArrayDeque<>(rootPinnedChildren); |
| seen.add(ModuleKey.ROOT); |
| |
| while (!toVisit.isEmpty()) { |
| ModuleKey key = toVisit.pop(); |
| AugmentedModule module = depGraph.get(key); |
| ResultNode.Builder nodeBuilder = ResultNode.builder(); |
| nodeBuilder.setTarget(!targets.isComplete() && targets.contains(key)); |
| |
| ImmutableSortedSet<ModuleKey> moduleDeps = module.getAllDeps(options.includeUnused).keySet(); |
| for (ModuleKey childKey : moduleDeps) { |
| if (!coloredPaths.contains(childKey)) { |
| continue; |
| } |
| if (isBuiltin(childKey) && !options.includeBuiltin) { |
| continue; |
| } |
| if (seen.contains(childKey)) { |
| // Single paths should not contain cycles or unexpanded (duplicate) children |
| // TODO(andreisolo): Move the single path extraction to DFS otherwise it can produce a |
| // wrong answer in cycle edge-case A -> B -> C -> B with target D will not find ABD |
| // \__ D |
| if (!singlePath) { |
| nodeBuilder.addChild(childKey, IsExpanded.FALSE, IsIndirect.FALSE); |
| } |
| continue; |
| } |
| nodeBuilder.addChild(childKey, IsExpanded.TRUE, IsIndirect.FALSE); |
| seen.add(childKey); |
| toVisit.add(childKey); |
| if (singlePath) { |
| break; |
| } |
| } |
| |
| resultBuilder.put(key, nodeBuilder.build()); |
| } |
| return new ResultGraphPruner(targets, resultBuilder.buildOrThrow()).pruneByDepth(); |
| } |
| |
| private class ResultGraphPruner { |
| |
| private final Map<ModuleKey, ResultNode> oldResult; |
| private final Map<ModuleKey, ResultNode.Builder> resultBuilder; |
| private final Set<ModuleKey> parentStack; |
| private final MaybeCompleteSet<ModuleKey> targets; |
| |
| /** |
| * Constructs a ResultGraphPruner to prune the result graph after the specified depth. |
| * |
| * @param targets If not complete, it means that the result graph contains paths to some |
| * specific targets. This will cause some branches to contain, after the specified depths, |
| * some targets or target parents. As any other nodes omitted, transitive edges (embedding |
| * multiple edges) will be stored as <i>indirect</i>. |
| * @param oldResult The unpruned result graph. |
| */ |
| ResultGraphPruner(MaybeCompleteSet<ModuleKey> targets, Map<ModuleKey, ResultNode> oldResult) { |
| this.oldResult = oldResult; |
| this.resultBuilder = new HashMap<>(); |
| this.parentStack = new HashSet<>(); |
| this.targets = targets; |
| } |
| |
| /** |
| * Prunes the result tree after the specified depth using DFS (because some nodes may still |
| * appear after the max depth). |
| */ |
| private ImmutableMap<ModuleKey, ResultNode> pruneByDepth() { |
| ResultNode.Builder rootBuilder = ResultNode.builder(); |
| resultBuilder.put(ModuleKey.ROOT, rootBuilder); |
| |
| parentStack.add(ModuleKey.ROOT); |
| |
| for (Entry<ModuleKey, NodeMetadata> e : |
| oldResult.get(ModuleKey.ROOT).getChildrenSortedByKey()) { |
| rootBuilder.addChild(e.getKey(), IsExpanded.TRUE, e.getValue().isIndirect()); |
| visitVisible(e.getKey(), 1, ModuleKey.ROOT, IsExpanded.TRUE); |
| } |
| |
| // Build everything at the end to allow children to add themselves to their parent's |
| // adjacency list. |
| ImmutableMap<ModuleKey, ResultNode> result = |
| resultBuilder.entrySet().stream() |
| .collect( |
| toImmutableSortedMap( |
| ModuleKey.LEXICOGRAPHIC_COMPARATOR, |
| Entry::getKey, |
| e -> e.getValue().build())); |
| // Filter imports for nodes that were pruned during this process. |
| extensionRepoImports = computeRepoImportsTable(result.keySet()); |
| return result; |
| } |
| |
| // Handles graph traversal within the specified depth. |
| private void visitVisible( |
| ModuleKey moduleKey, int depth, ModuleKey parentKey, IsExpanded expanded) { |
| parentStack.add(moduleKey); |
| ResultNode oldNode = oldResult.get(moduleKey); |
| ResultNode.Builder nodeBuilder = |
| resultBuilder.computeIfAbsent(moduleKey, k -> ResultNode.builder()); |
| |
| nodeBuilder.setTarget(oldNode.isTarget()); |
| if (depth > 1) { |
| resultBuilder.get(parentKey).addChild(moduleKey, expanded, IsIndirect.FALSE); |
| } |
| |
| if (expanded == IsExpanded.FALSE) { |
| parentStack.remove(moduleKey); |
| return; |
| } |
| for (Entry<ModuleKey, NodeMetadata> e : oldNode.getChildrenSortedByKey()) { |
| ModuleKey childKey = e.getKey(); |
| IsExpanded childExpanded = e.getValue().isExpanded(); |
| if (notCycle(childKey)) { |
| if (depth < options.depth) { |
| visitVisible(childKey, depth + 1, moduleKey, childExpanded); |
| } else if (!targets.isComplete()) { |
| visitDetached(childKey, moduleKey, moduleKey, childExpanded); |
| } |
| } else if (options.cycles) { |
| nodeBuilder.addCycle(childKey); |
| } |
| } |
| parentStack.remove(moduleKey); |
| } |
| |
| // Detached mode is only present in withTargets and handles adding targets and target parents |
| // living below the specified depth to the graph. |
| private void visitDetached( |
| ModuleKey moduleKey, |
| ModuleKey parentKey, |
| ModuleKey lastVisibleParentKey, |
| IsExpanded expanded) { |
| parentStack.add(moduleKey); |
| ResultNode oldNode = oldResult.get(moduleKey); |
| ResultNode.Builder nodeBuilder = ResultNode.builder(); |
| nodeBuilder.setTarget(oldNode.isTarget()); |
| |
| if (oldNode.isTarget() || isTargetParent(oldNode)) { |
| ResultNode.Builder parentBuilder = resultBuilder.get(lastVisibleParentKey); |
| IsIndirect childIndirect = |
| lastVisibleParentKey.equals(parentKey) ? IsIndirect.FALSE : IsIndirect.TRUE; |
| parentBuilder.addChild(moduleKey, expanded, childIndirect); |
| resultBuilder.put(moduleKey, nodeBuilder); |
| lastVisibleParentKey = moduleKey; |
| } |
| |
| if (expanded == IsExpanded.FALSE) { |
| parentStack.remove(moduleKey); |
| return; |
| } |
| for (Entry<ModuleKey, NodeMetadata> e : oldNode.getChildrenSortedByKey()) { |
| ModuleKey childKey = e.getKey(); |
| IsExpanded childExpanded = e.getValue().isExpanded(); |
| if (notCycle(childKey)) { |
| visitDetached(childKey, moduleKey, lastVisibleParentKey, childExpanded); |
| } else if (options.cycles) { |
| nodeBuilder.addCycle(childKey); |
| } |
| } |
| parentStack.remove(moduleKey); |
| } |
| |
| private boolean notCycle(ModuleKey key) { |
| return !parentStack.contains(key); |
| } |
| |
| private boolean isTargetParent(ResultNode node) { |
| return node.getChildren().keys().stream() |
| .filter(Predicate.not(parentStack::contains)) |
| .anyMatch(targets::contains); |
| } |
| } |
| |
| /** |
| * Return a sorted list of modules that will be the direct children of the root in the result |
| * graph (original root's direct dependencies along with the specified targets). |
| */ |
| private ImmutableSortedSet<ModuleKey> getPinnedChildrenOfRootInTheResultGraph( |
| ImmutableSet<ModuleKey> rootDirectDeps, ImmutableSet<ModuleKey> fromTargets) { |
| Set<ModuleKey> targetKeys = new HashSet<>(fromTargets); |
| if (fromTargets.contains(ModuleKey.ROOT)) { |
| targetKeys.remove(ModuleKey.ROOT); |
| targetKeys.addAll(rootDirectDeps); |
| } |
| return ImmutableSortedSet.copyOf(ModuleKey.LEXICOGRAPHIC_COMPARATOR, targetKeys); |
| } |
| |
| private static boolean intersect( |
| MaybeCompleteSet<ModuleExtensionId> a, Set<ModuleExtensionId> b) { |
| if (a.isComplete()) { |
| return !b.isEmpty(); |
| } |
| return !Collections.disjoint(a.getElementsIfNotComplete(), b); |
| } |
| |
| /** |
| * If the extensionFilter option is set, computes the set of target modules that use the specified |
| * extension(s) and adds them to the list of specified targets if the query is a path(s) query. |
| */ |
| private MaybeCompleteSet<ModuleKey> computeExtensionFilterTargets() { |
| if (extensionFilter.isEmpty()) { |
| // If no --extension_filter is set, don't do anything here. |
| return MaybeCompleteSet.completeSet(); |
| } |
| return MaybeCompleteSet.copyOf( |
| depGraph.keySet().stream() |
| .filter(this::filterUnused) |
| .filter(this::filterBuiltin) |
| .filter(k -> intersect(extensionFilter.get(), extensionUsages.column(k).keySet())) |
| .collect(toImmutableSet())); |
| } |
| |
| /** |
| * Color all reverse paths from the target modules to the root so only modules which are part of |
| * these paths will be included in the output graph during the breadth-first traversal. |
| */ |
| private MaybeCompleteSet<ModuleKey> colorReversePathsToRoot(MaybeCompleteSet<ModuleKey> to) { |
| if (to.isComplete()) { |
| return MaybeCompleteSet.completeSet(); |
| } |
| |
| Set<ModuleKey> seen = new HashSet<>(to.getElementsIfNotComplete()); |
| Deque<ModuleKey> toVisit = new ArrayDeque<>(to.getElementsIfNotComplete()); |
| |
| while (!toVisit.isEmpty()) { |
| ModuleKey key = toVisit.pop(); |
| AugmentedModule module = depGraph.get(key); |
| Set<ModuleKey> parents = new HashSet<>(module.getDependants()); |
| if (options.includeUnused) { |
| parents.addAll(module.getOriginalDependants()); |
| } |
| for (ModuleKey parent : parents) { |
| if (isBuiltin(parent) && !options.includeBuiltin) { |
| continue; |
| } |
| if (seen.contains(parent)) { |
| continue; |
| } |
| seen.add(parent); |
| toVisit.add(parent); |
| } |
| } |
| |
| return MaybeCompleteSet.copyOf(seen); |
| } |
| |
| /** Compute the multimap of repo imports to modules for each extension. */ |
| private ImmutableMap<ModuleExtensionId, ImmutableSetMultimap<String, ModuleKey>> |
| computeRepoImportsTable(ImmutableSet<ModuleKey> presentModules) { |
| ImmutableMap.Builder<ModuleExtensionId, ImmutableSetMultimap<String, ModuleKey>> resultBuilder = |
| new ImmutableMap.Builder<>(); |
| for (ModuleExtensionId extension : extensionUsages.rowKeySet()) { |
| if (extensionFilter.isPresent() && !extensionFilter.get().contains(extension)) { |
| continue; |
| } |
| ImmutableSetMultimap.Builder<ModuleKey, String> modulesToImportsBuilder = |
| new ImmutableSetMultimap.Builder<>(); |
| for (Entry<ModuleKey, ModuleExtensionUsage> usage : |
| extensionUsages.rowMap().get(extension).entrySet()) { |
| if (!presentModules.contains(usage.getKey())) { |
| continue; |
| } |
| modulesToImportsBuilder.putAll(usage.getKey(), usage.getValue().getImports().values()); |
| } |
| resultBuilder.put(extension, modulesToImportsBuilder.build().inverse()); |
| } |
| return resultBuilder.buildOrThrow(); |
| } |
| |
| private boolean filterUnused(ModuleKey key) { |
| AugmentedModule module = depGraph.get(key); |
| return options.includeUnused || module.isUsed(); |
| } |
| |
| private boolean filterBuiltin(ModuleKey key) { |
| return options.includeBuiltin || !isBuiltin(key); |
| } |
| |
| /** Helper to display show_extension info. */ |
| private void displayExtension(ModuleExtensionId extension, ImmutableSet<ModuleKey> fromUsages) { |
| printer.printf("## %s:\n", extension.asTargetString()); |
| printer.println(); |
| printer.println("Fetched repositories:"); |
| // TODO(wyv): if `extension` doesn't exist, we crash. We should report a good error instead! |
| ImmutableSortedSet<String> usedRepos = |
| ImmutableSortedSet.copyOf(extensionRepoImports.get(extension).keySet()); |
| ImmutableSortedSet<String> unusedRepos = |
| ImmutableSortedSet.copyOf(Sets.difference(extensionRepos.get(extension), usedRepos)); |
| for (String repo : usedRepos) { |
| printer.printf( |
| " - %s (imported by %s)\n", |
| repo, |
| extensionRepoImports.get(extension).get(repo).stream() |
| .sorted(ModuleKey.LEXICOGRAPHIC_COMPARATOR) |
| .map(ModuleKey::toString) |
| .collect(joining(", "))); |
| } |
| for (String repo : unusedRepos) { |
| printer.printf(" - %s\n", repo); |
| } |
| printer.println(); |
| if (fromUsages.isEmpty()) { |
| fromUsages = ImmutableSet.copyOf(extensionUsages.rowMap().get(extension).keySet()); |
| } |
| for (ModuleKey module : fromUsages) { |
| if (!extensionUsages.contains(extension, module)) { |
| continue; |
| } |
| ModuleExtensionUsage usage = extensionUsages.get(extension, module); |
| printer.printf( |
| "## Usage in %s from %s:%s\n", |
| module, usage.getLocation().file(), usage.getLocation().line()); |
| for (Tag tag : usage.getTags()) { |
| printer.printf( |
| "%s.%s(%s)\n", |
| extension.getExtensionName(), |
| tag.getTagName(), |
| tag.getAttributeValues().attributes().entrySet().stream() |
| .map(e -> String.format("%s=%s", e.getKey(), Starlark.repr(e.getValue()))) |
| .collect(joining(", "))); |
| } |
| printer.printf("use_repo(\n"); |
| printer.printf(" %s,\n", extension.getExtensionName()); |
| for (Entry<String, String> repo : usage.getImports().entrySet()) { |
| printer.printf( |
| " %s,\n", |
| repo.getKey().equals(repo.getValue()) |
| ? String.format("\"%s\"", repo.getKey()) |
| : String.format("%s=\"%s\"", repo.getKey(), repo.getValue())); |
| } |
| printer.printf(")\n\n"); |
| } |
| } |
| |
| private boolean isBuiltin(ModuleKey key) { |
| return key.equals(ModuleKey.create("bazel_tools", Version.EMPTY)) |
| || key.equals(ModuleKey.create("local_config_platform", Version.EMPTY)); |
| } |
| |
| /** A node representing a module that forms the result graph. */ |
| @AutoValue |
| public abstract static class ResultNode { |
| |
| /** Whether the module is one of the targets in a paths query. */ |
| abstract boolean isTarget(); |
| |
| enum IsExpanded { |
| FALSE, |
| TRUE |
| } |
| |
| enum IsIndirect { |
| FALSE, |
| TRUE |
| } |
| |
| enum IsCycle { |
| FALSE, |
| TRUE |
| } |
| |
| /** Detailed edge type for the {@link ResultNode} graph. */ |
| @AutoValue |
| public abstract static class NodeMetadata { |
| /** |
| * Whether the node should be expanded from this edge (the same node can appear in multiple |
| * places in a flattened graph). |
| */ |
| public abstract IsExpanded isExpanded(); |
| |
| /** Whether the edge is a direct edge or an indirect (transitive) one. */ |
| public abstract IsIndirect isIndirect(); |
| |
| /** Whether the edge is cycling back inside the flattened graph. */ |
| public abstract IsCycle isCycle(); |
| |
| private static NodeMetadata create( |
| IsExpanded isExpanded, IsIndirect isIndirect, IsCycle isCycle) { |
| return new AutoValue_ModExecutor_ResultNode_NodeMetadata(isExpanded, isIndirect, isCycle); |
| } |
| } |
| |
| /** List of children mapped to detailed edge types. */ |
| protected abstract ImmutableSetMultimap<ModuleKey, NodeMetadata> getChildren(); |
| |
| public ImmutableSortedSet<Entry<ModuleKey, NodeMetadata>> getChildrenSortedByKey() { |
| return ImmutableSortedSet.copyOf( |
| Entry.comparingByKey(ModuleKey.LEXICOGRAPHIC_COMPARATOR), getChildren().entries()); |
| } |
| |
| public ImmutableSortedSet<Entry<ModuleKey, NodeMetadata>> getChildrenSortedByEdgeType() { |
| return ImmutableSortedSet.copyOf( |
| Comparator.<Entry<ModuleKey, NodeMetadata>, IsCycle>comparing( |
| e -> e.getValue().isCycle(), reverseOrder()) |
| .thenComparing(e -> e.getValue().isExpanded()) |
| .thenComparing(e -> e.getValue().isIndirect()) |
| .thenComparing(Entry::getKey, ModuleKey.LEXICOGRAPHIC_COMPARATOR), |
| getChildren().entries()); |
| } |
| |
| static ResultNode.Builder builder() { |
| return new AutoValue_ModExecutor_ResultNode.Builder().setTarget(false); |
| } |
| |
| @AutoValue.Builder |
| abstract static class Builder { |
| |
| abstract ResultNode.Builder setTarget(boolean value); |
| |
| abstract ImmutableSetMultimap.Builder<ModuleKey, NodeMetadata> childrenBuilder(); |
| |
| @CanIgnoreReturnValue |
| final Builder addChild(ModuleKey value, IsExpanded expanded, IsIndirect indirect) { |
| childrenBuilder().put(value, NodeMetadata.create(expanded, indirect, IsCycle.FALSE)); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| final Builder addCycle(ModuleKey value) { |
| childrenBuilder() |
| .put(value, NodeMetadata.create(IsExpanded.FALSE, IsIndirect.FALSE, IsCycle.TRUE)); |
| return this; |
| } |
| |
| abstract ResultNode build(); |
| } |
| } |
| |
| /** |
| * Uses Query's {@link TargetOutputter} to display the generating repo rule and other information. |
| */ |
| static class RuleDisplayOutputter { |
| private static final AttributeReader attrReader = |
| (rule, attr) -> |
| // Query's implementation copied |
| PossibleAttributeValues.forRuleAndAttribute( |
| rule, attr, /* mayTreatMultipleAsNone= */ true); |
| private final TargetOutputter targetOutputter; |
| private final PrintWriter printer; |
| |
| RuleDisplayOutputter(PrintWriter printer) { |
| this.printer = printer; |
| this.targetOutputter = |
| new TargetOutputter( |
| this.printer, |
| (rule, attr) -> RawAttributeMapper.of(rule).isConfigurable(attr.getName()), |
| "\n", |
| LabelPrinter.legacy()); |
| } |
| |
| private void outputRule(Rule rule) { |
| try { |
| targetOutputter.outputRule(rule, attrReader, this.printer); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| } |
| } |