blob: 7413e42ff5886159f5d0fc3a0563d9c35d629fe8 [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.ImmutableSortedSet.toImmutableSortedSet;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionId;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey;
import com.google.devtools.build.lib.bazel.bzlmod.Version;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsCycle;
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.bazel.bzlmod.modcommand.ModOptions.Charset;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModOptions.ExtensionShow;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.OutputFormatters.OutputFormatter;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
/** Outputs graph-based results of {@link ModExecutor} in a human-readable text format. */
public class TextOutputFormatter extends OutputFormatter {
private Deque<Boolean> isLastChildStack;
private DrawCharset drawCharset;
private Set<ModuleExtensionId> seenExtensions;
private StringBuilder str;
@Override
public void output() {
if (options.charset == Charset.ASCII) {
drawCharset = DrawCharset.ASCII;
} else {
drawCharset = DrawCharset.UTF8;
}
isLastChildStack = new ArrayDeque<>();
seenExtensions = new HashSet<>();
str = new StringBuilder();
printModule(ModuleKey.ROOT, null, IsExpanded.TRUE, IsIndirect.FALSE, IsCycle.FALSE, 0);
this.printer.println(str);
}
// Prints the indents and the tree drawing characters.
private void printTreeDrawing(IsIndirect indirect, int depth) {
if (depth > 0) {
int indents = isLastChildStack.size() - 1;
Iterator<Boolean> value = isLastChildStack.descendingIterator();
for (int i = 0; i < indents; i++) {
boolean isLastChild = value.next();
if (isLastChild) {
str.append(drawCharset.emptyIndent);
} else {
str.append(drawCharset.prevChildIndent);
}
}
if (indirect == IsIndirect.TRUE) {
if (isLastChildStack.getFirst()) {
str.append(drawCharset.lastIndirectChildIndent);
} else {
str.append(drawCharset.indirectChildIndent);
}
} else {
if (isLastChildStack.getFirst()) {
str.append(drawCharset.lastChildIndent);
} else {
str.append(drawCharset.childIndent);
}
}
}
}
// Helper to print module extensions similarly to printModule.
private void printExtension(
ModuleKey key, ModuleExtensionId extensionId, boolean unexpanded, int depth) {
printTreeDrawing(IsIndirect.FALSE, depth);
str.append('$');
str.append(extensionId.asTargetString());
str.append(' ');
if (unexpanded && options.extensionInfo == ExtensionShow.ALL) {
str.append("... ");
}
str.append("\n");
if (options.extensionInfo == ExtensionShow.USAGES) {
return;
}
ImmutableSortedSet<String> repoImports =
ImmutableSortedSet.copyOf(extensionRepoImports.get(extensionId).inverse().get(key));
ImmutableSortedSet<String> unusedRepos = ImmutableSortedSet.of();
if (!unexpanded && options.extensionInfo == ExtensionShow.ALL) {
unusedRepos =
ImmutableSortedSet.copyOf(
Sets.difference(
extensionRepos.get(extensionId), extensionRepoImports.get(extensionId).keySet()));
}
int totalChildrenNum = repoImports.size() + unusedRepos.size();
int currChild = 1;
for (String usedRepo : repoImports) {
isLastChildStack.push(currChild++ == totalChildrenNum);
printExtensionRepo(usedRepo, IsIndirect.FALSE, depth + 1);
isLastChildStack.pop();
}
if (unexpanded || options.extensionInfo == ExtensionShow.REPOS) {
return;
}
for (String unusedPackage : unusedRepos) {
isLastChildStack.push(currChild++ == totalChildrenNum);
printExtensionRepo(unusedPackage, IsIndirect.TRUE, depth + 1);
isLastChildStack.pop();
}
}
// Prints an extension repo line.
private void printExtensionRepo(String repoName, IsIndirect indirectLink, int depth) {
printTreeDrawing(indirectLink, depth);
str.append(repoName).append("\n");
}
// Depth-first traversal to print the actual output
private void printModule(
ModuleKey key,
ModuleKey parent,
IsExpanded expanded,
IsIndirect indirect,
IsCycle cycle,
int depth) {
printTreeDrawing(indirect, depth);
ResultNode node = Objects.requireNonNull(result.get(key));
if (key.equals(ModuleKey.ROOT)) {
AugmentedModule rootModule = depGraph.get(ModuleKey.ROOT);
Preconditions.checkNotNull(rootModule);
str.append(
String.format(
"<root> (%s@%s)",
rootModule.getName(),
rootModule.getVersion().equals(Version.EMPTY) ? "_" : rootModule.getVersion()));
} else {
str.append(key).append(" ");
}
int totalChildrenNum = node.getChildren().size();
ImmutableSortedSet<ModuleExtensionId> extensionsUsed =
extensionRepoImports.keySet().stream()
.filter(e -> extensionRepoImports.get(e).inverse().containsKey(key))
.collect(toImmutableSortedSet(ModuleExtensionId.LEXICOGRAPHIC_COMPARATOR));
if (options.extensionInfo != ExtensionShow.HIDDEN) {
totalChildrenNum += extensionsUsed.size();
}
if (cycle == IsCycle.TRUE) {
str.append("(cycle) ");
} else if (expanded == IsExpanded.FALSE) {
str.append("(*) ");
} else {
if (totalChildrenNum != 0 && node.isTarget()) {
str.append("# ");
}
}
AugmentedModule module = Objects.requireNonNull(depGraph.get(key));
if (!options.verbose && !module.isUsed()) {
str.append("(unused) ");
}
// If the edge is indirect, the parent is not only unknown, but the node could have come
// from multiple paths merged in the process, so we skip the resolution explanation.
if (indirect == IsIndirect.FALSE && options.verbose && parent != null) {
Explanation explanation = getExtraResolutionExplanation(key, parent);
if (explanation != null) {
str.append(explanation.toExplanationString(!module.isUsed()));
}
}
str.append("\n");
if (expanded == IsExpanded.FALSE) {
return;
}
int currChild = 1;
if (options.extensionInfo != ExtensionShow.HIDDEN) {
for (ModuleExtensionId extensionId : extensionsUsed) {
boolean unexpandedExtension = !seenExtensions.add(extensionId);
isLastChildStack.push(currChild++ == totalChildrenNum);
printExtension(key, extensionId, unexpandedExtension, depth + 1);
isLastChildStack.pop();
}
}
for (Entry<ModuleKey, NodeMetadata> e : node.getChildrenSortedByEdgeType()) {
ModuleKey childKey = e.getKey();
IsExpanded childExpanded = e.getValue().isExpanded();
IsIndirect childIndirect = e.getValue().isIndirect();
IsCycle childCycles = e.getValue().isCycle();
isLastChildStack.push(currChild++ == totalChildrenNum);
printModule(childKey, key, childExpanded, childIndirect, childCycles, depth + 1);
isLastChildStack.pop();
}
}
enum DrawCharset {
ASCII(" ", "| ", "|___", "|...", "|___", "|..."),
UTF8(" ", "│ ", "├───", "├╌╌╌", "└───", "└╌╌╌");
final String emptyIndent;
final String prevChildIndent;
final String childIndent;
final String indirectChildIndent;
final String lastChildIndent;
final String lastIndirectChildIndent;
DrawCharset(
String emptyIndent,
String prevChildIndent,
String childIndent,
String indirectChildIndent,
String lastChildIndent,
String lastIndirectChildIndent) {
this.emptyIndent = emptyIndent;
this.prevChildIndent = prevChildIndent;
this.childIndent = childIndent;
this.indirectChildIndent = indirectChildIndent;
this.lastChildIndent = lastChildIndent;
this.lastIndirectChildIndent = lastIndirectChildIndent;
}
}
}