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 {}