gnodet-bot commented on code in PR #13392: URL: https://github.com/apache/maven/pull/13392#discussion_r4277425246
########## impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/FilterPredicate.java: ########## @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * Predicate for filtering {@link MojoExecution} entries from the lifecycle build plan. Review Comment: 📝 **Inaccurate class-level Javadoc** `"from the lifecycle build plan"` implies this interface is applied during build plan construction. It isn't — filtering happens at execution time inside `MojoExecutor.execute()`. The next sentence already says `"at runtime"` (line 26), making the opening sentence contradictory. ```suggestion * Predicate for filtering {@link MojoExecution} entries at execution time. ``` ########## impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/FilterPredicate.java: ########## @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * Predicate for filtering {@link MojoExecution} entries from the lifecycle build plan. + * + * <p>Used by {@code -Dmaven.lifecycle.filter} to skip matching executions at runtime. + * New predicate types can be added without breaking existing filter expressions. + * + * @see MojoExecutionFilter + * @since 4.1.0 + */ +public interface FilterPredicate { + + /** + * Returns {@code true} if the given mojo execution should be <em>excluded</em> from the build plan. + * + * @param execution the mojo execution to test + * @return {@code true} to remove this execution from the plan, {@code false} to keep it Review Comment: 📝 **Inaccurate method Javadoc** `"excluded from the build plan"` / `"remove this execution from the plan"` — same issue: executions aren't removed from the plan, they are skipped at runtime when `MojoExecutor` decides to skip them. ```suggestion * Returns {@code true} if the given mojo execution should be <em>skipped</em> at execution time. * * @param execution the mojo execution to test * @return {@code true} to skip this execution, {@code false} to execute it normally ``` ########## impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/filter/MojoExecutionFilter.java: ########## @@ -0,0 +1,217 @@ +/* + * 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.Collections; +import java.util.List; +import java.util.Optional; + +import org.apache.maven.api.Artifact; +import org.apache.maven.api.Constants; +import org.apache.maven.api.Dependency; +import org.apache.maven.api.MojoExecution; +import org.apache.maven.api.Plugin; +import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.model.PluginExecution; +import org.apache.maven.api.plugin.descriptor.PluginDescriptor; +import org.apache.maven.api.plugin.descriptor.lifecycle.Lifecycle; +import org.apache.maven.api.xml.XmlNode; + +/** + * 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>Adapts the legacy {@link org.apache.maven.plugin.MojoExecution} into a lightweight + * {@link MojoExecution} view — only the fields accessed by the predicates + * ({@code plugin()}, {@code goal()}, {@code executionId()}, {@code lifecyclePhase()}) + * are populated. No session is required. + * + * @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 = adapt(execution); + for (FilterPredicate predicate : predicates) { + if (predicate.matches(apiExecution)) { + return true; + } + } + return false; + } + + /** + * Adapts a legacy {@link org.apache.maven.plugin.MojoExecution} to the API + * {@link MojoExecution} interface using only the fields needed by the predicates. + * No session is required. + */ + private static MojoExecution adapt(org.apache.maven.plugin.MojoExecution legacy) { + org.apache.maven.plugin.descriptor.MojoDescriptor legacyMojo = legacy.getMojoDescriptor(); + org.apache.maven.plugin.descriptor.PluginDescriptor legacyPlugin = + legacyMojo != null ? legacyMojo.getPluginDescriptor() : null; + PluginDescriptor descriptorV4 = legacyPlugin != null ? legacyPlugin.getPluginDescriptorV4() : null; + + Plugin plugin = new Plugin() { + @Override + public org.apache.maven.api.model.Plugin getModel() { + return null; + } + + @Override + @Nullable + public PluginDescriptor getDescriptor() { + return descriptorV4; + } + + @Override + public List<Lifecycle> getLifecycles() { + return Collections.emptyList(); + } + + @Override + public ClassLoader getClassLoader() { + return null; Review Comment: ⚠️ **`@Nonnull` contract violated: `Plugin.getClassLoader()` returns `null`** `Plugin.getClassLoader()` is declared `@Nonnull` in the `Plugin` interface. The anonymous implementation here returns `null` unconditionally, violating the contract. While no current filter predicate calls `getClassLoader()` (predicates only call `getDescriptor()`), the violation still exposes the class to misuse if any code path ever passes this adapter to a non-predicate consumer. The correct approach for a stub-only adapter is to throw `UnsupportedOperationException` — this makes the violation loud rather than silent, and stops misuse at the call site instead of producing a delayed NPE. ```suggestion throw new UnsupportedOperationException("getClassLoader() is not available in the filter adapter"); ``` -- 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]
