blob: 2fcb9fa4ffc2394343ba6eda7b0f10d726da4887 [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.commands;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.util.stream.Collectors.toCollection;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
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.ModuleKey;
import com.google.devtools.build.lib.bazel.commands.ModqueryExecutor.ResultNode.IsExpanded;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayDeque;
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.Set;
import javax.annotation.Nullable;
/**
* Executes inspection queries for {@link
* com.google.devtools.build.lib.bazel.commands.ModqueryCommand}.
*/
public class ModqueryExecutor {
private final ImmutableMap<ModuleKey, AugmentedModule> depGraph;
private final ModqueryOptions options;
private final PrintWriter printer;
public ModqueryExecutor(
ImmutableMap<ModuleKey, AugmentedModule> depGraph, ModqueryOptions options, Writer writer) {
this.depGraph = depGraph;
this.options = options;
this.printer = new PrintWriter(writer);
}
public void tree(ImmutableSet<ModuleKey> targets) {
ImmutableMap<ModuleKey, ResultNode> result = expandAndPrune(targets, ImmutableSet.of(), false);
printer.println(result);
printer.println("OUTPUT NOT IMPLEMENTED YET");
}
public void path(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) {
ImmutableMap<ModuleKey, ResultNode> result = expandAndPrune(from, to, true);
printer.println(result);
printer.println("OUTPUT NOT IMPLEMENTED YET");
}
public void allPaths(ImmutableSet<ModuleKey> from, ImmutableSet<ModuleKey> to) {
ImmutableMap<ModuleKey, ResultNode> result = expandAndPrune(from, to, false);
printer.println(result);
printer.println("OUTPUT NOT IMPLEMENTED YET");
}
public void show(ImmutableMap<ModuleKey, BzlmodRepoRuleValue> repoRuleValues) {
printer.println("OUTPUT NOT IMPLEMENTED YET");
}
/**
* 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, ImmutableSet<ModuleKey> to, boolean singlePath) {
final MaybeCompleteSet<ModuleKey> coloredPaths = colorReversePathsToRoot(to);
ImmutableMap.Builder<ModuleKey, ResultNode> resultBuilder = new ImmutableMap.Builder<>();
ImmutableSet<ModuleKey> rootDirectChildren =
depGraph.get(ModuleKey.ROOT).getAllDeps(options.includeUnused).keySet();
ImmutableSet<ModuleKey> rootPinnedChildren =
getPinnedChildrenOfRootInTheResultGraph(rootDirectChildren, from);
ResultNode.Builder rootBuilder = ResultNode.builder();
rootPinnedChildren.stream()
.filter(coloredPaths::contains)
.forEach(
moduleKey -> {
if (rootDirectChildren.contains(moduleKey)) {
rootBuilder.addChild(moduleKey, IsExpanded.TRUE);
} else {
rootBuilder.addIndirectChild(moduleKey, IsExpanded.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(to.contains(key));
ImmutableSet<ModuleKey> moduleDeps = module.getAllDeps(options.includeUnused).keySet();
for (ModuleKey childKey : moduleDeps) {
if (!coloredPaths.contains(childKey)) {
continue;
}
if (to.contains(childKey)) {
nodeBuilder.setTargetParent(true);
}
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);
}
continue;
}
nodeBuilder.addChild(childKey, IsExpanded.TRUE);
seen.add(childKey);
toVisit.add(childKey);
if (singlePath) {
break;
}
}
resultBuilder.put(key, nodeBuilder.build());
}
return new ResultGraphPruner(!to.isEmpty(), 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 boolean withTargets;
/**
* Prunes the result tree after the specified depth using DFS (because some nodes may still
* appear after the max depth). <br>
*
* @param withTargets If set, it means that the result tree 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(boolean withTargets, Map<ModuleKey, ResultNode> oldResult) {
this.oldResult = oldResult;
this.resultBuilder = new HashMap<>();
this.parentStack = new HashSet<>();
this.withTargets = withTargets;
}
private ImmutableMap<ModuleKey, ResultNode> pruneByDepth() {
ResultNode.Builder rootBuilder = ResultNode.builder();
resultBuilder.put(ModuleKey.ROOT, rootBuilder);
parentStack.add(ModuleKey.ROOT);
for (ModuleKey childKey : oldResult.get(ModuleKey.ROOT).getChildren().keySet()) {
rootBuilder.addChild(childKey, IsExpanded.TRUE);
visitVisible(childKey, 1, ModuleKey.ROOT, IsExpanded.TRUE);
}
for (ModuleKey childKey : oldResult.get(ModuleKey.ROOT).getIndirectChildren().keySet()) {
rootBuilder.addIndirectChild(childKey, IsExpanded.TRUE);
visitVisible(childKey, 1, ModuleKey.ROOT, IsExpanded.TRUE);
}
// Build everything at the end to allow children to add themselves to their parent's
// adjacency list.
return resultBuilder.entrySet().stream()
.collect(toImmutableMap(Entry::getKey, e -> e.getValue().build()));
}
// 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 = ResultNode.builder();
resultBuilder.put(moduleKey, nodeBuilder);
nodeBuilder.setTarget(oldNode.isTarget());
if (depth > 1) {
resultBuilder.get(parentKey).addChild(moduleKey, expanded);
}
if (expanded == IsExpanded.FALSE) {
parentStack.remove(moduleKey);
return;
}
for (Entry<ModuleKey, IsExpanded> e : oldNode.getChildren().entrySet()) {
ModuleKey childKey = e.getKey();
IsExpanded childExpanded = e.getValue();
if (notCycle(childKey)) {
if (depth < options.depth) {
visitVisible(childKey, depth + 1, moduleKey, childExpanded);
} else if (withTargets) {
visitDetached(childKey, moduleKey, moduleKey, childExpanded);
}
} else if (options.cycles) {
nodeBuilder.addChild(childKey, IsExpanded.FALSE);
}
}
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() || oldNode.isTargetParent()) {
ResultNode.Builder parentBuilder = resultBuilder.get(lastVisibleParentKey);
if (lastVisibleParentKey.equals(parentKey)) {
parentBuilder.addChild(moduleKey, expanded);
} else {
parentBuilder.addIndirectChild(moduleKey, expanded);
}
resultBuilder.put(moduleKey, nodeBuilder);
lastVisibleParentKey = moduleKey;
}
if (expanded == IsExpanded.FALSE) {
parentStack.remove(moduleKey);
return;
}
for (Entry<ModuleKey, IsExpanded> e : oldNode.getChildren().entrySet()) {
ModuleKey childKey = e.getKey();
IsExpanded childExpanded = e.getValue();
if (notCycle(childKey)) {
visitDetached(childKey, moduleKey, lastVisibleParentKey, childExpanded);
} else if (options.cycles) {
nodeBuilder.addChild(childKey, IsExpanded.FALSE);
}
}
parentStack.remove(moduleKey);
}
private boolean notCycle(ModuleKey key) {
return !parentStack.contains(key);
}
}
/**
* Return a 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 ImmutableSet<ModuleKey> getPinnedChildrenOfRootInTheResultGraph(
ImmutableSet<ModuleKey> rootDirectDeps, ImmutableSet<ModuleKey> fromTargets) {
Set<ModuleKey> targetKeys =
fromTargets.stream()
.filter(k -> filterUnused(k, options.includeUnused, true, depGraph))
.collect(toCollection(HashSet::new));
if (fromTargets.contains(ModuleKey.ROOT)) {
targetKeys.addAll(rootDirectDeps);
}
return ImmutableSet.copyOf(targetKeys);
}
/**
* 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(ImmutableSet<ModuleKey> targets) {
if (targets.isEmpty()) {
return MaybeCompleteSet.completeSet();
}
Set<ModuleKey> seen = new HashSet<>(targets);
Deque<ModuleKey> toVisit = new ArrayDeque<>(targets);
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 (seen.contains(parent)) {
continue;
}
seen.add(parent);
toVisit.add(parent);
}
}
return MaybeCompleteSet.copyOf(seen);
}
// Helper to filter unused (and unloaded) specified target that cannot be explained and print a
// warning of that.
static boolean filterUnused(
ModuleKey key,
boolean includeUnused,
boolean allowNotLoaded,
ImmutableMap<ModuleKey, AugmentedModule> dependenciesGraph) {
AugmentedModule module = dependenciesGraph.get(key);
if (key.equals(ModuleKey.ROOT)) {
return false;
}
if (!module.isUsed() && !includeUnused) {
return false;
}
if (!module.isLoaded()) {
return allowNotLoaded;
}
return true;
}
@AutoValue
abstract static class ResultNode {
/** Whether the module is one of the targets in a paths query. */
abstract boolean isTarget();
/**
* Whether the module is a parent of one of the targets in a paths query (can also be a target).
*/
abstract boolean isTargetParent();
enum IsExpanded {
TRUE,
FALSE
}
/** List of direct children. True if the children will be expanded in this branch. */
abstract ImmutableSortedMap<ModuleKey, IsExpanded> getChildren();
/** List of indirect children. True if the children will be expanded in this branch. */
abstract ImmutableSortedMap<ModuleKey, IsExpanded> getIndirectChildren();
static ResultNode.Builder builder() {
return new AutoValue_ModqueryExecutor_ResultNode.Builder()
.setTarget(false)
.setTargetParent(false);
}
@AutoValue.Builder
abstract static class Builder {
abstract ResultNode.Builder setTargetParent(boolean value);
private final ImmutableSortedMap.Builder<ModuleKey, IsExpanded> childrenBuilder =
childrenBuilder(ModuleKey.LEXICOGRAPHIC_COMPARATOR);
private final ImmutableSortedMap.Builder<ModuleKey, IsExpanded> indirectChildrenBuilder =
indirectChildrenBuilder(ModuleKey.LEXICOGRAPHIC_COMPARATOR);
abstract ResultNode.Builder setTarget(boolean value);
abstract ImmutableSortedMap.Builder<ModuleKey, IsExpanded> childrenBuilder(
Comparator<ModuleKey> comparator);
abstract ImmutableSortedMap.Builder<ModuleKey, IsExpanded> indirectChildrenBuilder(
Comparator<ModuleKey> comparator);
@CanIgnoreReturnValue
final Builder addChild(ModuleKey value, IsExpanded expanded) {
childrenBuilder.put(value, expanded);
return this;
}
@CanIgnoreReturnValue
final Builder addIndirectChild(ModuleKey value, IsExpanded expanded) {
indirectChildrenBuilder.put(value, expanded);
return this;
}
abstract ResultNode build();
}
}
/**
* A set that either contains some elements or is the <i>complete</i> set (has a special
* constructor). <br>
* A complete set is stored internally as {@code null}. However, passing null to {@link
* #copyOf(Set)} is not allowed. Use {@link #completeSet()} instead.
*/
@AutoValue
abstract static class MaybeCompleteSet<T> {
@Nullable
protected abstract ImmutableSet<T> internalSet();
boolean contains(T value) {
if (internalSet() == null) {
return true;
}
return internalSet().contains(value);
}
static <T> MaybeCompleteSet<T> copyOf(Set<T> nullableSet) {
Preconditions.checkArgument(nullableSet != null);
return new AutoValue_ModqueryExecutor_MaybeCompleteSet<T>(ImmutableSet.copyOf(nullableSet));
}
static <T> MaybeCompleteSet<T> completeSet() {
return new AutoValue_ModqueryExecutor_MaybeCompleteSet<>(null);
}
}
}