blob: 861d5d7bd600df460b222301c0808d7bf3fab0a6 [file] [log] [blame]
// 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);
}
}
}
}