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 {