You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/28 13:27:07 UTC
(maven) branch master updated: Fix #12592: use string-based ID for forked execution cycle detection (#12867)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new ab262289a8 Fix #12592: use string-based ID for forked execution cycle detection (#12867)
ab262289a8 is described below
commit ab262289a84b0ef0855360f5f1aba83216e0227d
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Fri Aug 28 15:27:00 2026 +0200
Fix #12592: use string-based ID for forked execution cycle detection (#12867)
The cycle detection in DefaultLifecycleExecutionPlanCalculator used
Set<MojoDescriptor> (object equality) to track already-planned forked
executions. Because DefaultPluginDescriptorCache clones the
PluginDescriptor (and all its MojoDescriptor objects) on every cache
retrieval, independently-loaded descriptors for the same plugin are
always different object instances. While MojoDescriptor does override
equals/hashCode, relying on mutable object equality inside a HashSet
is fragile — if any identity field changes after insertion, the set
lookup silently fails.
Switch to Set<String> using MojoDescriptor.getId()
(groupId:artifactId:version:goal) for cycle detection. String keys
are immutable and make the comparison semantics explicit, eliminating
any risk of identity mismatch across cloned descriptor instances.
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../plugin/descriptor/MojoDescriptorTest.java | 41 ++++++++++++++++++++++
.../DefaultLifecycleExecutionPlanCalculator.java | 24 ++++++-------
.../internal/LifecycleExecutionPlanCalculator.java | 3 +-
.../stub/LifecycleExecutionPlanCalculatorStub.java | 2 +-
4 files changed, 55 insertions(+), 15 deletions(-)
diff --git a/compat/maven-plugin-api/src/test/java/org/apache/maven/plugin/descriptor/MojoDescriptorTest.java b/compat/maven-plugin-api/src/test/java/org/apache/maven/plugin/descriptor/MojoDescriptorTest.java
index 26cffced5d..723f41e0ce 100644
--- a/compat/maven-plugin-api/src/test/java/org/apache/maven/plugin/descriptor/MojoDescriptorTest.java
+++ b/compat/maven-plugin-api/src/test/java/org/apache/maven/plugin/descriptor/MojoDescriptorTest.java
@@ -18,11 +18,52 @@
*/
package org.apache.maven.plugin.descriptor;
+import java.util.HashSet;
+import java.util.Set;
+
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
class MojoDescriptorTest {
+
+ @Test
+ void idBasedCycleDetectionWithClonedDescriptors() {
+ // Simulate two independently-loaded PluginDescriptor instances for the same plugin,
+ // as happens when DefaultPluginDescriptorCache clones on every cache retrieval.
+ PluginDescriptor pd1 = new PluginDescriptor();
+ pd1.setGroupId("org.example");
+ pd1.setArtifactId("example-plugin");
+ pd1.setVersion("1.0.0");
+
+ PluginDescriptor pd2 = new PluginDescriptor();
+ pd2.setGroupId("org.example");
+ pd2.setArtifactId("example-plugin");
+ pd2.setVersion("1.0.0");
+
+ assertNotSame(pd1, pd2, "must be different object instances");
+
+ MojoDescriptor md1 = new MojoDescriptor();
+ md1.setGoal("compile");
+ md1.setPluginDescriptor(pd1);
+
+ MojoDescriptor md2 = new MojoDescriptor();
+ md2.setGoal("compile");
+ md2.setPluginDescriptor(pd2);
+
+ assertNotSame(md1, md2, "must be different object instances");
+
+ // getId() must produce the same key for both independently-loaded descriptors
+ assertEquals(md1.getId(), md2.getId(), "IDs must match for same groupId:artifactId:version:goal");
+
+ // String-based cycle detection set must detect the duplicate
+ Set<String> alreadyPlanned = new HashSet<>();
+ alreadyPlanned.add(md1.getId());
+ assertTrue(alreadyPlanned.contains(md2.getId()), "cycle detection must find the cloned descriptor by ID");
+ }
+
@Test
void getParameterMap() throws DuplicateParameterException {
MojoDescriptor mojoDescriptor = new MojoDescriptor();
diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleExecutionPlanCalculator.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleExecutionPlanCalculator.java
index e35bd8df1c..52cc4e27c1 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleExecutionPlanCalculator.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/DefaultLifecycleExecutionPlanCalculator.java
@@ -25,7 +25,6 @@
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -147,22 +146,22 @@ private void setupMojoExecutions(MavenSession session, MavenProject project, Lis
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
MojoNotFoundException, InvalidPluginDescriptorException, NoPluginFoundForPrefixException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {
- Set<MojoDescriptor> alreadyPlannedExecutions = fillMojoDescriptors(session, project, mojoExecutions);
+ Set<String> alreadyPlannedExecutions = fillMojoDescriptors(session, project, mojoExecutions);
for (MojoExecution mojoExecution : mojoExecutions) {
setupMojoExecution(session, project, mojoExecution, alreadyPlannedExecutions);
}
}
- private Set<MojoDescriptor> fillMojoDescriptors(
+ private Set<String> fillMojoDescriptors(
MavenSession session, MavenProject project, List<MojoExecution> mojoExecutions)
throws InvalidPluginDescriptorException, MojoNotFoundException, PluginResolutionException,
PluginDescriptorParsingException, PluginNotFoundException {
- Set<MojoDescriptor> descriptors = new HashSet<>(mojoExecutions.size());
+ Set<String> descriptors = new HashSet<>(mojoExecutions.size());
for (MojoExecution execution : mojoExecutions) {
MojoDescriptor mojoDescriptor = fillMojoDescriptor(session, project, execution);
- descriptors.add(mojoDescriptor);
+ descriptors.add(mojoDescriptor.getId());
}
return descriptors;
@@ -191,7 +190,7 @@ public void setupMojoExecution(
MavenSession session,
MavenProject project,
MojoExecution mojoExecution,
- Set<MojoDescriptor> alreadyPlannedExecutions)
+ Set<String> alreadyPlannedExecutions)
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
MojoNotFoundException, InvalidPluginDescriptorException, NoPluginFoundForPrefixException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {
@@ -354,7 +353,7 @@ private void calculateForkedExecutions(
MojoExecution mojoExecution,
MavenSession session,
MavenProject project,
- Collection<MojoDescriptor> alreadyPlannedExecutions)
+ Set<String> alreadyPlannedExecutions)
throws MojoNotFoundException, PluginNotFoundException, PluginResolutionException,
PluginDescriptorParsingException, NoPluginFoundForPrefixException, InvalidPluginDescriptorException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {
@@ -364,7 +363,7 @@ private void calculateForkedExecutions(
return;
}
- alreadyPlannedExecutions.add(mojoDescriptor);
+ alreadyPlannedExecutions.add(mojoDescriptor.getId());
List<MavenProject> forkedProjects =
LifecycleDependencyResolver.getProjects(project, session, mojoDescriptor.isAggregator());
@@ -395,7 +394,7 @@ private List<MojoExecution> calculateForkedLifecycle(
MojoExecution mojoExecution,
MavenSession session,
MavenProject project,
- Collection<MojoDescriptor> alreadyPlannedExecutions)
+ Set<String> alreadyPlannedExecutions)
throws MojoNotFoundException, PluginNotFoundException, PluginResolutionException,
PluginDescriptorParsingException, NoPluginFoundForPrefixException, InvalidPluginDescriptorException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {
@@ -427,7 +426,8 @@ private List<MojoExecution> calculateForkedLifecycle(
for (List<MojoExecution> forkedExecutions : lifecycleMappings.values()) {
for (MojoExecution forkedExecution : forkedExecutions) {
- if (!alreadyPlannedExecutions.contains(forkedExecution.getMojoDescriptor())) {
+ if (!alreadyPlannedExecutions.contains(
+ forkedExecution.getMojoDescriptor().getId())) {
finalizeMojoConfiguration(forkedExecution);
calculateForkedExecutions(forkedExecution, session, project, alreadyPlannedExecutions);
@@ -535,7 +535,7 @@ private List<MojoExecution> calculateForkedGoal(
MojoExecution mojoExecution,
MavenSession session,
MavenProject project,
- Collection<MojoDescriptor> alreadyPlannedExecutions)
+ Set<String> alreadyPlannedExecutions)
throws MojoNotFoundException, PluginNotFoundException, PluginResolutionException,
PluginDescriptorParsingException, NoPluginFoundForPrefixException, InvalidPluginDescriptorException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {
@@ -550,7 +550,7 @@ private List<MojoExecution> calculateForkedGoal(
throw new MojoNotFoundException(forkedGoal, pluginDescriptor);
}
- if (alreadyPlannedExecutions.contains(forkedMojoDescriptor)) {
+ if (alreadyPlannedExecutions.contains(forkedMojoDescriptor.getId())) {
return Collections.emptyList();
}
diff --git a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleExecutionPlanCalculator.java b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleExecutionPlanCalculator.java
index 2099d86291..4263bee3c3 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleExecutionPlanCalculator.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleExecutionPlanCalculator.java
@@ -31,7 +31,6 @@
import org.apache.maven.plugin.PluginDescriptorParsingException;
import org.apache.maven.plugin.PluginNotFoundException;
import org.apache.maven.plugin.PluginResolutionException;
-import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.prefix.NoPluginFoundForPrefixException;
import org.apache.maven.plugin.version.PluginVersionResolutionException;
import org.apache.maven.project.MavenProject;
@@ -60,7 +59,7 @@ void setupMojoExecution(
MavenSession session,
MavenProject project,
MojoExecution mojoExecution,
- Set<MojoDescriptor> alreadyPlannedExecutions)
+ Set<String> alreadyPlannedExecutions)
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
MojoNotFoundException, InvalidPluginDescriptorException, NoPluginFoundForPrefixException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException;
diff --git a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/stub/LifecycleExecutionPlanCalculatorStub.java b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/stub/LifecycleExecutionPlanCalculatorStub.java
index fb10992ba9..32d42c0a33 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/stub/LifecycleExecutionPlanCalculatorStub.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/stub/LifecycleExecutionPlanCalculatorStub.java
@@ -159,7 +159,7 @@ public void setupMojoExecution(
MavenSession session,
MavenProject project,
MojoExecution mojoExecution,
- Set<MojoDescriptor> alreadyForkedExecutions)
+ Set<String> alreadyForkedExecutions)
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
MojoNotFoundException, InvalidPluginDescriptorException, NoPluginFoundForPrefixException,
LifecyclePhaseNotFoundException, LifecycleNotFoundException, PluginVersionResolutionException {}