gnodet-bot commented on code in PR #13658: URL: https://github.com/apache/maven/pull/13658#discussion_r4329675413
########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) Review Comment: ⚠️ **`RELEASE_17` is too low for Maven 4.** When plugin authors compile with `-source 21` (or higher), javac emits: *"Supported source version 'RELEASE_17' from annotation processor 'MojoDescriptorProcessor' less than -source '21'"*. This warning will pollute every plugin author's build output. Use `RELEASE_21` at minimum, or override `getSupportedSourceVersion()` to return `SourceVersion.latestSupported()` for forward compatibility. ```suggestion @SupportedSourceVersion(SourceVersion.RELEASE_21) ``` ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); + } catch (Exception e) { + logError("Failed to generate plugin descriptor", e); + } + } + + return false; // do not claim the annotation + } + + // ------------------------------------------------------------------------- + // Option validation + // ------------------------------------------------------------------------- + + private void validateOptions() { + boolean ok = true; + for (String opt : new String[] {OPT_GROUP_ID, OPT_ARTIFACT_ID, OPT_VERSION}) { + if (getOption(opt) == null) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "Required compiler option -A" + opt + " is missing. " + + "Add it to the maven-compiler-plugin configuration."); + ok = false; + } + } + if (!ok) { + throw new IllegalStateException("Missing required compiler options"); + } Review Comment: 🐛 **Double error reporting.** `validateOptions()` calls `printMessage(ERROR)` for each missing option (lines 174–180), then throws `IllegalStateException` (line 184). The ISE is caught by the broad `catch (Exception e)` at line 158, which calls `logError()` — emitting a **second** `Diagnostic.Kind.ERROR` with a redundant stack trace. Users see both the specific "missing option" messages and a confusing generic failure. Fix: return a boolean from `validateOptions()` and guard generation on its result — no throw needed. ```suggestion if (!ok) { return; // errors already reported via printMessage } } ``` ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); Review Comment: 🐛 **Factories generated during `processingOver()` are invisible to `DiIndexProcessor`.** `generateFactories()` calls `Filer.createSourceFile()` inside the `processingOver()` block. Source files created in the final round do **not** trigger another annotation processing round — the spec (JSR 269 §3.4) does not guarantee additional rounds from the final round. `DiIndexProcessor` (in `maven-api-di`) collects `@Named` classes via `roundEnv.getElementsAnnotatedWith(Named.class)` in each non-final round. Since no additional round fires after `processingOver()`, `DiIndexProcessor` will never see the generated factory classes, and `META-INF/maven/org.apache.maven.api.di.Inject` will be incomplete. The generated factories won't be discoverable by the DI container at runtime. Fix: generate factory source files in a **non-final round** (e.g. immediately after collecting mojos in round 1), and defer only `writePluginXml()` to `processingOver()`. A flag like `boolean factoriesGenerated` avoids double-generation across rounds. ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); + } catch (Exception e) { Review Comment: 💡 **Broad `catch (Exception e)` swallows programming errors.** `IOException` and `XMLStreamException` are the only checked exceptions thrown by `writePluginXml()` and `generateFactories()`. Catching `Exception` also swallows `NullPointerException`, `ClassCastException`, and other bugs, reporting them as "Failed to generate plugin descriptor" — making processor bugs very hard to diagnose. Catch `IOException | XMLStreamException` specifically. ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); + } catch (Exception e) { + logError("Failed to generate plugin descriptor", e); + } + } + + return false; // do not claim the annotation + } + + // ------------------------------------------------------------------------- + // Option validation + // ------------------------------------------------------------------------- + + private void validateOptions() { + boolean ok = true; + for (String opt : new String[] {OPT_GROUP_ID, OPT_ARTIFACT_ID, OPT_VERSION}) { + if (getOption(opt) == null) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "Required compiler option -A" + opt + " is missing. " + + "Add it to the maven-compiler-plugin configuration."); + ok = false; + } + } + if (!ok) { + throw new IllegalStateException("Missing required compiler options"); + } + } + + private String getOption(String key) { + return processingEnv.getOptions().get(key); + } + + private String getOptionOrDefault(String key, String defaultValue) { + String v = getOption(key); + return (v != null && !v.isEmpty()) ? v : defaultValue; + } + + // ------------------------------------------------------------------------- + // plugin.xml generation + // ------------------------------------------------------------------------- + + private void writePluginXml() throws IOException, XMLStreamException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + String goalPrefix = getOptionOrDefault(OPT_GOAL_PREFIX, deriveGoalPrefix(artifactId)); + + FileObject out = + processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/maven/plugin.xml"); + + try (Writer w = out.openWriter()) { + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter xml = factory.createXMLStreamWriter(w); + try { + xml.writeStartDocument("UTF-8", "1.0"); + xml.writeCharacters("\n"); + xml.writeStartElement("plugin"); + xml.writeDefaultNamespace(PLUGIN_NS); + xml.writeCharacters("\n"); + + writeElement(xml, "name", artifactId, 2); + writeElement(xml, "groupId", groupId, 2); + writeElement(xml, "artifactId", artifactId, 2); + writeElement(xml, "version", version, 2); + writeElement(xml, "goalPrefix", goalPrefix, 2); + + xml.writeCharacters(" "); + xml.writeStartElement("mojos"); + xml.writeCharacters("\n"); + + for (TypeElement mojoType : mojoElements.values()) { + writeMojo(xml, mojoType, groupId, artifactId, version); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojos + xml.writeCharacters("\n"); + xml.writeEndElement(); // plugin + xml.writeCharacters("\n"); + xml.writeEndDocument(); + } finally { + xml.close(); + } + } + note("Generated META-INF/maven/plugin.xml with " + mojoElements.size() + " mojo(s)"); + } + + private void writeMojo(XMLStreamWriter xml, TypeElement mojoType, String groupId, String artifactId, String version) + throws XMLStreamException { + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + return; + } + + String fqn = mojoType.getQualifiedName().toString(); + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Mojo.name() must not be empty", mojoType); + return; + } + + // Javadoc for the mojo class + JavadocInfo mojoDoc = parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType)); + + xml.writeCharacters(" "); + xml.writeStartElement("mojo"); + xml.writeCharacters("\n"); + + writeElement(xml, "goal", goal, 6); + writeElementIfNotEmpty(xml, "phase", getStringValue(mojoAnn, "defaultPhase", ""), 6); + writeElement(xml, "implementation", fqn, 6); + writeElement(xml, "language", "java", 6); + writeElementIfNotEmpty(xml, "description", mojoDoc.description, 6); + writeElementIfNotEmpty(xml, "since", mojoDoc.since, 6); + writeElementIfNotEmpty(xml, "deprecated", mojoDoc.deprecated, 6); + writeElement(xml, "projectRequired", String.valueOf(getBooleanValue(mojoAnn, "projectRequired", true)), 6); + writeElement(xml, "onlineRequired", String.valueOf(getBooleanValue(mojoAnn, "onlineRequired", false)), 6); + writeElement(xml, "aggregator", String.valueOf(getBooleanValue(mojoAnn, "aggregator", false)), 6); + writeElementIfNotEmpty(xml, "configurator", getStringValue(mojoAnn, "configurator", ""), 6); + + // @Execute + AnnotationMirror executeAnn = findAnnotation(mojoType, EXECUTE_ANN); + if (executeAnn != null) { + writeElementIfNotEmpty(xml, "executePhase", getStringValue(executeAnn, "phase", ""), 6); + writeElementIfNotEmpty(xml, "executeGoal", getStringValue(executeAnn, "goal", ""), 6); + writeElementIfNotEmpty(xml, "executeLifecycle", getStringValue(executeAnn, "lifecycle", ""), 6); + } + + // dependencyResolution / dependencyCollection from @Mojo + writeElementIfNotEmpty( + xml, "dependencyResolution", getStringValue(mojoAnn, "dependencyResolutionPathScopes", ""), 6); + boolean depCollection = getBooleanValue(mojoAnn, "dependencyCollection", false); + if (depCollection) { + writeElement(xml, "dependencyCollection", "true", 6); + } + + // @Parameter fields — walk the full type hierarchy + List<ParameterInfo> params = collectParameters(mojoType); + if (!params.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("parameters"); + xml.writeCharacters("\n"); + for (ParameterInfo p : params) { + writeParameter(xml, p); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // parameters + xml.writeCharacters("\n"); + } + + // @Resolution fields + List<ResolutionInfo> resolutions = collectResolutions(mojoType); + if (!resolutions.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("resolutions"); + xml.writeCharacters("\n"); + for (ResolutionInfo r : resolutions) { + writeResolution(xml, r); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // resolutions + xml.writeCharacters("\n"); + } + + // @After / @Afters + List<AfterInfo> afters = collectAfters(mojoType); + if (!afters.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("afterLinks"); + xml.writeCharacters("\n"); + for (AfterInfo a : afters) { + writeAfterLink(xml, a); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLinks + xml.writeCharacters("\n"); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojo + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Parameter collection + // ------------------------------------------------------------------------- + + private List<ParameterInfo> collectParameters(TypeElement mojoType) { + List<ParameterInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD && enclosed.getKind() != ElementKind.METHOD) { + continue; + } + AnnotationMirror paramAnn = findAnnotation(enclosed, PARAMETER_ANN); + if (paramAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + String paramName = getStringValue(paramAnn, "name", ""); + if (paramName.isEmpty()) { + paramName = fieldName; + } + if (seen.contains(paramName)) { + continue; // subclass overrides parent + } + seen.add(paramName); + + String type; + if (enclosed.getKind() == ElementKind.FIELD) { + type = ((VariableElement) enclosed).asType().toString(); + } else { + // method: use parameter type of setter, or return type of getter + ExecutableElement method = (ExecutableElement) enclosed; + if (!method.getParameters().isEmpty()) { + type = method.getParameters().get(0).asType().toString(); + } else { + type = method.getReturnType().toString(); + } + } + + JavadocInfo doc = parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed)); + + ParameterInfo p = new ParameterInfo(); + p.name = paramName; + p.alias = getStringValue(paramAnn, "alias", ""); + p.type = type; + p.required = getBooleanValue(paramAnn, "required", false); + p.editable = !getBooleanValue(paramAnn, "readonly", false); // inverted! + p.expression = getStringValue(paramAnn, "property", ""); + if (!p.expression.isEmpty() && !p.expression.startsWith("${")) { + p.expression = "${" + p.expression + "}"; + } + p.defaultValue = getStringValue(paramAnn, "defaultValue", ""); + p.description = doc.description; + p.since = doc.since; + p.deprecated = doc.deprecated; + result.add(p); + } + current = superTypeElement(current); + } + return result; + } + + private void writeParameter(XMLStreamWriter xml, ParameterInfo p) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("parameter"); + xml.writeCharacters("\n"); + writeElement(xml, "name", p.name, 10); + writeElementIfNotEmpty(xml, "alias", p.alias, 10); + writeElement(xml, "type", p.type, 10); + writeElement(xml, "required", String.valueOf(p.required), 10); + writeElement(xml, "editable", String.valueOf(p.editable), 10); + writeElementIfNotEmpty(xml, "description", p.description, 10); + writeElementIfNotEmpty(xml, "expression", p.expression, 10); + writeElementIfNotEmpty(xml, "defaultValue", p.defaultValue, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // parameter + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Resolution collection + // ------------------------------------------------------------------------- + + private List<ResolutionInfo> collectResolutions(TypeElement mojoType) { + List<ResolutionInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD) { + continue; + } + AnnotationMirror resAnn = findAnnotation(enclosed, RESOLUTION_ANN); + if (resAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + if (seen.contains(fieldName)) { + continue; + } + seen.add(fieldName); + + ResolutionInfo r = new ResolutionInfo(); + r.field = fieldName; + r.pathScope = getStringValue(resAnn, "pathScope", ""); + r.requestType = getStringValue(resAnn, "requestType", ""); + result.add(r); + } + current = superTypeElement(current); + } + return result; + } + + private void writeResolution(XMLStreamWriter xml, ResolutionInfo r) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("resolution"); + xml.writeCharacters("\n"); + writeElement(xml, "field", r.field, 10); + writeElementIfNotEmpty(xml, "pathScope", r.pathScope, 10); + writeElementIfNotEmpty(xml, "requestType", r.requestType, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // resolution + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // @After / @Afters collection + // ------------------------------------------------------------------------- + + private List<AfterInfo> collectAfters(TypeElement mojoType) { + List<AfterInfo> result = new ArrayList<>(); + // Check for @Afters (container) first + AnnotationMirror aftersAnn = findAnnotation(mojoType, AFTERS_ANN); + if (aftersAnn != null) { + AnnotationValue valueAttr = getAnnotationValue(aftersAnn, "value"); + if (valueAttr != null && valueAttr.getValue() instanceof List<?> list) { + for (Object item : list) { + if (item instanceof AnnotationValue av && av.getValue() instanceof AnnotationMirror afterMirror) { + result.add(extractAfter(afterMirror)); + } + } + } + } + // Also check for a single @After directly on the class + AnnotationMirror afterAnn = findAnnotation(mojoType, AFTER_ANN); + if (afterAnn != null && aftersAnn == null) { + result.add(extractAfter(afterAnn)); + } + return result; + } + + private AfterInfo extractAfter(AnnotationMirror ann) { + AfterInfo a = new AfterInfo(); + a.phase = getStringValue(ann, "phase", ""); + a.type = getEnumValue(ann, "type", "PROJECT"); + a.scope = getStringValue(ann, "scope", ""); + return a; + } + + private void writeAfterLink(XMLStreamWriter xml, AfterInfo a) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("afterLink"); + xml.writeCharacters("\n"); + writeElementIfNotEmpty(xml, "phase", a.phase, 10); + writeElement(xml, "type", a.type, 10); + writeElementIfNotEmpty(xml, "scope", a.scope, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLink + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Factory class generation + // ------------------------------------------------------------------------- + + private void generateFactories() throws IOException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + + for (Map.Entry<String, TypeElement> entry : mojoElements.entrySet()) { + TypeElement mojoType = entry.getValue(); + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + continue; + } + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + continue; + } + + String fqn = mojoType.getQualifiedName().toString(); + String pkg = fqn.contains(".") ? fqn.substring(0, fqn.lastIndexOf('.')) : ""; + String simpleName = mojoType.getSimpleName().toString(); + String factorySimpleName = simpleName + "$MojoFactory"; + String factoryFqn = pkg.isEmpty() ? factorySimpleName : pkg + "." + factorySimpleName; + String roleHint = groupId + ":" + artifactId + ":" + version + ":" + goal; + + // Collect non-private constructors to replicate in the factory + List<ExecutableElement> ctors = new ArrayList<>(); + for (Element enc : mojoType.getEnclosedElements()) { + if (enc.getKind() == ElementKind.CONSTRUCTOR) { + ExecutableElement ctor = (ExecutableElement) enc; + if (!ctor.getModifiers().contains(Modifier.PRIVATE)) { + ctors.add(ctor); + } + } + } + + JavaFileObject src = processingEnv.getFiler().createSourceFile(factoryFqn, mojoType); + try (PrintWriter pw = new PrintWriter(src.openWriter())) { + writeLicenseHeader(pw); + if (!pkg.isEmpty()) { + pw.println("package " + pkg + ";"); + pw.println(); + } + pw.println("/**"); + pw.println(" * DI factory for the {@code " + goal + "} mojo."); + pw.println(" * Generated by {@link org.apache.maven.plugin.processor.MojoDescriptorProcessor}."); + pw.println(" *"); + pw.println(" * @since 4.1.0"); + pw.println(" */"); + pw.println("@org.apache.maven.api.di.Named(\"" + roleHint + "\")"); + pw.println("public class " + factorySimpleName + " extends " + simpleName + " {"); + pw.println(); + + if (ctors.isEmpty() || hasOnlyDefaultCtor(ctors)) { + // Nothing to generate — implicit default constructor delegates to super + } else { + for (ExecutableElement ctor : ctors) { + if (ctor.getParameters().isEmpty()) { + continue; // default ctor — no need to replicate + } + pw.print(" public " + factorySimpleName + "("); + List<? extends VariableElement> params = ctor.getParameters(); + StringBuilder args = new StringBuilder(); + for (int i = 0; i < params.size(); i++) { + VariableElement param = params.get(i); + String typeName = param.asType().toString(); + String paramName = param.getSimpleName().toString(); + if (i > 0) { + pw.print(", "); + args.append(", "); + } + pw.print(typeName + " " + paramName); + args.append(paramName); + } + pw.println(") {"); + pw.println(" super(" + args + ");"); + pw.println(" }"); + pw.println(); + } + } + + pw.println("}"); + } + + generatedFactoryNames.add(factoryFqn); + note("Generated factory " + factoryFqn + " for goal '" + goal + "'"); + } + } + + private boolean hasOnlyDefaultCtor(List<ExecutableElement> ctors) { + return ctors.size() == 1 && ctors.get(0).getParameters().isEmpty(); + } + + // ------------------------------------------------------------------------- + // Javadoc parsing + // ------------------------------------------------------------------------- + + private static JavadocInfo parseJavadoc(String raw) { + JavadocInfo info = new JavadocInfo(); + if (raw == null || raw.isBlank()) { + return info; + } + // Split on block Javadoc tags (tags at start of a line, possibly with leading whitespace) + String[] parts = raw.split("(?m)^\\s*@"); + String body = parts[0]; + + // Strip inline tags like {@code ...}, {@link ...} etc. — replace with their text content + body = body.replaceAll("\\{@\\w+\\s+([^}]*)}", "$1"); + // Remove HTML tags + body = body.replaceAll("<[^>]+>", ""); + info.description = normalizeWhitespace(body); + + // Parse @since, @deprecated from remaining parts + for (int i = 1; i < parts.length; i++) { + String tag = parts[i].trim(); + if (tag.startsWith("since")) { + info.since = tag.substring("since".length()).trim().replaceAll("\\s+", " "); + } else if (tag.startsWith("deprecated")) { + info.deprecated = tag.substring("deprecated".length()).trim().replaceAll("\\s+", " "); + } + } + return info; + } + + private static String normalizeWhitespace(String s) { + if (s == null) { + return null; + } + String trimmed = s.strip(); + if (trimmed.isEmpty()) { + return null; + } + // Collapse internal whitespace runs to a single space + return trimmed.replaceAll("\\s+", " "); + } + + // ------------------------------------------------------------------------- + // Annotation mirror utilities + // ------------------------------------------------------------------------- + + private static AnnotationMirror findAnnotation(Element element, String annotationFqn) { + for (AnnotationMirror am : element.getAnnotationMirrors()) { + if (am.getAnnotationType().toString().equals(annotationFqn)) { + return am; + } + } + return null; + } + + private static AnnotationValue getAnnotationValue(AnnotationMirror am, String methodName) { + for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : + am.getElementValues().entrySet()) { + if (entry.getKey().getSimpleName().toString().equals(methodName)) { + return entry.getValue(); + } + } + return null; + } + + private static String getStringValue(AnnotationMirror am, String methodName, String defaultValue) { + AnnotationValue av = getAnnotationValue(am, methodName); + if (av == null) { + return defaultValue; + } + Object v = av.getValue(); + return v != null ? v.toString() : defaultValue; + } + + private static boolean getBooleanValue(AnnotationMirror am, String methodName, boolean defaultValue) { + AnnotationValue av = getAnnotationValue(am, methodName); + if (av == null) { + return defaultValue; + } + Object v = av.getValue(); + return v instanceof Boolean ? (Boolean) v : defaultValue; + } + + /** + * Returns the simple name of an enum value from an annotation attribute. + * Enum values in annotation mirrors are represented as {@link VariableElement}. + */ + private static String getEnumValue(AnnotationMirror am, String methodName, String defaultValue) { + AnnotationValue av = getAnnotationValue(am, methodName); + if (av == null) { + return defaultValue; + } + Object v = av.getValue(); + if (v instanceof VariableElement ve) { + return ve.getSimpleName().toString(); + } + return defaultValue; + } + + // ------------------------------------------------------------------------- + // Type hierarchy walking + // ------------------------------------------------------------------------- + + private TypeElement superTypeElement(TypeElement te) { + TypeMirror superMirror = te.getSuperclass(); + if (superMirror == null || superMirror.getKind() == TypeKind.NONE) { + return null; + } + if (superMirror instanceof DeclaredType dt && dt.asElement() instanceof TypeElement superType) { + return isObject(superType) ? null : superType; + } + return null; + } + + private static boolean isObject(TypeElement te) { + return "java.lang.Object".equals(te.getQualifiedName().toString()); + } + + // ------------------------------------------------------------------------- + // XML writing helpers + // ------------------------------------------------------------------------- + + private static void writeElement(XMLStreamWriter xml, String tag, String value) throws XMLStreamException { Review Comment: 🧹 **Dead code.** The 3-argument `writeElement(XMLStreamWriter, String, String)` overload (hardcoded indent=4) is never called — all 18 call sites use the explicit-indent 4-argument version. Remove it. ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); + } catch (Exception e) { + logError("Failed to generate plugin descriptor", e); + } + } + + return false; // do not claim the annotation + } + + // ------------------------------------------------------------------------- + // Option validation + // ------------------------------------------------------------------------- + + private void validateOptions() { + boolean ok = true; + for (String opt : new String[] {OPT_GROUP_ID, OPT_ARTIFACT_ID, OPT_VERSION}) { + if (getOption(opt) == null) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "Required compiler option -A" + opt + " is missing. " + + "Add it to the maven-compiler-plugin configuration."); + ok = false; + } + } + if (!ok) { + throw new IllegalStateException("Missing required compiler options"); + } + } + + private String getOption(String key) { + return processingEnv.getOptions().get(key); + } + + private String getOptionOrDefault(String key, String defaultValue) { + String v = getOption(key); + return (v != null && !v.isEmpty()) ? v : defaultValue; + } + + // ------------------------------------------------------------------------- + // plugin.xml generation + // ------------------------------------------------------------------------- + + private void writePluginXml() throws IOException, XMLStreamException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + String goalPrefix = getOptionOrDefault(OPT_GOAL_PREFIX, deriveGoalPrefix(artifactId)); + + FileObject out = + processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/maven/plugin.xml"); + + try (Writer w = out.openWriter()) { + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter xml = factory.createXMLStreamWriter(w); + try { + xml.writeStartDocument("UTF-8", "1.0"); + xml.writeCharacters("\n"); + xml.writeStartElement("plugin"); + xml.writeDefaultNamespace(PLUGIN_NS); + xml.writeCharacters("\n"); + + writeElement(xml, "name", artifactId, 2); + writeElement(xml, "groupId", groupId, 2); + writeElement(xml, "artifactId", artifactId, 2); + writeElement(xml, "version", version, 2); + writeElement(xml, "goalPrefix", goalPrefix, 2); + + xml.writeCharacters(" "); + xml.writeStartElement("mojos"); + xml.writeCharacters("\n"); + + for (TypeElement mojoType : mojoElements.values()) { + writeMojo(xml, mojoType, groupId, artifactId, version); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojos + xml.writeCharacters("\n"); + xml.writeEndElement(); // plugin + xml.writeCharacters("\n"); + xml.writeEndDocument(); + } finally { + xml.close(); + } + } + note("Generated META-INF/maven/plugin.xml with " + mojoElements.size() + " mojo(s)"); + } + + private void writeMojo(XMLStreamWriter xml, TypeElement mojoType, String groupId, String artifactId, String version) + throws XMLStreamException { + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + return; + } + + String fqn = mojoType.getQualifiedName().toString(); + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Mojo.name() must not be empty", mojoType); + return; + } + + // Javadoc for the mojo class + JavadocInfo mojoDoc = parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType)); + + xml.writeCharacters(" "); + xml.writeStartElement("mojo"); + xml.writeCharacters("\n"); + + writeElement(xml, "goal", goal, 6); + writeElementIfNotEmpty(xml, "phase", getStringValue(mojoAnn, "defaultPhase", ""), 6); + writeElement(xml, "implementation", fqn, 6); + writeElement(xml, "language", "java", 6); + writeElementIfNotEmpty(xml, "description", mojoDoc.description, 6); + writeElementIfNotEmpty(xml, "since", mojoDoc.since, 6); + writeElementIfNotEmpty(xml, "deprecated", mojoDoc.deprecated, 6); + writeElement(xml, "projectRequired", String.valueOf(getBooleanValue(mojoAnn, "projectRequired", true)), 6); + writeElement(xml, "onlineRequired", String.valueOf(getBooleanValue(mojoAnn, "onlineRequired", false)), 6); + writeElement(xml, "aggregator", String.valueOf(getBooleanValue(mojoAnn, "aggregator", false)), 6); + writeElementIfNotEmpty(xml, "configurator", getStringValue(mojoAnn, "configurator", ""), 6); + + // @Execute + AnnotationMirror executeAnn = findAnnotation(mojoType, EXECUTE_ANN); + if (executeAnn != null) { + writeElementIfNotEmpty(xml, "executePhase", getStringValue(executeAnn, "phase", ""), 6); + writeElementIfNotEmpty(xml, "executeGoal", getStringValue(executeAnn, "goal", ""), 6); + writeElementIfNotEmpty(xml, "executeLifecycle", getStringValue(executeAnn, "lifecycle", ""), 6); + } + + // dependencyResolution / dependencyCollection from @Mojo + writeElementIfNotEmpty( + xml, "dependencyResolution", getStringValue(mojoAnn, "dependencyResolutionPathScopes", ""), 6); + boolean depCollection = getBooleanValue(mojoAnn, "dependencyCollection", false); + if (depCollection) { + writeElement(xml, "dependencyCollection", "true", 6); + } + + // @Parameter fields — walk the full type hierarchy + List<ParameterInfo> params = collectParameters(mojoType); + if (!params.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("parameters"); + xml.writeCharacters("\n"); + for (ParameterInfo p : params) { + writeParameter(xml, p); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // parameters + xml.writeCharacters("\n"); + } + + // @Resolution fields + List<ResolutionInfo> resolutions = collectResolutions(mojoType); + if (!resolutions.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("resolutions"); + xml.writeCharacters("\n"); + for (ResolutionInfo r : resolutions) { + writeResolution(xml, r); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // resolutions + xml.writeCharacters("\n"); + } + + // @After / @Afters + List<AfterInfo> afters = collectAfters(mojoType); + if (!afters.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("afterLinks"); + xml.writeCharacters("\n"); + for (AfterInfo a : afters) { + writeAfterLink(xml, a); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLinks + xml.writeCharacters("\n"); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojo + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Parameter collection + // ------------------------------------------------------------------------- + + private List<ParameterInfo> collectParameters(TypeElement mojoType) { + List<ParameterInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD && enclosed.getKind() != ElementKind.METHOD) { + continue; + } + AnnotationMirror paramAnn = findAnnotation(enclosed, PARAMETER_ANN); + if (paramAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + String paramName = getStringValue(paramAnn, "name", ""); + if (paramName.isEmpty()) { + paramName = fieldName; + } + if (seen.contains(paramName)) { + continue; // subclass overrides parent + } + seen.add(paramName); + + String type; + if (enclosed.getKind() == ElementKind.FIELD) { + type = ((VariableElement) enclosed).asType().toString(); + } else { + // method: use parameter type of setter, or return type of getter + ExecutableElement method = (ExecutableElement) enclosed; + if (!method.getParameters().isEmpty()) { + type = method.getParameters().get(0).asType().toString(); + } else { + type = method.getReturnType().toString(); + } + } + + JavadocInfo doc = parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed)); + + ParameterInfo p = new ParameterInfo(); + p.name = paramName; + p.alias = getStringValue(paramAnn, "alias", ""); + p.type = type; + p.required = getBooleanValue(paramAnn, "required", false); + p.editable = !getBooleanValue(paramAnn, "readonly", false); // inverted! + p.expression = getStringValue(paramAnn, "property", ""); + if (!p.expression.isEmpty() && !p.expression.startsWith("${")) { + p.expression = "${" + p.expression + "}"; + } + p.defaultValue = getStringValue(paramAnn, "defaultValue", ""); + p.description = doc.description; + p.since = doc.since; + p.deprecated = doc.deprecated; + result.add(p); + } + current = superTypeElement(current); + } + return result; + } + + private void writeParameter(XMLStreamWriter xml, ParameterInfo p) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("parameter"); + xml.writeCharacters("\n"); + writeElement(xml, "name", p.name, 10); + writeElementIfNotEmpty(xml, "alias", p.alias, 10); + writeElement(xml, "type", p.type, 10); + writeElement(xml, "required", String.valueOf(p.required), 10); + writeElement(xml, "editable", String.valueOf(p.editable), 10); + writeElementIfNotEmpty(xml, "description", p.description, 10); + writeElementIfNotEmpty(xml, "expression", p.expression, 10); + writeElementIfNotEmpty(xml, "defaultValue", p.defaultValue, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // parameter + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Resolution collection + // ------------------------------------------------------------------------- + + private List<ResolutionInfo> collectResolutions(TypeElement mojoType) { + List<ResolutionInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD) { + continue; + } + AnnotationMirror resAnn = findAnnotation(enclosed, RESOLUTION_ANN); + if (resAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + if (seen.contains(fieldName)) { + continue; + } + seen.add(fieldName); + + ResolutionInfo r = new ResolutionInfo(); + r.field = fieldName; + r.pathScope = getStringValue(resAnn, "pathScope", ""); + r.requestType = getStringValue(resAnn, "requestType", ""); + result.add(r); + } + current = superTypeElement(current); + } + return result; + } + + private void writeResolution(XMLStreamWriter xml, ResolutionInfo r) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("resolution"); + xml.writeCharacters("\n"); + writeElement(xml, "field", r.field, 10); + writeElementIfNotEmpty(xml, "pathScope", r.pathScope, 10); + writeElementIfNotEmpty(xml, "requestType", r.requestType, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // resolution + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // @After / @Afters collection + // ------------------------------------------------------------------------- + + private List<AfterInfo> collectAfters(TypeElement mojoType) { + List<AfterInfo> result = new ArrayList<>(); + // Check for @Afters (container) first + AnnotationMirror aftersAnn = findAnnotation(mojoType, AFTERS_ANN); + if (aftersAnn != null) { + AnnotationValue valueAttr = getAnnotationValue(aftersAnn, "value"); + if (valueAttr != null && valueAttr.getValue() instanceof List<?> list) { + for (Object item : list) { + if (item instanceof AnnotationValue av && av.getValue() instanceof AnnotationMirror afterMirror) { + result.add(extractAfter(afterMirror)); + } + } + } + } + // Also check for a single @After directly on the class + AnnotationMirror afterAnn = findAnnotation(mojoType, AFTER_ANN); + if (afterAnn != null && aftersAnn == null) { + result.add(extractAfter(afterAnn)); + } + return result; + } + + private AfterInfo extractAfter(AnnotationMirror ann) { + AfterInfo a = new AfterInfo(); + a.phase = getStringValue(ann, "phase", ""); + a.type = getEnumValue(ann, "type", "PROJECT"); + a.scope = getStringValue(ann, "scope", ""); + return a; + } + + private void writeAfterLink(XMLStreamWriter xml, AfterInfo a) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("afterLink"); + xml.writeCharacters("\n"); + writeElementIfNotEmpty(xml, "phase", a.phase, 10); + writeElement(xml, "type", a.type, 10); + writeElementIfNotEmpty(xml, "scope", a.scope, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLink + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Factory class generation + // ------------------------------------------------------------------------- + + private void generateFactories() throws IOException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + + for (Map.Entry<String, TypeElement> entry : mojoElements.entrySet()) { + TypeElement mojoType = entry.getValue(); + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + continue; + } + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + continue; + } + + String fqn = mojoType.getQualifiedName().toString(); + String pkg = fqn.contains(".") ? fqn.substring(0, fqn.lastIndexOf('.')) : ""; + String simpleName = mojoType.getSimpleName().toString(); + String factorySimpleName = simpleName + "$MojoFactory"; + String factoryFqn = pkg.isEmpty() ? factorySimpleName : pkg + "." + factorySimpleName; + String roleHint = groupId + ":" + artifactId + ":" + version + ":" + goal; + + // Collect non-private constructors to replicate in the factory + List<ExecutableElement> ctors = new ArrayList<>(); + for (Element enc : mojoType.getEnclosedElements()) { + if (enc.getKind() == ElementKind.CONSTRUCTOR) { + ExecutableElement ctor = (ExecutableElement) enc; + if (!ctor.getModifiers().contains(Modifier.PRIVATE)) { + ctors.add(ctor); + } + } + } + + JavaFileObject src = processingEnv.getFiler().createSourceFile(factoryFqn, mojoType); + try (PrintWriter pw = new PrintWriter(src.openWriter())) { + writeLicenseHeader(pw); + if (!pkg.isEmpty()) { + pw.println("package " + pkg + ";"); + pw.println(); + } + pw.println("/**"); + pw.println(" * DI factory for the {@code " + goal + "} mojo."); + pw.println(" * Generated by {@link org.apache.maven.plugin.processor.MojoDescriptorProcessor}."); + pw.println(" *"); + pw.println(" * @since 4.1.0"); + pw.println(" */"); + pw.println("@org.apache.maven.api.di.Named(\"" + roleHint + "\")"); + pw.println("public class " + factorySimpleName + " extends " + simpleName + " {"); + pw.println(); + + if (ctors.isEmpty() || hasOnlyDefaultCtor(ctors)) { + // Nothing to generate — implicit default constructor delegates to super + } else { + for (ExecutableElement ctor : ctors) { + if (ctor.getParameters().isEmpty()) { + continue; // default ctor — no need to replicate + } + pw.print(" public " + factorySimpleName + "("); + List<? extends VariableElement> params = ctor.getParameters(); + StringBuilder args = new StringBuilder(); + for (int i = 0; i < params.size(); i++) { + VariableElement param = params.get(i); + String typeName = param.asType().toString(); + String paramName = param.getSimpleName().toString(); + if (i > 0) { + pw.print(", "); + args.append(", "); + } + pw.print(typeName + " " + paramName); + args.append(paramName); + } + pw.println(") {"); + pw.println(" super(" + args + ");"); + pw.println(" }"); + pw.println(); + } + } + + pw.println("}"); + } + + generatedFactoryNames.add(factoryFqn); + note("Generated factory " + factoryFqn + " for goal '" + goal + "'"); + } + } + + private boolean hasOnlyDefaultCtor(List<ExecutableElement> ctors) { + return ctors.size() == 1 && ctors.get(0).getParameters().isEmpty(); + } + + // ------------------------------------------------------------------------- + // Javadoc parsing + // ------------------------------------------------------------------------- + + private static JavadocInfo parseJavadoc(String raw) { + JavadocInfo info = new JavadocInfo(); + if (raw == null || raw.isBlank()) { + return info; + } + // Split on block Javadoc tags (tags at start of a line, possibly with leading whitespace) + String[] parts = raw.split("(?m)^\\s*@"); Review Comment: ⚠️ **Javadoc tag-splitting regex is too greedy.** `(?m)^\\s*@` splits on **any** `@` character at the start of a line, including: - Email addresses in Javadoc (`@example.com`) - Annotation names in code examples that start a line - `{@link ...}` or `{@code ...}` that happen to wrap to a new line This silently truncates mojo descriptions for any Javadoc that happens to have `@` at line start before the actual block tags. Use a stricter lookahead for known block-tag names: ```suggestion String[] parts = raw.split("(?m)^\\s*@(?=param|return|throws|since|deprecated|see|author|version)\\b"); ``` ########## api/maven-api-plugin-processor/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java: ########## @@ -0,0 +1,855 @@ +/* + * 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.plugin.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * JSR 269 annotation processor that generates Maven plugin descriptors ({@code META-INF/maven/plugin.xml}) + * and DI factory classes at compile time from {@code @Mojo}-annotated classes. + * + * <p>This processor requires the following compiler options (via {@code -A}): + * <ul> + * <li>{@code maven.plugin.groupId} — the plugin's group ID</li> + * <li>{@code maven.plugin.artifactId} — the plugin's artifact ID</li> + * <li>{@code maven.plugin.version} — the plugin's version</li> + * <li>{@code maven.plugin.goalPrefix} — optional; derived from artifactId if absent</li> + * </ul> + * + * <p>The generated {@code plugin.xml} follows the {@code http://maven.apache.org/PLUGIN/2.0.0} namespace. + * One factory class is generated per mojo; the factory is annotated with + * {@code @org.apache.maven.api.di.Named("groupId:artifactId:version:goal")} so that the DI container + * can locate it by the role-hint used in {@code DefaultMavenPluginManager.loadV4Mojo()}. + * The {@code DiIndexProcessor} (from {@code maven-api-di}) picks up the {@code @Named} factory in the + * next processing round and registers it in {@code META-INF/maven/org.apache.maven.api.di.Inject}. + * + * <p>Design notes: + * <ul> + * <li>Returns {@code false} from {@code process()} — does not claim {@code @Mojo}; other processors + * and IDEs can still see it.</li> + * <li>Annotation references are string-based — no compile dependency on {@code maven-api-core} or + * {@code maven-api-di}; mirror names are compared as strings.</li> + * <li>Full type hierarchy is walked to collect {@code @Parameter} and {@code @Resolution} fields, + * including those declared in parent classes from dependency JARs.</li> + * <li>Javadoc is extracted via {@code Elements.getDocComment()} — only available for source elements; + * inherited parameters from binary JARs will have {@code null} descriptions.</li> + * </ul> + * + * @since 4.1.0 + */ +@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({ + MojoDescriptorProcessor.OPT_GROUP_ID, + MojoDescriptorProcessor.OPT_ARTIFACT_ID, + MojoDescriptorProcessor.OPT_VERSION, + MojoDescriptorProcessor.OPT_GOAL_PREFIX +}) +public class MojoDescriptorProcessor extends AbstractProcessor { + + static final String OPT_GROUP_ID = "maven.plugin.groupId"; + static final String OPT_ARTIFACT_ID = "maven.plugin.artifactId"; + static final String OPT_VERSION = "maven.plugin.version"; + static final String OPT_GOAL_PREFIX = "maven.plugin.goalPrefix"; + + private static final String MOJO_ANN = "org.apache.maven.api.plugin.annotations.Mojo"; + private static final String PARAMETER_ANN = "org.apache.maven.api.plugin.annotations.Parameter"; + private static final String RESOLUTION_ANN = "org.apache.maven.api.plugin.annotations.Resolution"; + private static final String EXECUTE_ANN = "org.apache.maven.api.plugin.annotations.Execute"; + private static final String AFTER_ANN = "org.apache.maven.api.plugin.annotations.After"; + private static final String AFTERS_ANN = "org.apache.maven.api.plugin.annotations.Afters"; + + private static final String PLUGIN_NS = "http://maven.apache.org/PLUGIN/2.0.0"; + + /** Mojos collected across all rounds (FQN → TypeElement). */ + private final Map<String, TypeElement> mojoElements = new LinkedHashMap<>(); + + /** Factory class names generated in previous rounds — skip re-processing them. */ + private final Set<String> generatedFactoryNames = new LinkedHashSet<>(); + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (!(element instanceof TypeElement typeElement)) { + continue; + } + String fqn = typeElement.getQualifiedName().toString(); + // Skip factory classes we generated in a previous round + if (generatedFactoryNames.contains(fqn)) { + continue; + } + // Validate: must be a concrete class + if (typeElement.getKind() == ElementKind.INTERFACE) { + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, "@Mojo must not be placed on an interface", element); + continue; + } + if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, "@Mojo must not be placed on an abstract class", element); + continue; + } + mojoElements.put(fqn, typeElement); + } + } + + if (roundEnv.processingOver()) { + if (mojoElements.isEmpty()) { + return false; + } + validateOptions(); + try { + writePluginXml(); + generateFactories(); + } catch (Exception e) { + logError("Failed to generate plugin descriptor", e); + } + } + + return false; // do not claim the annotation + } + + // ------------------------------------------------------------------------- + // Option validation + // ------------------------------------------------------------------------- + + private void validateOptions() { + boolean ok = true; + for (String opt : new String[] {OPT_GROUP_ID, OPT_ARTIFACT_ID, OPT_VERSION}) { + if (getOption(opt) == null) { + processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "Required compiler option -A" + opt + " is missing. " + + "Add it to the maven-compiler-plugin configuration."); + ok = false; + } + } + if (!ok) { + throw new IllegalStateException("Missing required compiler options"); + } + } + + private String getOption(String key) { + return processingEnv.getOptions().get(key); + } + + private String getOptionOrDefault(String key, String defaultValue) { + String v = getOption(key); + return (v != null && !v.isEmpty()) ? v : defaultValue; + } + + // ------------------------------------------------------------------------- + // plugin.xml generation + // ------------------------------------------------------------------------- + + private void writePluginXml() throws IOException, XMLStreamException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + String goalPrefix = getOptionOrDefault(OPT_GOAL_PREFIX, deriveGoalPrefix(artifactId)); + + FileObject out = + processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "META-INF/maven/plugin.xml"); + + try (Writer w = out.openWriter()) { + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter xml = factory.createXMLStreamWriter(w); + try { + xml.writeStartDocument("UTF-8", "1.0"); + xml.writeCharacters("\n"); + xml.writeStartElement("plugin"); + xml.writeDefaultNamespace(PLUGIN_NS); + xml.writeCharacters("\n"); + + writeElement(xml, "name", artifactId, 2); + writeElement(xml, "groupId", groupId, 2); + writeElement(xml, "artifactId", artifactId, 2); + writeElement(xml, "version", version, 2); + writeElement(xml, "goalPrefix", goalPrefix, 2); + + xml.writeCharacters(" "); + xml.writeStartElement("mojos"); + xml.writeCharacters("\n"); + + for (TypeElement mojoType : mojoElements.values()) { + writeMojo(xml, mojoType, groupId, artifactId, version); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojos + xml.writeCharacters("\n"); + xml.writeEndElement(); // plugin + xml.writeCharacters("\n"); + xml.writeEndDocument(); + } finally { + xml.close(); + } + } + note("Generated META-INF/maven/plugin.xml with " + mojoElements.size() + " mojo(s)"); + } + + private void writeMojo(XMLStreamWriter xml, TypeElement mojoType, String groupId, String artifactId, String version) + throws XMLStreamException { + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + return; + } + + String fqn = mojoType.getQualifiedName().toString(); + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Mojo.name() must not be empty", mojoType); + return; + } + + // Javadoc for the mojo class + JavadocInfo mojoDoc = parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType)); + + xml.writeCharacters(" "); + xml.writeStartElement("mojo"); + xml.writeCharacters("\n"); + + writeElement(xml, "goal", goal, 6); + writeElementIfNotEmpty(xml, "phase", getStringValue(mojoAnn, "defaultPhase", ""), 6); + writeElement(xml, "implementation", fqn, 6); + writeElement(xml, "language", "java", 6); + writeElementIfNotEmpty(xml, "description", mojoDoc.description, 6); + writeElementIfNotEmpty(xml, "since", mojoDoc.since, 6); + writeElementIfNotEmpty(xml, "deprecated", mojoDoc.deprecated, 6); + writeElement(xml, "projectRequired", String.valueOf(getBooleanValue(mojoAnn, "projectRequired", true)), 6); + writeElement(xml, "onlineRequired", String.valueOf(getBooleanValue(mojoAnn, "onlineRequired", false)), 6); + writeElement(xml, "aggregator", String.valueOf(getBooleanValue(mojoAnn, "aggregator", false)), 6); + writeElementIfNotEmpty(xml, "configurator", getStringValue(mojoAnn, "configurator", ""), 6); + + // @Execute + AnnotationMirror executeAnn = findAnnotation(mojoType, EXECUTE_ANN); + if (executeAnn != null) { + writeElementIfNotEmpty(xml, "executePhase", getStringValue(executeAnn, "phase", ""), 6); + writeElementIfNotEmpty(xml, "executeGoal", getStringValue(executeAnn, "goal", ""), 6); + writeElementIfNotEmpty(xml, "executeLifecycle", getStringValue(executeAnn, "lifecycle", ""), 6); + } + + // dependencyResolution / dependencyCollection from @Mojo + writeElementIfNotEmpty( + xml, "dependencyResolution", getStringValue(mojoAnn, "dependencyResolutionPathScopes", ""), 6); + boolean depCollection = getBooleanValue(mojoAnn, "dependencyCollection", false); + if (depCollection) { + writeElement(xml, "dependencyCollection", "true", 6); + } + + // @Parameter fields — walk the full type hierarchy + List<ParameterInfo> params = collectParameters(mojoType); + if (!params.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("parameters"); + xml.writeCharacters("\n"); + for (ParameterInfo p : params) { + writeParameter(xml, p); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // parameters + xml.writeCharacters("\n"); + } + + // @Resolution fields + List<ResolutionInfo> resolutions = collectResolutions(mojoType); + if (!resolutions.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("resolutions"); + xml.writeCharacters("\n"); + for (ResolutionInfo r : resolutions) { + writeResolution(xml, r); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // resolutions + xml.writeCharacters("\n"); + } + + // @After / @Afters + List<AfterInfo> afters = collectAfters(mojoType); + if (!afters.isEmpty()) { + xml.writeCharacters(" "); + xml.writeStartElement("afterLinks"); + xml.writeCharacters("\n"); + for (AfterInfo a : afters) { + writeAfterLink(xml, a); + } + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLinks + xml.writeCharacters("\n"); + } + + xml.writeCharacters(" "); + xml.writeEndElement(); // mojo + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Parameter collection + // ------------------------------------------------------------------------- + + private List<ParameterInfo> collectParameters(TypeElement mojoType) { + List<ParameterInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD && enclosed.getKind() != ElementKind.METHOD) { + continue; + } + AnnotationMirror paramAnn = findAnnotation(enclosed, PARAMETER_ANN); + if (paramAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + String paramName = getStringValue(paramAnn, "name", ""); + if (paramName.isEmpty()) { + paramName = fieldName; + } + if (seen.contains(paramName)) { + continue; // subclass overrides parent + } + seen.add(paramName); + + String type; + if (enclosed.getKind() == ElementKind.FIELD) { + type = ((VariableElement) enclosed).asType().toString(); + } else { + // method: use parameter type of setter, or return type of getter + ExecutableElement method = (ExecutableElement) enclosed; + if (!method.getParameters().isEmpty()) { + type = method.getParameters().get(0).asType().toString(); + } else { + type = method.getReturnType().toString(); + } + } + + JavadocInfo doc = parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed)); + + ParameterInfo p = new ParameterInfo(); + p.name = paramName; + p.alias = getStringValue(paramAnn, "alias", ""); + p.type = type; + p.required = getBooleanValue(paramAnn, "required", false); + p.editable = !getBooleanValue(paramAnn, "readonly", false); // inverted! + p.expression = getStringValue(paramAnn, "property", ""); + if (!p.expression.isEmpty() && !p.expression.startsWith("${")) { + p.expression = "${" + p.expression + "}"; + } + p.defaultValue = getStringValue(paramAnn, "defaultValue", ""); + p.description = doc.description; + p.since = doc.since; + p.deprecated = doc.deprecated; + result.add(p); + } + current = superTypeElement(current); + } + return result; + } + + private void writeParameter(XMLStreamWriter xml, ParameterInfo p) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("parameter"); + xml.writeCharacters("\n"); + writeElement(xml, "name", p.name, 10); + writeElementIfNotEmpty(xml, "alias", p.alias, 10); + writeElement(xml, "type", p.type, 10); + writeElement(xml, "required", String.valueOf(p.required), 10); + writeElement(xml, "editable", String.valueOf(p.editable), 10); + writeElementIfNotEmpty(xml, "description", p.description, 10); + writeElementIfNotEmpty(xml, "expression", p.expression, 10); + writeElementIfNotEmpty(xml, "defaultValue", p.defaultValue, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // parameter + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Resolution collection + // ------------------------------------------------------------------------- + + private List<ResolutionInfo> collectResolutions(TypeElement mojoType) { + List<ResolutionInfo> result = new ArrayList<>(); + Set<String> seen = new LinkedHashSet<>(); + TypeElement current = mojoType; + while (current != null && !isObject(current)) { + for (Element enclosed : current.getEnclosedElements()) { + if (enclosed.getKind() != ElementKind.FIELD) { + continue; + } + AnnotationMirror resAnn = findAnnotation(enclosed, RESOLUTION_ANN); + if (resAnn == null) { + continue; + } + String fieldName = enclosed.getSimpleName().toString(); + if (seen.contains(fieldName)) { + continue; + } + seen.add(fieldName); + + ResolutionInfo r = new ResolutionInfo(); + r.field = fieldName; + r.pathScope = getStringValue(resAnn, "pathScope", ""); + r.requestType = getStringValue(resAnn, "requestType", ""); + result.add(r); + } + current = superTypeElement(current); + } + return result; + } + + private void writeResolution(XMLStreamWriter xml, ResolutionInfo r) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("resolution"); + xml.writeCharacters("\n"); + writeElement(xml, "field", r.field, 10); + writeElementIfNotEmpty(xml, "pathScope", r.pathScope, 10); + writeElementIfNotEmpty(xml, "requestType", r.requestType, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // resolution + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // @After / @Afters collection + // ------------------------------------------------------------------------- + + private List<AfterInfo> collectAfters(TypeElement mojoType) { + List<AfterInfo> result = new ArrayList<>(); + // Check for @Afters (container) first + AnnotationMirror aftersAnn = findAnnotation(mojoType, AFTERS_ANN); + if (aftersAnn != null) { + AnnotationValue valueAttr = getAnnotationValue(aftersAnn, "value"); + if (valueAttr != null && valueAttr.getValue() instanceof List<?> list) { + for (Object item : list) { + if (item instanceof AnnotationValue av && av.getValue() instanceof AnnotationMirror afterMirror) { + result.add(extractAfter(afterMirror)); + } + } + } + } + // Also check for a single @After directly on the class + AnnotationMirror afterAnn = findAnnotation(mojoType, AFTER_ANN); + if (afterAnn != null && aftersAnn == null) { + result.add(extractAfter(afterAnn)); + } + return result; + } + + private AfterInfo extractAfter(AnnotationMirror ann) { + AfterInfo a = new AfterInfo(); + a.phase = getStringValue(ann, "phase", ""); + a.type = getEnumValue(ann, "type", "PROJECT"); + a.scope = getStringValue(ann, "scope", ""); + return a; + } + + private void writeAfterLink(XMLStreamWriter xml, AfterInfo a) throws XMLStreamException { + xml.writeCharacters(" "); + xml.writeStartElement("afterLink"); + xml.writeCharacters("\n"); + writeElementIfNotEmpty(xml, "phase", a.phase, 10); + writeElement(xml, "type", a.type, 10); + writeElementIfNotEmpty(xml, "scope", a.scope, 10); + xml.writeCharacters(" "); + xml.writeEndElement(); // afterLink + xml.writeCharacters("\n"); + } + + // ------------------------------------------------------------------------- + // Factory class generation + // ------------------------------------------------------------------------- + + private void generateFactories() throws IOException { + String groupId = getOption(OPT_GROUP_ID); + String artifactId = getOption(OPT_ARTIFACT_ID); + String version = getOption(OPT_VERSION); + + for (Map.Entry<String, TypeElement> entry : mojoElements.entrySet()) { + TypeElement mojoType = entry.getValue(); + AnnotationMirror mojoAnn = findAnnotation(mojoType, MOJO_ANN); + if (mojoAnn == null) { + continue; + } + String goal = getStringValue(mojoAnn, "name", ""); + if (goal.isEmpty()) { + continue; + } + + String fqn = mojoType.getQualifiedName().toString(); + String pkg = fqn.contains(".") ? fqn.substring(0, fqn.lastIndexOf('.')) : ""; + String simpleName = mojoType.getSimpleName().toString(); + String factorySimpleName = simpleName + "$MojoFactory"; Review Comment: 💡 **`$` in generated class names.** `MyMojo$MojoFactory` uses `$`, which the JLS §3.8 reserves for compiler-generated identifiers. Tools like ProGuard, ASM-based frameworks, and some IDEs interpret `$` as an inner-class separator and may misclassify these factories as inner classes of `MyMojo`. Consider `MyMojo__MojoFactory` or `MyMojoFactory` to avoid false classification. -- 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]
