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


##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/CoordinatePredicate.java:
##########
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.filter;
+
+import org.apache.maven.api.MojoExecution;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * A {@link FilterPredicate} that matches {@link MojoExecution}s by plugin 
coordinate or prefix.
+ *
+ * <h2>Syntax: {@code ([G[:A]]|P)[:v][:g[@e]]}</h2>
+ *
+ * <p>The {@code @} separator for execution ID is compatible with Maven's 
existing
+ * {@code plugin:version:goal@executionId} notation used in
+ * {@code DefaultLifecycleExecutionPlanCalculator} for goal tasks.
+ *
+ * <p>Matching uses {@link MojoExecution#getDescriptor()} for goal-level 
fields and
+ * {@link MojoExecution#getPlugin()} for plugin-level coordinates (groupId, 
artifactId, version,
+ * goal prefix).
+ *
+ * <p>Forms:
+ * <ul>
+ *   <li>{@code *} — matches every mojo execution</li>
+ *   <li>{@code :A} — any groupId, specific artifactId (e.g. {@code 
:maven-enforcer-plugin})</li>
+ *   <li>{@code G:A} — exact groupId:artifactId (e.g. {@code 
org.apache.maven.plugins:maven-enforcer-plugin}).
+ *       The groupId <strong>must contain at least one {@code '.'}</strong> 
for this form to be recognised;
+ *       groupIds without a dot (e.g. {@code commons-io}) are routed to 
prefix-based matching instead.
+ *       Use the {@code :A} form to match by artifactId only when the groupId 
has no dot.</li>
+ *   <li>{@code P} — plugin prefix (e.g. {@code enforcer}), resolved against 
the plugin descriptor's
+ *       goal prefix.
+ *       The prefix <strong>must not contain a {@code '.'}</strong>; prefixes 
with a dot
+ *       (e.g. {@code io.smallrye}) are routed to groupId:artifactId matching 
instead.</li>
+ *   <li>{@code P:v:g} — prefix + version + goal</li>
+ *   <li>{@code P:v:g@e} — prefix + version + goal + executionId</li>
+ * </ul>
+ *
+ * <p>When any field is {@code null} or not specified, it is treated as a 
wildcard (matches any value).
+ *
+ * @since 4.1.0
+ */
+public class CoordinatePredicate implements FilterPredicate {
+
+    /** Wildcard token — matches any value. */
+    private static final String ANY = null;
+
+    private final boolean matchAll;
+    private final String groupId; // null = any, non-null = exact match
+    private final String artifactId; // null = any, non-null = exact match
+    private final String prefix; // null = not used, non-null = match by goal 
prefix
+    private final String version; // null = any
+    private final String goal; // null = any
+    private final String executionId; // null = any
+
+    /** Wildcard predicate — matches everything. */
+    public static final CoordinatePredicate MATCH_ALL = new 
CoordinatePredicate();
+
+    private CoordinatePredicate() {
+        this.matchAll = true;
+        this.groupId = ANY;
+        this.artifactId = ANY;
+        this.prefix = ANY;
+        this.version = ANY;
+        this.goal = ANY;
+        this.executionId = ANY;
+    }
+
+    private CoordinatePredicate(
+            String groupId, String artifactId, String prefix, String version, 
String goal, String executionId) {
+        this.matchAll = false;
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.prefix = prefix;
+        this.version = version;
+        this.goal = goal;
+        this.executionId = executionId;
+    }
+
+    /**
+     * Parses a coordinate predicate from a string token.
+     *
+     * <p>Supported forms:
+     * <ul>
+     *   <li>{@code *} — match all</li>
+     *   <li>{@code :A} — by artifactId only</li>
+     *   <li>{@code G:A} — by groupId:artifactId. The groupId must contain at 
least one {@code '.'} to be
+     *       recognised as a G:A form; groupIds without a dot are treated as a 
plugin prefix instead.</li>
+     *   <li>{@code P} — by prefix</li>
+     *   <li>{@code P:v:g} — by prefix + version + goal</li>
+     *   <li>{@code P:v:g@e} — by prefix + version + goal + executionId</li>
+     * </ul>
+     *
+     * @param token the filter expression token (not {@code null}, not blank)
+     * @return the parsed predicate
+     */
+    public static CoordinatePredicate parse(String token) {
+        if ("*".equals(token)) {
+            return MATCH_ALL;
+        }
+
+        if (token.startsWith(":")) {
+            // :A form — any groupId, specific artifactId, optional :v:g[@e]
+            // e.g. ":maven-enforcer-plugin" or 
":maven-enforcer-plugin:3.0.0:enforce@enforce-id"
+            String rest = token.substring(1); // remove leading ':'
+            String[] parts = rest.split(":", 3);
+            String artifactId = emptyToNull(parts[0]);
+            String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+            String goalAndExec = parts.length > 2 ? parts[2] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(ANY, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // Try to detect G:A form: the token contains ':' AND the part before 
the first ':' looks like a
+        // groupId (contains a '.' suggesting it's a Java package name like 
org.apache.maven).
+        // This distinguishes "org.apache.maven.plugins:maven-enforcer-plugin" 
from "enforcer:3.1.0:enforce".
+        // Limitation: groupIds that do not contain a '.' (e.g. 
"commons-io:commons-io") are misrouted
+        // to prefix-based matching. For such groupIds use the explicit ":A" 
form for artifactId-only
+        // matching, or the full "G:A" form only when the groupId contains at 
least one '.'.
+        int firstColon = token.indexOf(':');
+        if (firstColon > 0 && token.substring(0, firstColon).contains(".")) {
+            // G:A[:v[:g[@e]]] form
+            String[] parts = token.split(":", 4);
+            String groupId = emptyToNull(parts[0]);
+            String artifactId = parts.length > 1 ? emptyToNull(parts[1]) : 
null;
+            String version = parts.length > 2 ? emptyToNull(parts[2]) : null;
+            String goalAndExec = parts.length > 3 ? parts[3] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(groupId, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // P[:v[:g[@e]]] form — prefix-based
+        String[] parts = token.split(":", 3);
+        String prefix = emptyToNull(parts[0]);
+        String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+        String goalAndExec = parts.length > 2 ? parts[2] : null;
+        String[] ge = splitGoalExecution(goalAndExec);
+        return new CoordinatePredicate(ANY, ANY, prefix, version, ge[0], 
ge[1]);
+    }
+
+    /**
+     * Splits a {@code "goal"} or {@code "goal@executionId"} string into a 
2-element array
+     * {@code [goal, executionId]}. Either element may be {@code null}.
+     */
+    private static String[] splitGoalExecution(String goalAndExec) {
+        if (goalAndExec == null || goalAndExec.isEmpty()) {
+            return new String[] {null, null};
+        }
+        int atIdx = goalAndExec.indexOf('@');
+        if (atIdx < 0) {
+            return new String[] {emptyToNull(goalAndExec), null};
+        }
+        return new String[] {emptyToNull(goalAndExec.substring(0, atIdx)), 
emptyToNull(goalAndExec.substring(atIdx + 1))
+        };
+    }
+
+    private static String emptyToNull(String s) {
+        return (s == null || s.isEmpty()) ? null : s;
+    }
+
+    @Override
+    public boolean matches(MojoExecution execution) {
+        if (matchAll) {
+            return true;
+        }
+
+        PluginDescriptor descriptor = execution.getPlugin().getDescriptor();

Review Comment:
   ⚠️ **Compilation error:** `getPlugin()` does not exist on 
`org.apache.maven.api.MojoExecution`. The Maven 4 API uses noun-style 
accessors: use `plugin()`.
   
   ```suggestion
           PluginDescriptor descriptor = execution.plugin().getDescriptor();
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/CoordinatePredicate.java:
##########
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.filter;
+
+import org.apache.maven.api.MojoExecution;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * A {@link FilterPredicate} that matches {@link MojoExecution}s by plugin 
coordinate or prefix.
+ *
+ * <h2>Syntax: {@code ([G[:A]]|P)[:v][:g[@e]]}</h2>
+ *
+ * <p>The {@code @} separator for execution ID is compatible with Maven's 
existing
+ * {@code plugin:version:goal@executionId} notation used in
+ * {@code DefaultLifecycleExecutionPlanCalculator} for goal tasks.
+ *
+ * <p>Matching uses {@link MojoExecution#getDescriptor()} for goal-level 
fields and
+ * {@link MojoExecution#getPlugin()} for plugin-level coordinates (groupId, 
artifactId, version,
+ * goal prefix).
+ *
+ * <p>Forms:
+ * <ul>
+ *   <li>{@code *} — matches every mojo execution</li>
+ *   <li>{@code :A} — any groupId, specific artifactId (e.g. {@code 
:maven-enforcer-plugin})</li>
+ *   <li>{@code G:A} — exact groupId:artifactId (e.g. {@code 
org.apache.maven.plugins:maven-enforcer-plugin}).
+ *       The groupId <strong>must contain at least one {@code '.'}</strong> 
for this form to be recognised;
+ *       groupIds without a dot (e.g. {@code commons-io}) are routed to 
prefix-based matching instead.
+ *       Use the {@code :A} form to match by artifactId only when the groupId 
has no dot.</li>
+ *   <li>{@code P} — plugin prefix (e.g. {@code enforcer}), resolved against 
the plugin descriptor's
+ *       goal prefix.
+ *       The prefix <strong>must not contain a {@code '.'}</strong>; prefixes 
with a dot
+ *       (e.g. {@code io.smallrye}) are routed to groupId:artifactId matching 
instead.</li>
+ *   <li>{@code P:v:g} — prefix + version + goal</li>
+ *   <li>{@code P:v:g@e} — prefix + version + goal + executionId</li>
+ * </ul>
+ *
+ * <p>When any field is {@code null} or not specified, it is treated as a 
wildcard (matches any value).
+ *
+ * @since 4.1.0
+ */
+public class CoordinatePredicate implements FilterPredicate {
+
+    /** Wildcard token — matches any value. */
+    private static final String ANY = null;
+
+    private final boolean matchAll;
+    private final String groupId; // null = any, non-null = exact match
+    private final String artifactId; // null = any, non-null = exact match
+    private final String prefix; // null = not used, non-null = match by goal 
prefix
+    private final String version; // null = any
+    private final String goal; // null = any
+    private final String executionId; // null = any
+
+    /** Wildcard predicate — matches everything. */
+    public static final CoordinatePredicate MATCH_ALL = new 
CoordinatePredicate();
+
+    private CoordinatePredicate() {
+        this.matchAll = true;
+        this.groupId = ANY;
+        this.artifactId = ANY;
+        this.prefix = ANY;
+        this.version = ANY;
+        this.goal = ANY;
+        this.executionId = ANY;
+    }
+
+    private CoordinatePredicate(
+            String groupId, String artifactId, String prefix, String version, 
String goal, String executionId) {
+        this.matchAll = false;
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.prefix = prefix;
+        this.version = version;
+        this.goal = goal;
+        this.executionId = executionId;
+    }
+
+    /**
+     * Parses a coordinate predicate from a string token.
+     *
+     * <p>Supported forms:
+     * <ul>
+     *   <li>{@code *} — match all</li>
+     *   <li>{@code :A} — by artifactId only</li>
+     *   <li>{@code G:A} — by groupId:artifactId. The groupId must contain at 
least one {@code '.'} to be
+     *       recognised as a G:A form; groupIds without a dot are treated as a 
plugin prefix instead.</li>
+     *   <li>{@code P} — by prefix</li>
+     *   <li>{@code P:v:g} — by prefix + version + goal</li>
+     *   <li>{@code P:v:g@e} — by prefix + version + goal + executionId</li>
+     * </ul>
+     *
+     * @param token the filter expression token (not {@code null}, not blank)
+     * @return the parsed predicate
+     */
+    public static CoordinatePredicate parse(String token) {
+        if ("*".equals(token)) {
+            return MATCH_ALL;
+        }
+
+        if (token.startsWith(":")) {
+            // :A form — any groupId, specific artifactId, optional :v:g[@e]
+            // e.g. ":maven-enforcer-plugin" or 
":maven-enforcer-plugin:3.0.0:enforce@enforce-id"
+            String rest = token.substring(1); // remove leading ':'
+            String[] parts = rest.split(":", 3);
+            String artifactId = emptyToNull(parts[0]);
+            String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+            String goalAndExec = parts.length > 2 ? parts[2] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(ANY, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // Try to detect G:A form: the token contains ':' AND the part before 
the first ':' looks like a
+        // groupId (contains a '.' suggesting it's a Java package name like 
org.apache.maven).
+        // This distinguishes "org.apache.maven.plugins:maven-enforcer-plugin" 
from "enforcer:3.1.0:enforce".
+        // Limitation: groupIds that do not contain a '.' (e.g. 
"commons-io:commons-io") are misrouted
+        // to prefix-based matching. For such groupIds use the explicit ":A" 
form for artifactId-only
+        // matching, or the full "G:A" form only when the groupId contains at 
least one '.'.
+        int firstColon = token.indexOf(':');
+        if (firstColon > 0 && token.substring(0, firstColon).contains(".")) {
+            // G:A[:v[:g[@e]]] form
+            String[] parts = token.split(":", 4);
+            String groupId = emptyToNull(parts[0]);
+            String artifactId = parts.length > 1 ? emptyToNull(parts[1]) : 
null;
+            String version = parts.length > 2 ? emptyToNull(parts[2]) : null;
+            String goalAndExec = parts.length > 3 ? parts[3] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(groupId, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // P[:v[:g[@e]]] form — prefix-based
+        String[] parts = token.split(":", 3);
+        String prefix = emptyToNull(parts[0]);
+        String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+        String goalAndExec = parts.length > 2 ? parts[2] : null;
+        String[] ge = splitGoalExecution(goalAndExec);
+        return new CoordinatePredicate(ANY, ANY, prefix, version, ge[0], 
ge[1]);
+    }
+
+    /**
+     * Splits a {@code "goal"} or {@code "goal@executionId"} string into a 
2-element array
+     * {@code [goal, executionId]}. Either element may be {@code null}.
+     */
+    private static String[] splitGoalExecution(String goalAndExec) {
+        if (goalAndExec == null || goalAndExec.isEmpty()) {
+            return new String[] {null, null};
+        }
+        int atIdx = goalAndExec.indexOf('@');
+        if (atIdx < 0) {
+            return new String[] {emptyToNull(goalAndExec), null};
+        }
+        return new String[] {emptyToNull(goalAndExec.substring(0, atIdx)), 
emptyToNull(goalAndExec.substring(atIdx + 1))
+        };
+    }
+
+    private static String emptyToNull(String s) {
+        return (s == null || s.isEmpty()) ? null : s;
+    }
+
+    @Override
+    public boolean matches(MojoExecution execution) {
+        if (matchAll) {
+            return true;
+        }
+
+        PluginDescriptor descriptor = execution.getPlugin().getDescriptor();
+
+        // prefix-based matching
+        if (prefix != null) {
+            String execPrefix = descriptor != null ? 
descriptor.getGoalPrefix() : null;
+            if (!prefix.equals(execPrefix)) {
+                return false;
+            }
+        } else {
+            // coordinate-based matching
+            if (groupId != null) {
+                String execGroupId = descriptor != null ? 
descriptor.getGroupId() : null;
+                if (!groupId.equals(execGroupId)) {
+                    return false;
+                }
+            }
+            if (artifactId != null) {
+                String execArtifactId = descriptor != null ? 
descriptor.getArtifactId() : null;
+                if (!artifactId.equals(execArtifactId)) {
+                    return false;
+                }
+            }
+        }
+
+        if (version != null) {
+            String execVersion = descriptor != null ? descriptor.getVersion() 
: null;
+            if (!version.equals(execVersion)) {
+                return false;
+            }
+        }
+        if (goal != null && !goal.equals(execution.getGoal())) {

Review Comment:
   ⚠️ **Compilation error:** `getGoal()` does not exist on 
`org.apache.maven.api.MojoExecution`. Use `goal()`.
   
   ```suggestion
           if (goal != null && !goal.equals(execution.goal())) {
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/CoordinatePredicate.java:
##########
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.filter;
+
+import org.apache.maven.api.MojoExecution;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * A {@link FilterPredicate} that matches {@link MojoExecution}s by plugin 
coordinate or prefix.
+ *
+ * <h2>Syntax: {@code ([G[:A]]|P)[:v][:g[@e]]}</h2>
+ *
+ * <p>The {@code @} separator for execution ID is compatible with Maven's 
existing
+ * {@code plugin:version:goal@executionId} notation used in
+ * {@code DefaultLifecycleExecutionPlanCalculator} for goal tasks.
+ *
+ * <p>Matching uses {@link MojoExecution#getDescriptor()} for goal-level 
fields and
+ * {@link MojoExecution#getPlugin()} for plugin-level coordinates (groupId, 
artifactId, version,
+ * goal prefix).
+ *
+ * <p>Forms:
+ * <ul>
+ *   <li>{@code *} — matches every mojo execution</li>
+ *   <li>{@code :A} — any groupId, specific artifactId (e.g. {@code 
:maven-enforcer-plugin})</li>
+ *   <li>{@code G:A} — exact groupId:artifactId (e.g. {@code 
org.apache.maven.plugins:maven-enforcer-plugin}).
+ *       The groupId <strong>must contain at least one {@code '.'}</strong> 
for this form to be recognised;
+ *       groupIds without a dot (e.g. {@code commons-io}) are routed to 
prefix-based matching instead.
+ *       Use the {@code :A} form to match by artifactId only when the groupId 
has no dot.</li>
+ *   <li>{@code P} — plugin prefix (e.g. {@code enforcer}), resolved against 
the plugin descriptor's
+ *       goal prefix.
+ *       The prefix <strong>must not contain a {@code '.'}</strong>; prefixes 
with a dot
+ *       (e.g. {@code io.smallrye}) are routed to groupId:artifactId matching 
instead.</li>
+ *   <li>{@code P:v:g} — prefix + version + goal</li>
+ *   <li>{@code P:v:g@e} — prefix + version + goal + executionId</li>
+ * </ul>
+ *
+ * <p>When any field is {@code null} or not specified, it is treated as a 
wildcard (matches any value).
+ *
+ * @since 4.1.0
+ */
+public class CoordinatePredicate implements FilterPredicate {
+
+    /** Wildcard token — matches any value. */
+    private static final String ANY = null;
+
+    private final boolean matchAll;
+    private final String groupId; // null = any, non-null = exact match
+    private final String artifactId; // null = any, non-null = exact match
+    private final String prefix; // null = not used, non-null = match by goal 
prefix
+    private final String version; // null = any
+    private final String goal; // null = any
+    private final String executionId; // null = any
+
+    /** Wildcard predicate — matches everything. */
+    public static final CoordinatePredicate MATCH_ALL = new 
CoordinatePredicate();
+
+    private CoordinatePredicate() {
+        this.matchAll = true;
+        this.groupId = ANY;
+        this.artifactId = ANY;
+        this.prefix = ANY;
+        this.version = ANY;
+        this.goal = ANY;
+        this.executionId = ANY;
+    }
+
+    private CoordinatePredicate(
+            String groupId, String artifactId, String prefix, String version, 
String goal, String executionId) {
+        this.matchAll = false;
+        this.groupId = groupId;
+        this.artifactId = artifactId;
+        this.prefix = prefix;
+        this.version = version;
+        this.goal = goal;
+        this.executionId = executionId;
+    }
+
+    /**
+     * Parses a coordinate predicate from a string token.
+     *
+     * <p>Supported forms:
+     * <ul>
+     *   <li>{@code *} — match all</li>
+     *   <li>{@code :A} — by artifactId only</li>
+     *   <li>{@code G:A} — by groupId:artifactId. The groupId must contain at 
least one {@code '.'} to be
+     *       recognised as a G:A form; groupIds without a dot are treated as a 
plugin prefix instead.</li>
+     *   <li>{@code P} — by prefix</li>
+     *   <li>{@code P:v:g} — by prefix + version + goal</li>
+     *   <li>{@code P:v:g@e} — by prefix + version + goal + executionId</li>
+     * </ul>
+     *
+     * @param token the filter expression token (not {@code null}, not blank)
+     * @return the parsed predicate
+     */
+    public static CoordinatePredicate parse(String token) {
+        if ("*".equals(token)) {
+            return MATCH_ALL;
+        }
+
+        if (token.startsWith(":")) {
+            // :A form — any groupId, specific artifactId, optional :v:g[@e]
+            // e.g. ":maven-enforcer-plugin" or 
":maven-enforcer-plugin:3.0.0:enforce@enforce-id"
+            String rest = token.substring(1); // remove leading ':'
+            String[] parts = rest.split(":", 3);
+            String artifactId = emptyToNull(parts[0]);
+            String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+            String goalAndExec = parts.length > 2 ? parts[2] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(ANY, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // Try to detect G:A form: the token contains ':' AND the part before 
the first ':' looks like a
+        // groupId (contains a '.' suggesting it's a Java package name like 
org.apache.maven).
+        // This distinguishes "org.apache.maven.plugins:maven-enforcer-plugin" 
from "enforcer:3.1.0:enforce".
+        // Limitation: groupIds that do not contain a '.' (e.g. 
"commons-io:commons-io") are misrouted
+        // to prefix-based matching. For such groupIds use the explicit ":A" 
form for artifactId-only
+        // matching, or the full "G:A" form only when the groupId contains at 
least one '.'.
+        int firstColon = token.indexOf(':');
+        if (firstColon > 0 && token.substring(0, firstColon).contains(".")) {
+            // G:A[:v[:g[@e]]] form
+            String[] parts = token.split(":", 4);
+            String groupId = emptyToNull(parts[0]);
+            String artifactId = parts.length > 1 ? emptyToNull(parts[1]) : 
null;
+            String version = parts.length > 2 ? emptyToNull(parts[2]) : null;
+            String goalAndExec = parts.length > 3 ? parts[3] : null;
+            String[] ge = splitGoalExecution(goalAndExec);
+            return new CoordinatePredicate(groupId, artifactId, ANY, version, 
ge[0], ge[1]);
+        }
+
+        // P[:v[:g[@e]]] form — prefix-based
+        String[] parts = token.split(":", 3);
+        String prefix = emptyToNull(parts[0]);
+        String version = parts.length > 1 ? emptyToNull(parts[1]) : null;
+        String goalAndExec = parts.length > 2 ? parts[2] : null;
+        String[] ge = splitGoalExecution(goalAndExec);
+        return new CoordinatePredicate(ANY, ANY, prefix, version, ge[0], 
ge[1]);
+    }
+
+    /**
+     * Splits a {@code "goal"} or {@code "goal@executionId"} string into a 
2-element array
+     * {@code [goal, executionId]}. Either element may be {@code null}.
+     */
+    private static String[] splitGoalExecution(String goalAndExec) {
+        if (goalAndExec == null || goalAndExec.isEmpty()) {
+            return new String[] {null, null};
+        }
+        int atIdx = goalAndExec.indexOf('@');
+        if (atIdx < 0) {
+            return new String[] {emptyToNull(goalAndExec), null};
+        }
+        return new String[] {emptyToNull(goalAndExec.substring(0, atIdx)), 
emptyToNull(goalAndExec.substring(atIdx + 1))
+        };
+    }
+
+    private static String emptyToNull(String s) {
+        return (s == null || s.isEmpty()) ? null : s;
+    }
+
+    @Override
+    public boolean matches(MojoExecution execution) {
+        if (matchAll) {
+            return true;
+        }
+
+        PluginDescriptor descriptor = execution.getPlugin().getDescriptor();
+
+        // prefix-based matching
+        if (prefix != null) {
+            String execPrefix = descriptor != null ? 
descriptor.getGoalPrefix() : null;
+            if (!prefix.equals(execPrefix)) {
+                return false;
+            }
+        } else {
+            // coordinate-based matching
+            if (groupId != null) {
+                String execGroupId = descriptor != null ? 
descriptor.getGroupId() : null;
+                if (!groupId.equals(execGroupId)) {
+                    return false;
+                }
+            }
+            if (artifactId != null) {
+                String execArtifactId = descriptor != null ? 
descriptor.getArtifactId() : null;
+                if (!artifactId.equals(execArtifactId)) {
+                    return false;
+                }
+            }
+        }
+
+        if (version != null) {
+            String execVersion = descriptor != null ? descriptor.getVersion() 
: null;
+            if (!version.equals(execVersion)) {
+                return false;
+            }
+        }
+        if (goal != null && !goal.equals(execution.getGoal())) {
+            return false;
+        }
+        if (executionId != null && 
!executionId.equals(execution.getExecutionId())) {

Review Comment:
   ⚠️ **Compilation error:** `getExecutionId()` does not exist. The API method 
is `executionId()` returning `Optional<String>`.
   
   ```suggestion
           if (executionId != null && 
!executionId.equals(execution.executionId().orElse(null))) {
   ```



##########
its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng12535LifecycleFilterPhaseTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that {@code -Dmaven.lifecycle.filter=phase(test)} skips the {@code 
test} phase,
+ * while running the same lifecycle without the filter executes tests normally.
+ *
+ * @see <a href="https://github.com/apache/maven/issues/12535";>MNG-12535</a>
+ */
+class MavenITmng12535LifecycleFilterPhaseTest extends 
AbstractMavenIntegrationTestCase {

Review Comment:
   💡 All existing IT classes in the suite use `public class`. This should be 
`public class` to match the convention.
   
   ```suggestion
   public class MavenITmng12535LifecycleFilterPhaseTest extends 
AbstractMavenIntegrationTestCase {
   ```



##########
its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng12536LifecycleFilterCoordinateTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that {@code maven.lifecycle.filter} can target plugins by 
artifactId ({@code :A})
+ * and by goal prefix.
+ *
+ * @see <a href="https://github.com/apache/maven/issues/12536";>MNG-12536</a>
+ */
+class MavenITmng12536LifecycleFilterCoordinateTest extends 
AbstractMavenIntegrationTestCase {

Review Comment:
   💡 Same — should be `public class`.
   
   ```suggestion
   public class MavenITmng12536LifecycleFilterCoordinateTest extends 
AbstractMavenIntegrationTestCase {
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/MojoExecutionFilter.java:
##########
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.filter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.MojoExecution;
+import org.apache.maven.internal.impl.DefaultMojoExecution;
+
+/**
+ * Parses the {@code maven.lifecycle.filter} user property value into a list 
of {@link FilterPredicate}s,
+ * and applies them at mojo execution time in {@code MojoExecutor}.
+ *
+ * <p>The property value is a comma-separated list of predicates, OR-ed 
together:
+ * a mojo execution matching <em>any</em> predicate is skipped.
+ *
+ * <p>Supported predicate forms:
+ * <ul>
+ *   <li>{@code *} — skip all mojo executions</li>
+ *   <li>{@code :A} — skip by artifactId (e.g. {@code 
:maven-enforcer-plugin})</li>
+ *   <li>{@code G:A} — skip by groupId:artifactId</li>
+ *   <li>{@code P} — skip by plugin prefix (e.g. {@code enforcer})</li>
+ *   <li>{@code P:v:g} — skip by prefix:version:goal</li>
+ *   <li>{@code P:v:g@e} — skip by prefix:version:goal@executionId</li>
+ *   <li>{@code phase(name)} — skip all mojos bound to the named phase</li>
+ * </ul>
+ *
+ * @since 4.1.0
+ */
+public class MojoExecutionFilter {
+
+    /** The name of the user property that activates the filter. */
+    public static final String PROPERTY_NAME = 
Constants.MAVEN_LIFECYCLE_FILTER;
+
+    private MojoExecutionFilter() {
+        // utility class
+    }
+
+    /**
+     * Parses a filter expression string into a list of {@link 
FilterPredicate}s.
+     *
+     * @param expression the comma-separated filter expression (may be {@code 
null} or blank)
+     * @return list of parsed predicates; empty if the expression is absent or 
blank
+     */
+    public static List<FilterPredicate> parse(String expression) {
+        if (expression == null || expression.isBlank()) {
+            return List.of();
+        }
+        List<FilterPredicate> predicates = new ArrayList<>();
+        for (String token : expression.split(",")) {
+            token = token.strip();
+            if (token.isEmpty()) {
+                continue;
+            }
+            predicates.add(parseToken(token));
+        }
+        return List.copyOf(predicates);
+    }
+
+    private static FilterPredicate parseToken(String token) {
+        if (token.startsWith("phase(")) {
+            if (!token.endsWith(")")) {
+                throw new IllegalArgumentException("Invalid phase() predicate 
'" + token + "': missing closing ')'");
+            }
+            String phaseName = token.substring("phase(".length(), 
token.length() - 1);
+            if (phaseName.isBlank()) {
+                throw new IllegalArgumentException("phase() predicate requires 
a non-blank phase name");
+            }
+            if (phaseName.contains(")")) {
+                throw new IllegalArgumentException(
+                        "Invalid phase() predicate '" + token + "': phase name 
must not contain ')'");
+            }
+            return new PhasePredicate(phaseName);
+        }
+        return CoordinatePredicate.parse(token);
+    }
+
+    /**
+     * Returns {@code true} if the given legacy mojo execution matches any 
predicate in the list
+     * and should be skipped.
+     *
+     * <p>Uses {@link DefaultMojoExecution} with a {@code null} session: only 
string fields
+     * ({@link MojoExecution#getDescriptor()}, {@link 
MojoExecution#getGoal()}, etc.) are accessed
+     * by the predicates.
+     *
+     * @param execution  the legacy mojo execution to test
+     * @param predicates the predicates to apply (OR-ed)
+     * @return {@code true} if the execution should be skipped
+     */
+    public static boolean matches(org.apache.maven.plugin.MojoExecution 
execution, List<FilterPredicate> predicates) {
+        if (predicates.isEmpty()) {
+            return false;
+        }
+        MojoExecution apiExecution = new DefaultMojoExecution(null, execution);

Review Comment:
   ⚠️ **NPE on every filter invocation:** `new DefaultMojoExecution(null, 
execution)` will throw immediately — the constructor does 
`requireNonNull(session, "session")`.
   
   Options: (a) pass the real `InternalMavenSession` from `MojoExecutor` 
(accessible via `(InternalMavenSession) 
session.get(InternalMavenSession.class)`) down through 
`resolveFilterPredicates()` into `matches()`, or (b) make the predicates 
operate directly on `org.apache.maven.plugin.MojoExecution` (the legacy type 
already available in `MojoExecutor`) to avoid the wrapping entirely.



##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/PhasePredicate.java:
##########
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.lifecycle.internal.filter;
+
+import org.apache.maven.api.MojoExecution;
+
+/**
+ * A {@link FilterPredicate} that matches all {@link MojoExecution}s bound to 
a specific lifecycle phase.
+ *
+ * <p>Syntax in {@code -Dmaven.lifecycle.filter}: {@code phase(<phaseName>)}
+ *
+ * <p>Example: {@code -Dmaven.lifecycle.filter=phase(test)} removes all mojos 
bound to the {@code test} phase.
+ *
+ * @since 4.1.0
+ */
+public class PhasePredicate implements FilterPredicate {
+
+    private final String phaseName;
+
+    public PhasePredicate(String phaseName) {
+        this.phaseName = phaseName;
+    }
+
+    @Override
+    public boolean matches(MojoExecution execution) {
+        return phaseName.equals(execution.getLifecyclePhase());

Review Comment:
   ⚠️ **Compilation error:** `getLifecyclePhase()` does not exist. The API 
method is `lifecyclePhase()` returning `Optional<String>`.
   
   ```suggestion
           return 
execution.lifecyclePhase().map(phaseName::equals).orElse(false);
   ```



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