Fix StackOverflowError in bazel mod graph output
This change fixes an infinite recursion issue in TextOutputFormatter when the dependency graph contains cycles formed by merging different paths. It adds a parentStack to track visited nodes and detect cycles dynamically.
Fixes https://github.com/bazelbuild/bazel/issues/27839
Closes #27876.
PiperOrigin-RevId: 842137415
Change-Id: I131d3310b1fa939164379a50419f16ebb071ec55
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/TextOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/TextOutputFormatter.java
index a371b38..426a025 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/TextOutputFormatter.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/TextOutputFormatter.java
@@ -46,6 +46,7 @@
private DrawCharset drawCharset;
private Set<ModuleExtensionId> seenExtensions;
private StringBuilder str;
+ private Set<ModuleKey> visted;
@Override
public void output() {
@@ -56,6 +57,7 @@
}
isLastChildStack = new ArrayDeque<>();
seenExtensions = new HashSet<>();
+ visted = new HashSet<>();
str = new StringBuilder();
printModule(ModuleKey.ROOT, null, IsExpanded.TRUE, IsIndirect.FALSE, IsCycle.FALSE, 0);
this.printer.println(str);
@@ -147,74 +149,85 @@
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.name(),
- rootModule.version().equals(Version.EMPTY) ? "_" : rootModule.version()));
- } 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 (node.isTarget()) {
- str.append("# ");
+ boolean added = visted.add(key);
+ try {
+ 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.name(),
+ rootModule.version().equals(Version.EMPTY) ? "_" : rootModule.version()));
+ } else {
+ str.append(key).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()));
+
+ 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();
}
- }
- str.append("\n");
+ // If we've already seen this node in the current traversal path, treat it as a cycle
+ // even if the graph structure says otherwise (which can happen due to merged paths).
+ boolean isCycle = cycle == IsCycle.TRUE || !added;
- if (expanded == IsExpanded.FALSE) {
- return;
- }
+ if (isCycle) {
+ str.append("(cycle) ");
+ } else if (expanded == IsExpanded.FALSE) {
+ str.append("(*) ");
+ } else {
+ if (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()));
+ }
+ }
- int currChild = 1;
- if (options.extensionInfo != ExtensionShow.HIDDEN) {
- for (ModuleExtensionId extensionId : extensionsUsed) {
- boolean unexpandedExtension = !seenExtensions.add(extensionId);
+ str.append("\n");
+
+ if (expanded == IsExpanded.FALSE || isCycle) {
+ 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);
- printExtension(key, extensionId, unexpandedExtension, depth + 1);
+ printModule(childKey, key, childExpanded, childIndirect, childCycles, 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();
+ } finally {
+ if (added) {
+ visted.remove(key);
+ }
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
index da8c175..5a7d16d 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
@@ -811,6 +811,15 @@
Optional.empty());
}
+ private ModuleExtensionId createExtensionId(
+ String moduleName, String version, String path, String extensionName)
+ throws LabelSyntaxException {
+ return ModuleExtensionId.create(
+ Label.parseCanonical("@@" + moduleName + "+" + version + "//" + path + ":" + path),
+ extensionName,
+ Optional.empty());
+ }
+
@Test
public void testModCommandPath_complexGraphFiltersCorrectly() throws ParseException, IOException {
// <root> -> A -> B -> C -> D
@@ -1077,4 +1086,114 @@
"<root> (main@1.0)", "└───$@@//extensions:extensions%maven ", " └───repo1", "")
.inOrder();
}
+
+ @Test
+ public void testGraphWithExtensionFilterAndCycle() throws Exception {
+ // <root> -> A -> B -> A (cycle)
+ // A and B both use extension defined in A.
+ ImmutableMap<ModuleKey, AugmentedModule> depGraph =
+ new ImmutableMap.Builder<ModuleKey, AugmentedModule>()
+ .put(
+ buildAugmentedModule(ModuleKey.ROOT, "main", Version.parse("1.0"), true)
+ .addDep("A", "1.0")
+ .addDep("B", "1.0")
+ .buildEntry())
+ .put(
+ buildAugmentedModule("A", "1.0")
+ .addStillDependant(ModuleKey.ROOT)
+ .addStillDependant("B", "1.0")
+ .addDep("B", "1.0")
+ .buildEntry())
+ .put(
+ buildAugmentedModule("B", "1.0")
+ .addStillDependant(ModuleKey.ROOT)
+ .addStillDependant("A", "1.0")
+ .addDep("A", "1.0")
+ .buildEntry())
+ .buildOrThrow();
+
+ ModuleExtensionId extensionId = createExtensionId("A", "1.0", "extensions", "ext");
+ ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsages =
+ new ImmutableTable.Builder<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>()
+ .put(
+ extensionId,
+ createModuleKey("A", "1.0"),
+ ModuleExtensionUsage.builder()
+ .setExtensionBzlFile("//extensions:extensions.bzl")
+ .setExtensionName("ext")
+ .setRepoOverrides(ImmutableMap.of())
+ .addProxy(
+ ModuleExtensionUsage.Proxy.builder()
+ .setLocation(Location.fromFileLineColumn("MODULE.bazel", 1, 1))
+ .setImports(ImmutableBiMap.of("repo1", "repo1"))
+ .setDevDependency(false)
+ .setContainingModuleFilePath(LabelConstants.MODULE_DOT_BAZEL_FILE_NAME)
+ .build())
+ .build())
+ .put(
+ extensionId,
+ createModuleKey("B", "1.0"),
+ ModuleExtensionUsage.builder()
+ .setExtensionBzlFile("//extensions:extensions.bzl")
+ .setExtensionName("ext")
+ .setRepoOverrides(ImmutableMap.of())
+ .addProxy(
+ ModuleExtensionUsage.Proxy.builder()
+ .setLocation(Location.fromFileLineColumn("MODULE.bazel", 1, 1))
+ .setImports(ImmutableBiMap.of("repo2", "repo2"))
+ .setDevDependency(false)
+ .setContainingModuleFilePath(LabelConstants.MODULE_DOT_BAZEL_FILE_NAME)
+ .build())
+ .build())
+ .buildOrThrow();
+
+ ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos =
+ new ImmutableSetMultimap.Builder<ModuleExtensionId, String>()
+ .putAll(extensionId, ImmutableSet.of("repo1", "repo2"))
+ .build();
+
+ ModOptions options = ModOptions.getDefaultOptions();
+ options.outputFormat = OutputFormat.TEXT;
+ options.extensionInfo = ExtensionShow.ALL;
+ options.cycles = true;
+
+ File file = File.createTempFile("output_text_cycle_ext", "txt");
+ file.deleteOnExit();
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8)) {
+ ModExecutor executor =
+ new ModExecutor(
+ depGraph,
+ extensionUsages,
+ extensionRepos,
+ Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(extensionId))),
+ options,
+ writer);
+ executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+ }
+
+ List<String> textOutput = Files.readAllLines(file.toPath());
+ assertThat(textOutput)
+ .containsExactly(
+ "<root> (main@1.0)",
+ "├───A@1.0 # ",
+ "│ ├───$@@A+1.0//extensions:extensions%ext ",
+ "│ │ └───repo1",
+ "│ ├───B@1.0 (cycle) ",
+ "│ └───B@1.0 # ",
+ "│ ├───$@@A+1.0//extensions:extensions%ext ... ",
+ "│ │ └───repo2",
+ "│ ├───A@1.0 (cycle) ",
+ "│ └───A@1.0 (cycle) ",
+ "└───B@1.0 # ",
+ " ├───$@@A+1.0//extensions:extensions%ext ... ",
+ " │ └───repo2",
+ " ├───A@1.0 (cycle) ",
+ " └───A@1.0 # ",
+ " ├───$@@A+1.0//extensions:extensions%ext ... ",
+ " │ └───repo1",
+ " ├───B@1.0 (cycle) ",
+ " └───B@1.0 (cycle) ",
+ "")
+ .inOrder();
+ }
}