diff --git a/pom.xml b/pom.xml
index a5861f4..48411d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -253,6 +253,12 @@
4.11.0
test
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+ test
+
diff --git a/src/it/projects/describe-cmd/verify.groovy b/src/it/projects/describe-cmd/verify.groovy
index b46af06..de3cdb4 100644
--- a/src/it/projects/describe-cmd/verify.groovy
+++ b/src/it/projects/describe-cmd/verify.groovy
@@ -22,7 +22,7 @@ def result = new File(basedir, 'result-deploy.txt').text;
def ls = System.getProperty( "line.separator" );
// used deprecated methods - FIXME in DescribeMojo
-if (mavenVersion.startsWith('4.') || mavenVersion.startsWith('3.10.')) {
+if (mavenVersion.startsWith('4.')) {
assert result.contains("'deploy' is a phase within the 'default' lifecycle, which has the following phases:")
} else {
assert result.contains("'deploy' is a phase corresponding to this plugin:" + ls +
diff --git a/src/main/java/org/apache/maven/plugins/help/DescribeMojo.java b/src/main/java/org/apache/maven/plugins/help/DescribeMojo.java
index 9caa5cf..17948d6 100644
--- a/src/main/java/org/apache/maven/plugins/help/DescribeMojo.java
+++ b/src/main/java/org/apache/maven/plugins/help/DescribeMojo.java
@@ -29,7 +29,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -39,6 +38,8 @@
import org.apache.maven.lifecycle.Lifecycle;
import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
import org.apache.maven.lifecycle.mapping.LifecycleMapping;
+import org.apache.maven.lifecycle.mapping.LifecycleMojo;
+import org.apache.maven.lifecycle.mapping.LifecyclePhase;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.plugin.MavenPluginManager;
@@ -496,7 +497,7 @@ private void describeMojoGuts(MojoDescriptor md, StringBuilder buffer, boolean f
deprecation = NO_REASON;
}
- if (deprecation != null && !deprecation.isEmpty()) {
+ if (deprecation != null) {
append(
buffer,
MessageUtils.buffer().warning("Deprecated. " + deprecation).build(),
@@ -624,7 +625,7 @@ private void describeMojoParameters(MojoDescriptor md, StringBuilder buffer)
deprecation = NO_REASON;
}
- if (deprecation != null && !deprecation.isEmpty()) {
+ if (deprecation != null) {
append(
buffer,
MessageUtils.buffer()
@@ -650,15 +651,16 @@ private boolean describeCommand(StringBuilder descriptionBuffer) throws MojoExec
throw new MojoExecutionException("The given phase '" + cmd + "' is an unknown phase.");
}
- // FIXME don't use a deprecated methods
- Map defaultLifecyclePhases = lifecycleMappings
- .get(project.getPackaging())
- .getLifecycles()
- .get("default")
- .getPhases();
List phases = lifecycle.getPhases();
- if (lifecycle.getDefaultPhases() == null) {
+ if (lifecycle.getDefaultLifecyclePhases() == null
+ || lifecycle.getDefaultLifecyclePhases().isEmpty()) {
+ Map defaultLifecyclePhases = lifecycleMappings
+ .get(project.getPackaging())
+ .getLifecycles()
+ .get("default")
+ .getLifecyclePhases();
+
descriptionBuffer.append("'").append(cmd);
descriptionBuffer
.append("' is a phase corresponding to this plugin:")
@@ -680,17 +682,13 @@ private boolean describeCommand(StringBuilder descriptionBuffer) throws MojoExec
descriptionBuffer.append(LS);
for (String key : phases) {
descriptionBuffer.append("* ").append(key).append(": ");
- String value = defaultLifecyclePhases.get(key);
- if (value != null && !value.isEmpty()) {
- for (StringTokenizer tok = new StringTokenizer(value, ","); tok.hasMoreTokens(); ) {
- descriptionBuffer.append(tok.nextToken().trim());
-
- if (!tok.hasMoreTokens()) {
- descriptionBuffer.append(LS);
- } else {
- descriptionBuffer.append(", ");
- }
- }
+ LifecyclePhase phase = defaultLifecyclePhases.get(key);
+ if (phase != null && !phase.getMojos().isEmpty()) {
+ descriptionBuffer
+ .append(phase.getMojos().stream()
+ .map(LifecycleMojo::getGoal)
+ .collect(Collectors.joining(", ")))
+ .append(LS);
} else {
descriptionBuffer.append(NOT_DEFINED).append(LS);
}
@@ -703,9 +701,9 @@ private boolean describeCommand(StringBuilder descriptionBuffer) throws MojoExec
for (String key : phases) {
descriptionBuffer.append("* ").append(key).append(": ");
- if (lifecycle.getDefaultPhases().get(key) != null) {
+ if (lifecycle.getDefaultLifecyclePhases().get(key) != null) {
descriptionBuffer
- .append(lifecycle.getDefaultPhases().get(key))
+ .append(lifecycle.getDefaultLifecyclePhases().get(key))
.append(LS);
} else {
descriptionBuffer.append(NOT_DEFINED).append(LS);
diff --git a/src/test/java/org/apache/maven/plugins/help/DescribeMojoTest.java b/src/test/java/org/apache/maven/plugins/help/DescribeMojoTest.java
index fe458c6..5d64e37 100644
--- a/src/test/java/org/apache/maven/plugins/help/DescribeMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/help/DescribeMojoTest.java
@@ -21,12 +21,21 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.DefaultLifecycles;
+import org.apache.maven.lifecycle.Lifecycle;
import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
+import org.apache.maven.lifecycle.mapping.LifecycleMapping;
+import org.apache.maven.lifecycle.mapping.LifecyclePhase;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MavenPluginManager;
+import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.Parameter;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
@@ -40,6 +49,7 @@
import org.mockito.ArgumentCaptor;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -348,6 +358,124 @@ void testLookupPluginDescriptorAMissingG() {
}
}
+ /**
+ * Regression test for Maven 3.10.0 behavior where {@code Lifecycle.getDefaultLifecyclePhases()}
+ * returns an empty map (not null) for the "default" lifecycle.
+ * The mojo must fall through to the packaging-specific lifecycle mapping in that case.
+ */
+ @Test
+ void testDescribeCommandPackagingSpecificPhaseShowsBindings() throws Exception {
+ // default lifecycle: built-in phases are empty (3.10.0 behavior)
+ Lifecycle lifecycle = mock(Lifecycle.class);
+ when(lifecycle.getId()).thenReturn("default");
+ when(lifecycle.getPhases()).thenReturn(Arrays.asList("validate", "compile", "test", "package"));
+ when(lifecycle.getDefaultLifecyclePhases()).thenReturn(Collections.emptyMap());
+
+ Map phaseMap = new HashMap<>();
+ for (String p : Arrays.asList("validate", "compile", "test", "package")) {
+ phaseMap.put(p, lifecycle);
+ }
+ DefaultLifecycles defaultLifecycles = mock(DefaultLifecycles.class);
+ when(defaultLifecycles.getPhaseToLifecycleMap()).thenReturn(phaseMap);
+
+ // packaging-specific bindings: only "compile" phase is bound
+ Map lifecyclePhases = new LinkedHashMap<>();
+ lifecyclePhases.put(
+ "compile", new LifecyclePhase("org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile"));
+ org.apache.maven.lifecycle.mapping.Lifecycle mappingLifecycle =
+ new org.apache.maven.lifecycle.mapping.Lifecycle();
+ mappingLifecycle.setLifecyclePhases(lifecyclePhases);
+
+ LifecycleMapping lifecycleMapping = mock(LifecycleMapping.class);
+ when(lifecycleMapping.getLifecycles()).thenReturn(Collections.singletonMap("default", mappingLifecycle));
+
+ MavenProject project = mock(MavenProject.class);
+ when(project.getPackaging()).thenReturn("jar");
+
+ DescribeMojo mojo = new DescribeMojo(
+ null, null, null, null, null, defaultLifecycles, Collections.singletonMap("jar", lifecycleMapping));
+ setFieldWithReflection(mojo, "cmd", "compile");
+ setParentFieldWithReflection(mojo, "project", project);
+
+ StringBuilder sb = new StringBuilder();
+ Method describeCommand = DescribeMojo.class.getDeclaredMethod("describeCommand", StringBuilder.class);
+ describeCommand.setAccessible(true);
+ boolean result = (boolean) describeCommand.invoke(mojo, sb);
+
+ String output = sb.toString();
+ assertFalse(result);
+ assertTrue(output.contains("maven-compiler-plugin"), "Should show compiler plugin: " + output);
+ assertFalse(output.contains("* compile: Not defined"), "compile should not be 'Not defined': " + output);
+ assertTrue(output.contains("* validate: Not defined"), "validate has no binding: " + output);
+ assertTrue(output.contains("It is a part of the lifecycle for the POM packaging 'jar'"), output);
+ }
+
+ /**
+ * Built-in lifecycle (e.g. "clean") has non-empty {@code getDefaultLifecyclePhases()} —
+ * the mojo should use those directly without consulting lifecycle mappings.
+ */
+ @Test
+ void testDescribeCommandBuiltinLifecyclePhaseShowsBindings() throws Exception {
+ Map builtinPhases = new LinkedHashMap<>();
+ builtinPhases.put("pre-clean", null);
+ builtinPhases.put("clean", new LifecyclePhase("org.apache.maven.plugins:maven-clean-plugin:3.2.0:clean"));
+ builtinPhases.put("post-clean", null);
+
+ Lifecycle lifecycle = mock(Lifecycle.class);
+ when(lifecycle.getId()).thenReturn("clean");
+ when(lifecycle.getPhases()).thenReturn(Arrays.asList("pre-clean", "clean", "post-clean"));
+ when(lifecycle.getDefaultLifecyclePhases()).thenReturn(builtinPhases);
+
+ Map phaseMap = new HashMap<>();
+ for (String p : Arrays.asList("pre-clean", "clean", "post-clean")) {
+ phaseMap.put(p, lifecycle);
+ }
+ DefaultLifecycles defaultLifecycles = mock(DefaultLifecycles.class);
+ when(defaultLifecycles.getPhaseToLifecycleMap()).thenReturn(phaseMap);
+
+ MavenProject project = mock(MavenProject.class);
+ when(project.getPackaging()).thenReturn("jar");
+
+ DescribeMojo mojo = new DescribeMojo(null, null, null, null, null, defaultLifecycles, Collections.emptyMap());
+ setFieldWithReflection(mojo, "cmd", "clean");
+ setFieldWithReflection(mojo, "lifecycleMappings", Collections.emptyMap());
+ setParentFieldWithReflection(mojo, "project", project);
+
+ StringBuilder sb = new StringBuilder();
+ Method describeCommand = DescribeMojo.class.getDeclaredMethod("describeCommand", StringBuilder.class);
+ describeCommand.setAccessible(true);
+ boolean result = (boolean) describeCommand.invoke(mojo, sb);
+
+ String output = sb.toString();
+ assertFalse(result);
+ assertTrue(output.contains("'clean' is a phase within the 'clean' lifecycle"), output);
+ assertTrue(output.contains("maven-clean-plugin"), output);
+ assertTrue(output.contains("* pre-clean: Not defined"), output);
+ assertTrue(output.contains("* post-clean: Not defined"), output);
+ }
+
+ @Test
+ void testDescribeCommandUnknownPhaseThrows() throws Exception {
+ DefaultLifecycles defaultLifecycles = mock(DefaultLifecycles.class);
+ when(defaultLifecycles.getPhaseToLifecycleMap()).thenReturn(Collections.emptyMap());
+
+ DescribeMojo mojo = new DescribeMojo(null, null, null, null, null, defaultLifecycles, Collections.emptyMap());
+ setFieldWithReflection(mojo, "cmd", "nonexistent-phase");
+ setParentFieldWithReflection(mojo, "project", mock(MavenProject.class));
+
+ Method describeCommand = DescribeMojo.class.getDeclaredMethod("describeCommand", StringBuilder.class);
+ describeCommand.setAccessible(true);
+ try {
+ describeCommand.invoke(mojo, new StringBuilder());
+ fail("Expected MojoExecutionException");
+ } catch (InvocationTargetException e) {
+ assertTrue(
+ e.getTargetException() instanceof MojoExecutionException,
+ "Expected MojoExecutionException, got: " + e.getTargetException());
+ assertTrue(e.getTargetException().getMessage().contains("nonexistent-phase"));
+ }
+ }
+
private static void setParentFieldWithReflection(
final DescribeMojo mojo, final String fieldName, final Object value)
throws NoSuchFieldException, IllegalAccessException {