gnodet-bot commented on code in PR #13366:
URL: https://github.com/apache/maven/pull/13366#discussion_r4215490535


##########
impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultMojoExecution.java:
##########
@@ -38,110 +38,153 @@
 import org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle;
 import org.apache.maven.api.xml.XmlNode;
 import org.apache.maven.impl.DefaultNode;
-import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.eclipse.aether.graph.DependencyNode;
 
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Immutable snapshot of a mojo execution, captured at the point when 
execution begins
+ * (after configuration merging, descriptor resolution, and lifecycle phase 
assignment are complete).
+ * All state is copied at construction time; no reference to the mutable legacy
+ * {@link org.apache.maven.plugin.MojoExecution} is retained after 
construction.
+ */
 public class DefaultMojoExecution implements MojoExecution {
-    private final InternalMavenSession session;
-    private final org.apache.maven.plugin.MojoExecution delegate;
+
+    private final Plugin plugin;
+    private final Optional<PluginExecution> model;
+    private final MojoDescriptor descriptor;
+    private final String executionId;
+    private final String goal;
+    private final String lifecyclePhase;
+    private final XmlNode configuration;
 
     public DefaultMojoExecution(InternalMavenSession session, 
org.apache.maven.plugin.MojoExecution delegate) {
-        this.session = session;
-        this.delegate = delegate;
+        requireNonNull(session, "session");
+        requireNonNull(delegate, "delegate");
+        this.descriptor = requireNonNull(delegate.getMojoDescriptor(), 
"delegate.mojoDescriptor")
+                .getMojoDescriptorV4();
+        this.executionId = delegate.getExecutionId();
+        this.goal = delegate.getGoal();
+        this.lifecyclePhase = delegate.getLifecyclePhase();

Review Comment:
   ⚠️ **Null stored into @Nonnull field:** `delegate.getLifecyclePhase()` 
returns `null` for CLI-invoked direct goals. The `lifecyclePhase` field is then 
returned by `lifecyclePhase()` which is declared `@Nonnull` on the interface — 
callers trusting the annotation will NPE.
   
   Either add a null guard here (e.g. 
`Objects.requireNonNullElse(delegate.getLifecyclePhase(), "")` — though empty 
string is semantically wrong) or change the interface method to return 
`Optional<String>` as suggested above.



##########
api/maven-api-core/src/main/java/org/apache/maven/api/MojoExecution.java:
##########
@@ -31,30 +33,118 @@
  * An instance of this object is bound to the {@link 
org.apache.maven.api.di.MojoExecutionScoped}
  * and available as {@code mojoExecution} within {@link 
org.apache.maven.api.plugin.annotations.Parameter}
  * expressions.
+ * <p>
+ * Instances are immutable snapshots taken at the point when execution begins 
(after configuration
+ * merging, descriptor resolution, and lifecycle phase assignment are all 
complete).
  *
  * @since 4.0.0
  */
 @Experimental
+@Immutable
 public interface MojoExecution {
 
+    /** {@return the plugin that owns this execution} */
     @Nonnull
-    Plugin getPlugin();
+    Plugin plugin();
 
+    /**
+     * {@return the {@code <execution>} element from the POM (or from the 
default lifecycle bindings)
+     * that corresponds to this execution, or empty for a direct CLI invocation
+     * (e.g. {@code mvn groupId:artifactId:goal})}
+     * <p>
+     * Default lifecycle bindings (such as {@code 
maven-compiler-plugin:compile} bound to the
+     * {@code compile} phase for {@code jar} packaging) are synthesised from 
lifecycle mapping
+     * metadata and injected as {@code <execution>} elements before model 
resolution, so they
+     * are also present here.  Only a CLI-invoked goal — which has no backing 
{@code <plugin>}
+     * entry in the model — results in an empty {@code Optional}.
+     */
     @Nonnull
-    PluginExecution getModel();
+    Optional<PluginExecution> model();
 
+    /** {@return the descriptor of the mojo being executed} */
     @Nonnull
-    MojoDescriptor getDescriptor();
+    MojoDescriptor descriptor();
 
+    /** {@return the execution identifier as declared in the POM} */
     @Nonnull
-    String getExecutionId();
+    String executionId();
 
+    /** {@return the goal being executed} */
     @Nonnull
-    String getGoal();
+    String goal();
 
+    /** {@return the lifecycle phase this execution is bound to} */
     @Nonnull
-    String getLifecyclePhase();
+    String lifecyclePhase();

Review Comment:
   ⚠️ **@Nonnull contract violation:** `lifecyclePhase()` returns `null` for 
direct CLI-invoked goals (e.g. `mvn groupId:artifactId:goal`) where no 
lifecycle phase is assigned. `DefaultMojoExecution` stores 
`delegate.getLifecyclePhase()` verbatim with no null guard, and the legacy 
`MojoExecution.lifecyclePhase` field defaults to `null` when 
`setLifecyclePhase()` is never called.
   
   For consistency with how `model()` handles optionality:
   
   ```suggestion
       /** {@return the lifecycle phase this execution is bound to, or empty 
for a direct CLI invocation} */
       @Nonnull
       Optional<String> lifecyclePhase();
   ```
   
   The deprecated bridge would then be:
   ```java
   @Deprecated(since = "4.1.0", forRemoval = true)
   @Nullable
   default String getLifecyclePhase() { return lifecyclePhase().orElse(null); }
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultMojoExecution.java:
##########
@@ -38,110 +38,153 @@
 import org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle;
 import org.apache.maven.api.xml.XmlNode;
 import org.apache.maven.impl.DefaultNode;
-import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.eclipse.aether.graph.DependencyNode;
 
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Immutable snapshot of a mojo execution, captured at the point when 
execution begins
+ * (after configuration merging, descriptor resolution, and lifecycle phase 
assignment are complete).
+ * All state is copied at construction time; no reference to the mutable legacy
+ * {@link org.apache.maven.plugin.MojoExecution} is retained after 
construction.
+ */
 public class DefaultMojoExecution implements MojoExecution {
-    private final InternalMavenSession session;
-    private final org.apache.maven.plugin.MojoExecution delegate;
+
+    private final Plugin plugin;
+    private final Optional<PluginExecution> model;
+    private final MojoDescriptor descriptor;
+    private final String executionId;
+    private final String goal;
+    private final String lifecyclePhase;
+    private final XmlNode configuration;
 
     public DefaultMojoExecution(InternalMavenSession session, 
org.apache.maven.plugin.MojoExecution delegate) {
-        this.session = session;
-        this.delegate = delegate;
+        requireNonNull(session, "session");
+        requireNonNull(delegate, "delegate");
+        this.descriptor = requireNonNull(delegate.getMojoDescriptor(), 
"delegate.mojoDescriptor")
+                .getMojoDescriptorV4();
+        this.executionId = delegate.getExecutionId();
+        this.goal = delegate.getGoal();
+        this.lifecyclePhase = delegate.getLifecyclePhase();
+        this.configuration = delegate.getConfiguration() != null
+                ? delegate.getConfiguration().getDom()
+                : null;
+        this.plugin = buildPlugin(session, delegate);
+        this.model = buildModel(delegate);
     }
 
-    public org.apache.maven.plugin.MojoExecution getDelegate() {
-        return delegate;
-    }
+    private static Plugin buildPlugin(InternalMavenSession session, 
org.apache.maven.plugin.MojoExecution delegate) {
+        org.apache.maven.plugin.descriptor.MojoDescriptor legacyDescriptor = 
delegate.getMojoDescriptor();
+        org.apache.maven.plugin.descriptor.PluginDescriptor 
legacyPluginDescriptor =
+                legacyDescriptor.getPluginDescriptor();
+        PluginDescriptor pluginDescriptorV4 = 
legacyPluginDescriptor.getPluginDescriptorV4();
+
+        ClassLoader classLoader = legacyDescriptor.getRealm();
+
+        org.apache.maven.artifact.Artifact legacyArtifact = 
legacyPluginDescriptor.getPluginArtifact();
+        org.eclipse.aether.artifact.Artifact resolverArtifact = 
RepositoryUtils.toArtifact(legacyArtifact);
+        Artifact artifact = resolverArtifact != null ? 
session.getArtifact(resolverArtifact) : null;
+
+        DependencyNode resolverNode = 
legacyPluginDescriptor.getDependencyNode();
+        Map<String, Dependency> dependenciesMap = resolverNode != null
+                ? Collections.unmodifiableMap(new DefaultNode(session, 
resolverNode, false)
+                        .stream()
+                                .filter(Objects::nonNull)
+                                .map(Node::getDependency)
+                                .filter(Objects::nonNull)
+                                .collect(Collectors.toMap(
+                                        d -> d.getGroupId() + ":" + 
d.getArtifactId(), d -> d, (a, b) -> a)))

Review Comment:
   ⚠️ **@Nonnull contract violation on Plugin.getModel():** `modelPlugin` can 
be `null` when `delegate.getPlugin() == null` (CLI-invoked goals have no 
backing `<plugin>` in the model). The anonymous `Plugin.getModel()` returns it 
directly, but `Plugin.getModel()` is declared `@Nonnull` in the `Plugin` 
interface.
   
   Same for `Plugin.getArtifact()`: `artifact` is `null` when 
`RepositoryUtils.toArtifact(legacyArtifact)` returns null, but `getArtifact()` 
is declared `@Nonnull Artifact`.
   
   Both were pre-existing issues in the old lazy-delegation code but the 
snapshot pattern makes them structural — worth fixing now. Either add 
`requireNonNull` guards (with a sensible sentinel), update the `Plugin` 
interface contracts to `@Nullable`, or ensure null-hostile call sites are 
guarded.



##########
impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultMojoExecution.java:
##########
@@ -38,110 +38,153 @@
 import org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle;
 import org.apache.maven.api.xml.XmlNode;
 import org.apache.maven.impl.DefaultNode;
-import org.codehaus.plexus.util.xml.Xpp3Dom;
 import org.eclipse.aether.graph.DependencyNode;
 
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Immutable snapshot of a mojo execution, captured at the point when 
execution begins
+ * (after configuration merging, descriptor resolution, and lifecycle phase 
assignment are complete).
+ * All state is copied at construction time; no reference to the mutable legacy
+ * {@link org.apache.maven.plugin.MojoExecution} is retained after 
construction.
+ */
 public class DefaultMojoExecution implements MojoExecution {
-    private final InternalMavenSession session;
-    private final org.apache.maven.plugin.MojoExecution delegate;
+
+    private final Plugin plugin;
+    private final Optional<PluginExecution> model;
+    private final MojoDescriptor descriptor;
+    private final String executionId;
+    private final String goal;
+    private final String lifecyclePhase;
+    private final XmlNode configuration;
 
     public DefaultMojoExecution(InternalMavenSession session, 
org.apache.maven.plugin.MojoExecution delegate) {
-        this.session = session;
-        this.delegate = delegate;
+        requireNonNull(session, "session");
+        requireNonNull(delegate, "delegate");
+        this.descriptor = requireNonNull(delegate.getMojoDescriptor(), 
"delegate.mojoDescriptor")
+                .getMojoDescriptorV4();
+        this.executionId = delegate.getExecutionId();
+        this.goal = delegate.getGoal();
+        this.lifecyclePhase = delegate.getLifecyclePhase();
+        this.configuration = delegate.getConfiguration() != null
+                ? delegate.getConfiguration().getDom()
+                : null;
+        this.plugin = buildPlugin(session, delegate);
+        this.model = buildModel(delegate);
     }
 
-    public org.apache.maven.plugin.MojoExecution getDelegate() {
-        return delegate;
-    }
+    private static Plugin buildPlugin(InternalMavenSession session, 
org.apache.maven.plugin.MojoExecution delegate) {
+        org.apache.maven.plugin.descriptor.MojoDescriptor legacyDescriptor = 
delegate.getMojoDescriptor();
+        org.apache.maven.plugin.descriptor.PluginDescriptor 
legacyPluginDescriptor =
+                legacyDescriptor.getPluginDescriptor();
+        PluginDescriptor pluginDescriptorV4 = 
legacyPluginDescriptor.getPluginDescriptorV4();
+
+        ClassLoader classLoader = legacyDescriptor.getRealm();
+
+        org.apache.maven.artifact.Artifact legacyArtifact = 
legacyPluginDescriptor.getPluginArtifact();
+        org.eclipse.aether.artifact.Artifact resolverArtifact = 
RepositoryUtils.toArtifact(legacyArtifact);
+        Artifact artifact = resolverArtifact != null ? 
session.getArtifact(resolverArtifact) : null;
+
+        DependencyNode resolverNode = 
legacyPluginDescriptor.getDependencyNode();
+        Map<String, Dependency> dependenciesMap = resolverNode != null
+                ? Collections.unmodifiableMap(new DefaultNode(session, 
resolverNode, false)
+                        .stream()
+                                .filter(Objects::nonNull)
+                                .map(Node::getDependency)
+                                .filter(Objects::nonNull)
+                                .collect(Collectors.toMap(
+                                        d -> d.getGroupId() + ":" + 
d.getArtifactId(), d -> d, (a, b) -> a)))
+                : Collections.emptyMap();
+
+        org.apache.maven.api.model.Plugin modelPlugin =
+                delegate.getPlugin() != null ? 
delegate.getPlugin().getDelegate() : null;
 
-    @Override
-    public Plugin getPlugin() {
         return new Plugin() {
             @Override
             public org.apache.maven.api.model.Plugin getModel() {
-                return delegate.getPlugin().getDelegate();
+                return modelPlugin;
             }
 
             @Override
             public PluginDescriptor getDescriptor() {
-                return 
delegate.getMojoDescriptor().getPluginDescriptor().getPluginDescriptorV4();
+                return pluginDescriptorV4;
             }
 

Review Comment:
   💡 **Behavioral change worth documenting:** The old `Collectors.toMap()` 
without a merge function would throw `IllegalStateException` on duplicate 
`groupId:artifactId` keys in the dependency graph. The new `(a, b) -> a` 
silently picks the first entry. This is a fix (duplicate transitive 
dependencies are normal), but a brief comment would help future readers 
understand the intent:
   
   ```suggestion
                           .collect(Collectors.toMap(
                                   d -> d.getGroupId() + ":" + 
d.getArtifactId(),
                                   d -> d,
                                   (a, b) -> a))) // first-wins on duplicate 
groupId:artifactId (transitive duplicates are normal)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to