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


##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,768 @@
+/*
+ * 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.lang.model.SourceVersion;
+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;
+
+import org.apache.maven.api.plugin.annotations.After;
+import org.apache.maven.api.plugin.annotations.Execute;
+import org.apache.maven.api.plugin.annotations.Mojo;
+import org.apache.maven.api.plugin.annotations.Resolution;
+import org.apache.maven.api.plugin.descriptor.AfterLink;
+import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * JSR 269 annotation processor that generates Maven plugin descriptors 
({@code META-INF/maven/plugin.xml})
+ * and DI factory classes at compile time from {@link Mojo @Mojo}-annotated 
classes.
+ *
+ * <p>The processor reads plugin coordinates from compiler {@code -A} options, 
typically
+ * provided by the {@code maven-plugin-mixin}:
+ * <ul>
+ *   <li>{@code maven.plugin.groupId} — plugin group ID (required)</li>
+ *   <li>{@code maven.plugin.artifactId} — plugin artifact ID (required)</li>
+ *   <li>{@code maven.plugin.version} — plugin version (required)</li>
+ *   <li>{@code maven.plugin.goalPrefix} — goal prefix (optional, derived from 
artifactId)</li>
+ * </ul>
+ *
+ * @since 4.1.0
+ */
+@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo")
+@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 PLUGIN_NS = 
"http://maven.apache.org/PLUGIN/2.0.0";;
+
+    private final Map<String, TypeElement> mojoElements = new 
LinkedHashMap<>();
+    private final Set<String> generatedFactoryNames = new LinkedHashSet<>();
+    private String[] resolvedCoordinates;
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latestSupported();
+    }
+
+    @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();
+                if (generatedFactoryNames.contains(fqn)) {
+                    continue;
+                }
+                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;
+                }
+                if (!mojoElements.containsKey(fqn)) {
+                    mojoElements.put(fqn, typeElement);
+                    String[] coords = resolveCoordinates();
+                    if (coords != null) {
+                        try {
+                            generateFactory(typeElement, coords);
+                        } catch (IOException e) {
+                            logError("Failed to generate factory for " + fqn, 
e);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (roundEnv.processingOver()) {
+            if (mojoElements.isEmpty()) {
+                return false;
+            }
+            String[] coords = resolveCoordinates();
+            if (coords == null) {
+                return false;
+            }
+            try {
+                writePluginXml(coords);
+            } catch (Exception e) {
+                logError("Failed to generate plugin.xml", e);

Review Comment:
   ⚠️ **`catch (Exception e)` too broad — still open from prior reviews.**
   
   `writePluginXml()` declares `throws IOException, XMLStreamException`. 
Catching `Exception` here silently swallows any unchecked exception 
(`NullPointerException`, `IllegalStateException`, etc.) from the XML generation 
path, converting a processor bug into a misleading "Failed to generate 
plugin.xml" compiler diagnostic. Narrow to the declared checked exceptions:
   
   ```suggestion
               } catch (IOException | XMLStreamException e) {
   ```



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,768 @@
+/*
+ * 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.lang.model.SourceVersion;
+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;
+
+import org.apache.maven.api.plugin.annotations.After;
+import org.apache.maven.api.plugin.annotations.Execute;
+import org.apache.maven.api.plugin.annotations.Mojo;
+import org.apache.maven.api.plugin.annotations.Resolution;
+import org.apache.maven.api.plugin.descriptor.AfterLink;
+import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * JSR 269 annotation processor that generates Maven plugin descriptors 
({@code META-INF/maven/plugin.xml})
+ * and DI factory classes at compile time from {@link Mojo @Mojo}-annotated 
classes.
+ *
+ * <p>The processor reads plugin coordinates from compiler {@code -A} options, 
typically
+ * provided by the {@code maven-plugin-mixin}:
+ * <ul>
+ *   <li>{@code maven.plugin.groupId} — plugin group ID (required)</li>
+ *   <li>{@code maven.plugin.artifactId} — plugin artifact ID (required)</li>
+ *   <li>{@code maven.plugin.version} — plugin version (required)</li>
+ *   <li>{@code maven.plugin.goalPrefix} — goal prefix (optional, derived from 
artifactId)</li>
+ * </ul>
+ *
+ * @since 4.1.0
+ */
+@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo")
+@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 PLUGIN_NS = 
"http://maven.apache.org/PLUGIN/2.0.0";;
+
+    private final Map<String, TypeElement> mojoElements = new 
LinkedHashMap<>();
+    private final Set<String> generatedFactoryNames = new LinkedHashSet<>();
+    private String[] resolvedCoordinates;
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latestSupported();
+    }
+
+    @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();
+                if (generatedFactoryNames.contains(fqn)) {
+                    continue;
+                }
+                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;
+                }
+                if (!mojoElements.containsKey(fqn)) {
+                    mojoElements.put(fqn, typeElement);
+                    String[] coords = resolveCoordinates();
+                    if (coords != null) {
+                        try {
+                            generateFactory(typeElement, coords);
+                        } catch (IOException e) {
+                            logError("Failed to generate factory for " + fqn, 
e);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (roundEnv.processingOver()) {
+            if (mojoElements.isEmpty()) {
+                return false;
+            }
+            String[] coords = resolveCoordinates();
+            if (coords == null) {
+                return false;
+            }
+            try {
+                writePluginXml(coords);
+            } catch (Exception e) {
+                logError("Failed to generate plugin.xml", e);
+            }
+        }
+
+        return false;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Coordinate resolution
+    // 
-------------------------------------------------------------------------
+
+    String[] resolveCoordinates() {
+        if (resolvedCoordinates != null) {
+            return resolvedCoordinates;
+        }
+
+        String groupId = getOption(OPT_GROUP_ID);
+        String artifactId = getOption(OPT_ARTIFACT_ID);
+        String version = getOption(OPT_VERSION);
+        String goalPrefix = getOption(OPT_GOAL_PREFIX);
+        if (goalPrefix == null || goalPrefix.isEmpty()) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+
+        if (groupId == null
+                || groupId.isEmpty()
+                || artifactId == null
+                || artifactId.isEmpty()
+                || version == null
+                || version.isEmpty()) {
+            processingEnv
+                    .getMessager()
+                    .printMessage(
+                            Diagnostic.Kind.ERROR,
+                            "maven-plugin-mixin: could not determine plugin 
coordinates. "
+                                    + "Supply -Amaven.plugin.groupId, 
-Amaven.plugin.artifactId and "
+                                    + "-Amaven.plugin.version via the 
maven-plugin-mixin.");
+            return null;
+        }
+
+        resolvedCoordinates = new String[] {groupId, artifactId, version, 
goalPrefix};
+        return resolvedCoordinates;
+    }
+
+    private String getOption(String key) {
+        return processingEnv.getOptions().get(key);
+    }
+
+    // 
-------------------------------------------------------------------------
+    // plugin.xml generation
+    // 
-------------------------------------------------------------------------
+
+    private void writePluginXml(String[] coords) throws IOException, 
XMLStreamException {
+        String groupId = coords[0];
+        String artifactId = coords[1];
+        String version = coords[2];
+        String goalPrefix = coords[3];
+
+        List<MojoDescriptor> mojos = new ArrayList<>();
+        for (TypeElement mojoType : mojoElements.values()) {
+            MojoDescriptor mojo = buildMojoDescriptor(mojoType);
+            if (mojo != null) {
+                mojos.add(mojo);
+            }
+        }
+
+        PluginDescriptor descriptor = PluginDescriptor.newBuilder(true)
+                .name(artifactId)
+                .groupId(groupId)
+                .artifactId(artifactId)
+                .version(version)
+                .goalPrefix(goalPrefix)
+                .mojos(mojos)
+                .build();
+
+        FileObject out =
+                
processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml");
+        try (Writer w = out.openWriter()) {
+            writeDescriptorXml(w, descriptor);
+        }
+        note("Generated META-INF/maven/plugin.xml with " + mojos.size() + " 
mojo(s)");
+    }
+
+    private MojoDescriptor buildMojoDescriptor(TypeElement mojoType) {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null) {
+            return null;
+        }
+
+        String goal = mojoAnn.name();
+        if (goal.isEmpty()) {
+            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
"@Mojo.name() must not be empty", mojoType);
+            return null;
+        }
+
+        String fqn = mojoType.getQualifiedName().toString();
+        JavadocInfo mojoDoc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+
+        MojoDescriptor.Builder builder = MojoDescriptor.newBuilder(true)
+                .goal(goal)
+                .implementation(fqn)
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+
+        if (!mojoAnn.defaultPhase().isEmpty()) {
+            builder.phase(mojoAnn.defaultPhase());
+        }
+        if (!mojoAnn.configurator().isEmpty()) {
+            builder.configurator(mojoAnn.configurator());
+        }
+        if (!mojoAnn.dependencyResolutionPathScopes().isEmpty()) {
+            
builder.dependencyResolution(mojoAnn.dependencyResolutionPathScopes());
+        }
+        if (mojoAnn.dependencyCollection()) {
+            builder.dependencyCollection("true");
+        }
+        if (mojoDoc.description != null) {
+            builder.description(mojoDoc.description);
+        }
+        if (mojoDoc.since != null) {
+            builder.since(mojoDoc.since);
+        }
+        if (mojoDoc.deprecated != null) {
+            builder.deprecated(mojoDoc.deprecated);
+        }
+
+        // @Execute
+        Execute executeAnn = mojoType.getAnnotation(Execute.class);
+        if (executeAnn != null) {
+            if (!executeAnn.phase().isEmpty()) {
+                builder.executePhase(executeAnn.phase());
+            }
+            if (!executeAnn.goal().isEmpty()) {
+                builder.executeGoal(executeAnn.goal());
+            }
+            if (!executeAnn.lifecycle().isEmpty()) {
+                builder.executeLifecycle(executeAnn.lifecycle());
+            }
+        }
+
+        builder.parameters(collectParameters(mojoType));
+        builder.resolutions(collectResolutions(mojoType));
+        builder.afterLinks(collectAfterLinks(mojoType));
+
+        return builder.build();
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Parameter collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Parameter> 
collectParameters(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Parameter> 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;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter paramAnn =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (paramAnn == null) {
+                    continue;
+                }
+                String paramName =
+                        paramAnn.name().isEmpty() ? 
enclosed.getSimpleName().toString() : paramAnn.name();
+                if (seen.contains(paramName)) {
+                    continue;
+                }
+                seen.add(paramName);
+
+                String type;
+                if (enclosed.getKind() == ElementKind.FIELD) {
+                    type = ((VariableElement) enclosed).asType().toString();
+                } else {
+                    ExecutableElement method = (ExecutableElement) enclosed;
+                    type = !method.getParameters().isEmpty()
+                            ? method.getParameters().get(0).asType().toString()
+                            : method.getReturnType().toString();
+                }
+
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+
+                String expression = paramAnn.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(paramName)
+                                .type(type)
+                                .required(paramAnn.required())
+                                .editable(!paramAnn.readonly());
+                if (!paramAnn.alias().isEmpty()) {
+                    pb.alias(paramAnn.alias());
+                }
+                if (!expression.isEmpty()) {
+                    pb.expression(expression);
+                }
+                if (!paramAnn.defaultValue().isEmpty()) {
+                    pb.defaultValue(paramAnn.defaultValue());
+                }
+                if (doc.description != null) {
+                    pb.description(doc.description);
+                }
+                if (doc.since != null) {
+                    pb.since(doc.since);
+                }
+                if (doc.deprecated != null) {
+                    pb.deprecated(doc.deprecated);
+                }
+                result.add(pb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Resolution collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Resolution> 
collectResolutions(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Resolution> 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;
+                }
+                Resolution resAnn = enclosed.getAnnotation(Resolution.class);
+                if (resAnn == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (seen.contains(fieldName)) {
+                    continue;
+                }
+                seen.add(fieldName);
+
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                if (!resAnn.pathScope().isEmpty()) {
+                    rb.pathScope(resAnn.pathScope());
+                }
+                if (!resAnn.requestType().isEmpty()) {
+                    rb.requestType(resAnn.requestType());
+                }
+                result.add(rb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // @After collection
+    // 
-------------------------------------------------------------------------
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>();
+        for (After after : afters) {
+            AfterLink.Builder ab = AfterLink.newBuilder(true)
+                    .phase(after.phase())
+                    .type(after.type().name());
+            if (!after.scope().isEmpty()) {
+                ab.scope(after.scope());
+            }
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Factory class generation
+    // 
-------------------------------------------------------------------------
+
+    private void generateFactory(TypeElement mojoType, String[] coords) throws 
IOException {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null || mojoAnn.name().isEmpty()) {
+            return;
+        }
+
+        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;

Review Comment:
   💡 **`$` in generated class names — still open from prior reviews.**
   
   `MyMojo$MojoFactory` uses `$`, which the JLS (§3.8) reserves for 
compiler-generated names (inner classes, lambdas, bridge methods). Tools such 
as ProGuard, Byte Buddy, and some IDEs treat `$` as a nested-class separator 
and may misclassify these factories as inner classes of `MyMojo`. The canonical 
separator for generated peer classes that avoids ambiguity is `_`:
   
   ```suggestion
           String factorySimpleName = simpleName + "_MojoFactory";
   ```



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,768 @@
+/*
+ * 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.lang.model.SourceVersion;
+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;
+
+import org.apache.maven.api.plugin.annotations.After;
+import org.apache.maven.api.plugin.annotations.Execute;
+import org.apache.maven.api.plugin.annotations.Mojo;
+import org.apache.maven.api.plugin.annotations.Resolution;
+import org.apache.maven.api.plugin.descriptor.AfterLink;
+import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * JSR 269 annotation processor that generates Maven plugin descriptors 
({@code META-INF/maven/plugin.xml})
+ * and DI factory classes at compile time from {@link Mojo @Mojo}-annotated 
classes.
+ *
+ * <p>The processor reads plugin coordinates from compiler {@code -A} options, 
typically
+ * provided by the {@code maven-plugin-mixin}:
+ * <ul>
+ *   <li>{@code maven.plugin.groupId} — plugin group ID (required)</li>
+ *   <li>{@code maven.plugin.artifactId} — plugin artifact ID (required)</li>
+ *   <li>{@code maven.plugin.version} — plugin version (required)</li>
+ *   <li>{@code maven.plugin.goalPrefix} — goal prefix (optional, derived from 
artifactId)</li>
+ * </ul>
+ *
+ * @since 4.1.0
+ */
+@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo")
+@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 PLUGIN_NS = 
"http://maven.apache.org/PLUGIN/2.0.0";;
+
+    private final Map<String, TypeElement> mojoElements = new 
LinkedHashMap<>();
+    private final Set<String> generatedFactoryNames = new LinkedHashSet<>();
+    private String[] resolvedCoordinates;
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latestSupported();
+    }
+
+    @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();
+                if (generatedFactoryNames.contains(fqn)) {
+                    continue;
+                }
+                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;
+                }
+                if (!mojoElements.containsKey(fqn)) {
+                    mojoElements.put(fqn, typeElement);
+                    String[] coords = resolveCoordinates();
+                    if (coords != null) {
+                        try {
+                            generateFactory(typeElement, coords);
+                        } catch (IOException e) {
+                            logError("Failed to generate factory for " + fqn, 
e);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (roundEnv.processingOver()) {
+            if (mojoElements.isEmpty()) {
+                return false;
+            }
+            String[] coords = resolveCoordinates();
+            if (coords == null) {
+                return false;
+            }
+            try {
+                writePluginXml(coords);
+            } catch (Exception e) {
+                logError("Failed to generate plugin.xml", e);
+            }
+        }
+
+        return false;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Coordinate resolution
+    // 
-------------------------------------------------------------------------
+
+    String[] resolveCoordinates() {
+        if (resolvedCoordinates != null) {
+            return resolvedCoordinates;
+        }
+
+        String groupId = getOption(OPT_GROUP_ID);
+        String artifactId = getOption(OPT_ARTIFACT_ID);
+        String version = getOption(OPT_VERSION);
+        String goalPrefix = getOption(OPT_GOAL_PREFIX);
+        if (goalPrefix == null || goalPrefix.isEmpty()) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+
+        if (groupId == null
+                || groupId.isEmpty()
+                || artifactId == null
+                || artifactId.isEmpty()
+                || version == null
+                || version.isEmpty()) {
+            processingEnv
+                    .getMessager()
+                    .printMessage(
+                            Diagnostic.Kind.ERROR,
+                            "maven-plugin-mixin: could not determine plugin 
coordinates. "
+                                    + "Supply -Amaven.plugin.groupId, 
-Amaven.plugin.artifactId and "
+                                    + "-Amaven.plugin.version via the 
maven-plugin-mixin.");
+            return null;
+        }
+
+        resolvedCoordinates = new String[] {groupId, artifactId, version, 
goalPrefix};
+        return resolvedCoordinates;
+    }
+
+    private String getOption(String key) {
+        return processingEnv.getOptions().get(key);
+    }
+
+    // 
-------------------------------------------------------------------------
+    // plugin.xml generation
+    // 
-------------------------------------------------------------------------
+
+    private void writePluginXml(String[] coords) throws IOException, 
XMLStreamException {
+        String groupId = coords[0];
+        String artifactId = coords[1];
+        String version = coords[2];
+        String goalPrefix = coords[3];
+
+        List<MojoDescriptor> mojos = new ArrayList<>();
+        for (TypeElement mojoType : mojoElements.values()) {
+            MojoDescriptor mojo = buildMojoDescriptor(mojoType);
+            if (mojo != null) {
+                mojos.add(mojo);
+            }
+        }
+
+        PluginDescriptor descriptor = PluginDescriptor.newBuilder(true)
+                .name(artifactId)
+                .groupId(groupId)
+                .artifactId(artifactId)
+                .version(version)
+                .goalPrefix(goalPrefix)
+                .mojos(mojos)
+                .build();
+
+        FileObject out =
+                
processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml");
+        try (Writer w = out.openWriter()) {
+            writeDescriptorXml(w, descriptor);
+        }
+        note("Generated META-INF/maven/plugin.xml with " + mojos.size() + " 
mojo(s)");
+    }
+
+    private MojoDescriptor buildMojoDescriptor(TypeElement mojoType) {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null) {
+            return null;
+        }
+
+        String goal = mojoAnn.name();
+        if (goal.isEmpty()) {
+            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
"@Mojo.name() must not be empty", mojoType);
+            return null;
+        }
+
+        String fqn = mojoType.getQualifiedName().toString();
+        JavadocInfo mojoDoc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+
+        MojoDescriptor.Builder builder = MojoDescriptor.newBuilder(true)
+                .goal(goal)
+                .implementation(fqn)
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+
+        if (!mojoAnn.defaultPhase().isEmpty()) {
+            builder.phase(mojoAnn.defaultPhase());
+        }
+        if (!mojoAnn.configurator().isEmpty()) {
+            builder.configurator(mojoAnn.configurator());
+        }
+        if (!mojoAnn.dependencyResolutionPathScopes().isEmpty()) {
+            
builder.dependencyResolution(mojoAnn.dependencyResolutionPathScopes());
+        }
+        if (mojoAnn.dependencyCollection()) {
+            builder.dependencyCollection("true");
+        }
+        if (mojoDoc.description != null) {
+            builder.description(mojoDoc.description);
+        }
+        if (mojoDoc.since != null) {
+            builder.since(mojoDoc.since);
+        }
+        if (mojoDoc.deprecated != null) {
+            builder.deprecated(mojoDoc.deprecated);
+        }
+
+        // @Execute
+        Execute executeAnn = mojoType.getAnnotation(Execute.class);
+        if (executeAnn != null) {
+            if (!executeAnn.phase().isEmpty()) {
+                builder.executePhase(executeAnn.phase());
+            }
+            if (!executeAnn.goal().isEmpty()) {
+                builder.executeGoal(executeAnn.goal());
+            }
+            if (!executeAnn.lifecycle().isEmpty()) {
+                builder.executeLifecycle(executeAnn.lifecycle());
+            }
+        }
+
+        builder.parameters(collectParameters(mojoType));
+        builder.resolutions(collectResolutions(mojoType));
+        builder.afterLinks(collectAfterLinks(mojoType));
+
+        return builder.build();
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Parameter collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Parameter> 
collectParameters(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Parameter> 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;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter paramAnn =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (paramAnn == null) {
+                    continue;
+                }
+                String paramName =
+                        paramAnn.name().isEmpty() ? 
enclosed.getSimpleName().toString() : paramAnn.name();
+                if (seen.contains(paramName)) {
+                    continue;
+                }
+                seen.add(paramName);
+
+                String type;
+                if (enclosed.getKind() == ElementKind.FIELD) {
+                    type = ((VariableElement) enclosed).asType().toString();
+                } else {
+                    ExecutableElement method = (ExecutableElement) enclosed;
+                    type = !method.getParameters().isEmpty()
+                            ? method.getParameters().get(0).asType().toString()
+                            : method.getReturnType().toString();
+                }
+
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+
+                String expression = paramAnn.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(paramName)
+                                .type(type)
+                                .required(paramAnn.required())
+                                .editable(!paramAnn.readonly());
+                if (!paramAnn.alias().isEmpty()) {
+                    pb.alias(paramAnn.alias());
+                }
+                if (!expression.isEmpty()) {
+                    pb.expression(expression);
+                }
+                if (!paramAnn.defaultValue().isEmpty()) {
+                    pb.defaultValue(paramAnn.defaultValue());
+                }
+                if (doc.description != null) {
+                    pb.description(doc.description);
+                }
+                if (doc.since != null) {
+                    pb.since(doc.since);
+                }
+                if (doc.deprecated != null) {
+                    pb.deprecated(doc.deprecated);
+                }
+                result.add(pb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Resolution collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Resolution> 
collectResolutions(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Resolution> 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;
+                }
+                Resolution resAnn = enclosed.getAnnotation(Resolution.class);
+                if (resAnn == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (seen.contains(fieldName)) {
+                    continue;
+                }
+                seen.add(fieldName);
+
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                if (!resAnn.pathScope().isEmpty()) {
+                    rb.pathScope(resAnn.pathScope());
+                }
+                if (!resAnn.requestType().isEmpty()) {
+                    rb.requestType(resAnn.requestType());
+                }
+                result.add(rb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // @After collection
+    // 
-------------------------------------------------------------------------
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>();
+        for (After after : afters) {
+            AfterLink.Builder ab = AfterLink.newBuilder(true)
+                    .phase(after.phase())
+                    .type(after.type().name());
+            if (!after.scope().isEmpty()) {
+                ab.scope(after.scope());
+            }
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Factory class generation
+    // 
-------------------------------------------------------------------------
+
+    private void generateFactory(TypeElement mojoType, String[] coords) throws 
IOException {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null || mojoAnn.name().isEmpty()) {
+            return;
+        }
+
+        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 = coords[0] + ":" + coords[1] + ":" + coords[2] + ":" 
+ mojoAnn.name();
+
+        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("@org.apache.maven.api.di.Named(\"" + roleHint + "\")");
+            pw.println("public class " + factorySimpleName + " extends " + 
simpleName + " {");
+            pw.println();
+
+            if (!ctors.isEmpty()
+                    && !(ctors.size() == 1 && 
ctors.get(0).getParameters().isEmpty())) {
+                for (ExecutableElement ctor : ctors) {
+                    if (ctor.getParameters().isEmpty()) {
+                        continue;
+                    }
+                    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);
+                        if (i > 0) {
+                            pw.print(", ");
+                            args.append(", ");
+                        }
+                        pw.print(param.asType() + " " + param.getSimpleName());
+                        args.append(param.getSimpleName());
+                    }
+                    pw.println(") {");
+                    pw.println("        super(" + args + ");");
+                    pw.println("    }");
+                    pw.println();
+                }
+            }
+
+            pw.println("}");
+        }
+
+        generatedFactoryNames.add(factoryFqn);
+        note("Generated factory " + factoryFqn + " for goal '" + 
mojoAnn.name() + "'");
+    }
+
+    // 
-------------------------------------------------------------------------
+    // XML writing (using JDK XMLStreamWriter to avoid woodstox dependency)
+    // 
-------------------------------------------------------------------------
+
+    private static void writeDescriptorXml(Writer w, PluginDescriptor 
descriptor)
+            throws IOException, XMLStreamException {
+        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", descriptor.getName(), 2);
+            writeElement(xml, "groupId", descriptor.getGroupId(), 2);
+            writeElement(xml, "artifactId", descriptor.getArtifactId(), 2);
+            writeElement(xml, "version", descriptor.getVersion(), 2);
+            writeElement(xml, "goalPrefix", descriptor.getGoalPrefix(), 2);
+
+            if (descriptor.getMojos() != null && 
!descriptor.getMojos().isEmpty()) {
+                xml.writeCharacters("  ");
+                xml.writeStartElement("mojos");
+                xml.writeCharacters("\n");
+                for (MojoDescriptor mojo : descriptor.getMojos()) {
+                    writeMojoXml(xml, mojo);
+                }
+                xml.writeCharacters("  ");
+                xml.writeEndElement();
+                xml.writeCharacters("\n");
+            }
+
+            xml.writeEndElement();
+            xml.writeCharacters("\n");
+            xml.writeEndDocument();
+        } finally {
+            xml.close();
+        }
+    }
+
+    private static void writeMojoXml(XMLStreamWriter xml, MojoDescriptor mojo) 
throws XMLStreamException {
+        xml.writeCharacters("    ");
+        xml.writeStartElement("mojo");
+        xml.writeCharacters("\n");
+
+        writeElement(xml, "goal", mojo.getGoal(), 6);
+        writeOptional(xml, "phase", mojo.getPhase(), 6);
+        writeElement(xml, "implementation", mojo.getImplementation(), 6);
+        writeElement(xml, "language", mojo.getLanguage(), 6);
+        writeOptional(xml, "description", mojo.getDescription(), 6);
+        writeOptional(xml, "since", mojo.getSince(), 6);
+        writeOptional(xml, "deprecated", mojo.getDeprecated(), 6);
+        writeElement(xml, "projectRequired", 
String.valueOf(mojo.isProjectRequired()), 6);
+        writeElement(xml, "onlineRequired", 
String.valueOf(mojo.isOnlineRequired()), 6);
+        writeElement(xml, "aggregator", String.valueOf(mojo.isAggregator()), 
6);
+        writeOptional(xml, "configurator", mojo.getConfigurator(), 6);
+        writeOptional(xml, "executePhase", mojo.getExecutePhase(), 6);
+        writeOptional(xml, "executeGoal", mojo.getExecuteGoal(), 6);
+        writeOptional(xml, "executeLifecycle", mojo.getExecuteLifecycle(), 6);
+        writeOptional(xml, "dependencyResolution", 
mojo.getDependencyResolution(), 6);
+        writeOptional(xml, "dependencyCollection", 
mojo.getDependencyCollection(), 6);
+
+        if (mojo.getParameters() != null && !mojo.getParameters().isEmpty()) {
+            xml.writeCharacters("      ");
+            xml.writeStartElement("parameters");
+            xml.writeCharacters("\n");
+            for (org.apache.maven.api.plugin.descriptor.Parameter p : 
mojo.getParameters()) {
+                xml.writeCharacters("        ");
+                xml.writeStartElement("parameter");
+                xml.writeCharacters("\n");
+                writeElement(xml, "name", p.getName(), 10);
+                writeOptional(xml, "alias", p.getAlias(), 10);
+                writeElement(xml, "type", p.getType(), 10);
+                writeElement(xml, "required", String.valueOf(p.isRequired()), 
10);
+                writeElement(xml, "editable", String.valueOf(p.isEditable()), 
10);
+                writeOptional(xml, "description", p.getDescription(), 10);
+                writeOptional(xml, "since", p.getSince(), 10);
+                writeOptional(xml, "deprecated", p.getDeprecated(), 10);
+                writeOptional(xml, "expression", p.getExpression(), 10);
+                writeOptional(xml, "defaultValue", p.getDefaultValue(), 10);
+                xml.writeCharacters("        ");
+                xml.writeEndElement();
+                xml.writeCharacters("\n");
+            }
+            xml.writeCharacters("      ");
+            xml.writeEndElement();
+            xml.writeCharacters("\n");
+        }
+
+        if (mojo.getResolutions() != null && !mojo.getResolutions().isEmpty()) 
{
+            xml.writeCharacters("      ");
+            xml.writeStartElement("resolutions");
+            xml.writeCharacters("\n");
+            for (org.apache.maven.api.plugin.descriptor.Resolution r : 
mojo.getResolutions()) {
+                xml.writeCharacters("        ");
+                xml.writeStartElement("resolution");
+                xml.writeCharacters("\n");
+                writeElement(xml, "field", r.getField(), 10);
+                writeOptional(xml, "pathScope", r.getPathScope(), 10);
+                writeOptional(xml, "requestType", r.getRequestType(), 10);
+                xml.writeCharacters("        ");
+                xml.writeEndElement();
+                xml.writeCharacters("\n");
+            }
+            xml.writeCharacters("      ");
+            xml.writeEndElement();
+            xml.writeCharacters("\n");
+        }
+
+        if (mojo.getAfterLinks() != null && !mojo.getAfterLinks().isEmpty()) {
+            xml.writeCharacters("      ");
+            xml.writeStartElement("afterLinks");
+            xml.writeCharacters("\n");
+            for (AfterLink a : mojo.getAfterLinks()) {
+                xml.writeCharacters("        ");
+                xml.writeStartElement("afterLink");
+                xml.writeCharacters("\n");
+                writeOptional(xml, "phase", a.getPhase(), 10);
+                writeElement(xml, "type", a.getType(), 10);
+                writeOptional(xml, "scope", a.getScope(), 10);
+                xml.writeCharacters("        ");
+                xml.writeEndElement();
+                xml.writeCharacters("\n");
+            }
+            xml.writeCharacters("      ");
+            xml.writeEndElement();
+            xml.writeCharacters("\n");
+        }
+
+        xml.writeCharacters("    ");
+        xml.writeEndElement();
+        xml.writeCharacters("\n");
+    }
+
+    private static void writeElement(XMLStreamWriter xml, String tag, String 
value, int indent)
+            throws XMLStreamException {
+        xml.writeCharacters(" ".repeat(indent));
+        xml.writeStartElement(tag);
+        xml.writeCharacters(value != null ? value : "");
+        xml.writeEndElement();
+        xml.writeCharacters("\n");
+    }
+
+    private static void writeOptional(XMLStreamWriter xml, String tag, String 
value, int indent)
+            throws XMLStreamException {
+        if (value != null && !value.isEmpty()) {
+            writeElement(xml, tag, value, indent);
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Javadoc parsing
+    // 
-------------------------------------------------------------------------
+
+    private static JavadocInfo parseJavadoc(String raw) {
+        JavadocInfo info = new JavadocInfo();
+        if (raw == null || raw.isBlank()) {
+            return info;
+        }
+        String[] parts = raw.split("(?m)^\\s*@");
+        String body = parts[0];

Review Comment:
   ⚠️ **Greedy Javadoc `@` regex — still open from prior reviews.**
   
   `(?m)^\s*@` splits on **any** `@` at the start of a line — including 
`@Override` annotations in code snippets embedded in Javadoc, email addresses 
(`@example.com`), and `{@link ...}` / `{@code ...}` that wrap to a new line. 
This silently truncates mojo descriptions. Restrict to known Javadoc block-tag 
keywords:
   
   ```suggestion
           String[] parts = 
raw.split("(?m)^\\s*@(?=param|return|throws|exception|since|deprecated|see|author|version|serial|serialField|serialData)\\b");
   ```



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,768 @@
+/*
+ * 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.lang.model.SourceVersion;
+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;
+
+import org.apache.maven.api.plugin.annotations.After;
+import org.apache.maven.api.plugin.annotations.Execute;
+import org.apache.maven.api.plugin.annotations.Mojo;
+import org.apache.maven.api.plugin.annotations.Resolution;
+import org.apache.maven.api.plugin.descriptor.AfterLink;
+import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
+import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+
+/**
+ * JSR 269 annotation processor that generates Maven plugin descriptors 
({@code META-INF/maven/plugin.xml})
+ * and DI factory classes at compile time from {@link Mojo @Mojo}-annotated 
classes.
+ *
+ * <p>The processor reads plugin coordinates from compiler {@code -A} options, 
typically
+ * provided by the {@code maven-plugin-mixin}:
+ * <ul>
+ *   <li>{@code maven.plugin.groupId} — plugin group ID (required)</li>
+ *   <li>{@code maven.plugin.artifactId} — plugin artifact ID (required)</li>
+ *   <li>{@code maven.plugin.version} — plugin version (required)</li>
+ *   <li>{@code maven.plugin.goalPrefix} — goal prefix (optional, derived from 
artifactId)</li>
+ * </ul>
+ *
+ * @since 4.1.0
+ */
+@SupportedAnnotationTypes("org.apache.maven.api.plugin.annotations.Mojo")
+@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 PLUGIN_NS = 
"http://maven.apache.org/PLUGIN/2.0.0";;
+
+    private final Map<String, TypeElement> mojoElements = new 
LinkedHashMap<>();
+    private final Set<String> generatedFactoryNames = new LinkedHashSet<>();
+    private String[] resolvedCoordinates;
+
+    @Override
+    public SourceVersion getSupportedSourceVersion() {
+        return SourceVersion.latestSupported();
+    }
+
+    @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();
+                if (generatedFactoryNames.contains(fqn)) {
+                    continue;
+                }
+                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;
+                }
+                if (!mojoElements.containsKey(fqn)) {
+                    mojoElements.put(fqn, typeElement);
+                    String[] coords = resolveCoordinates();
+                    if (coords != null) {
+                        try {
+                            generateFactory(typeElement, coords);
+                        } catch (IOException e) {
+                            logError("Failed to generate factory for " + fqn, 
e);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (roundEnv.processingOver()) {
+            if (mojoElements.isEmpty()) {
+                return false;
+            }
+            String[] coords = resolveCoordinates();
+            if (coords == null) {
+                return false;
+            }
+            try {
+                writePluginXml(coords);
+            } catch (Exception e) {
+                logError("Failed to generate plugin.xml", e);
+            }
+        }
+
+        return false;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Coordinate resolution
+    // 
-------------------------------------------------------------------------
+
+    String[] resolveCoordinates() {
+        if (resolvedCoordinates != null) {
+            return resolvedCoordinates;
+        }
+
+        String groupId = getOption(OPT_GROUP_ID);
+        String artifactId = getOption(OPT_ARTIFACT_ID);
+        String version = getOption(OPT_VERSION);
+        String goalPrefix = getOption(OPT_GOAL_PREFIX);
+        if (goalPrefix == null || goalPrefix.isEmpty()) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+
+        if (groupId == null
+                || groupId.isEmpty()
+                || artifactId == null
+                || artifactId.isEmpty()
+                || version == null
+                || version.isEmpty()) {
+            processingEnv
+                    .getMessager()
+                    .printMessage(
+                            Diagnostic.Kind.ERROR,
+                            "maven-plugin-mixin: could not determine plugin 
coordinates. "
+                                    + "Supply -Amaven.plugin.groupId, 
-Amaven.plugin.artifactId and "
+                                    + "-Amaven.plugin.version via the 
maven-plugin-mixin.");
+            return null;
+        }
+
+        resolvedCoordinates = new String[] {groupId, artifactId, version, 
goalPrefix};
+        return resolvedCoordinates;
+    }
+
+    private String getOption(String key) {
+        return processingEnv.getOptions().get(key);
+    }
+
+    // 
-------------------------------------------------------------------------
+    // plugin.xml generation
+    // 
-------------------------------------------------------------------------
+
+    private void writePluginXml(String[] coords) throws IOException, 
XMLStreamException {
+        String groupId = coords[0];
+        String artifactId = coords[1];
+        String version = coords[2];
+        String goalPrefix = coords[3];
+
+        List<MojoDescriptor> mojos = new ArrayList<>();
+        for (TypeElement mojoType : mojoElements.values()) {
+            MojoDescriptor mojo = buildMojoDescriptor(mojoType);
+            if (mojo != null) {
+                mojos.add(mojo);
+            }
+        }
+
+        PluginDescriptor descriptor = PluginDescriptor.newBuilder(true)
+                .name(artifactId)
+                .groupId(groupId)
+                .artifactId(artifactId)
+                .version(version)
+                .goalPrefix(goalPrefix)
+                .mojos(mojos)
+                .build();
+
+        FileObject out =
+                
processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml");
+        try (Writer w = out.openWriter()) {
+            writeDescriptorXml(w, descriptor);
+        }
+        note("Generated META-INF/maven/plugin.xml with " + mojos.size() + " 
mojo(s)");
+    }
+
+    private MojoDescriptor buildMojoDescriptor(TypeElement mojoType) {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null) {
+            return null;
+        }
+
+        String goal = mojoAnn.name();
+        if (goal.isEmpty()) {
+            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
"@Mojo.name() must not be empty", mojoType);
+            return null;
+        }
+
+        String fqn = mojoType.getQualifiedName().toString();
+        JavadocInfo mojoDoc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+
+        MojoDescriptor.Builder builder = MojoDescriptor.newBuilder(true)
+                .goal(goal)
+                .implementation(fqn)
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+
+        if (!mojoAnn.defaultPhase().isEmpty()) {
+            builder.phase(mojoAnn.defaultPhase());
+        }
+        if (!mojoAnn.configurator().isEmpty()) {
+            builder.configurator(mojoAnn.configurator());
+        }
+        if (!mojoAnn.dependencyResolutionPathScopes().isEmpty()) {
+            
builder.dependencyResolution(mojoAnn.dependencyResolutionPathScopes());
+        }
+        if (mojoAnn.dependencyCollection()) {
+            builder.dependencyCollection("true");
+        }
+        if (mojoDoc.description != null) {
+            builder.description(mojoDoc.description);
+        }
+        if (mojoDoc.since != null) {
+            builder.since(mojoDoc.since);
+        }
+        if (mojoDoc.deprecated != null) {
+            builder.deprecated(mojoDoc.deprecated);
+        }
+
+        // @Execute
+        Execute executeAnn = mojoType.getAnnotation(Execute.class);
+        if (executeAnn != null) {
+            if (!executeAnn.phase().isEmpty()) {
+                builder.executePhase(executeAnn.phase());
+            }
+            if (!executeAnn.goal().isEmpty()) {
+                builder.executeGoal(executeAnn.goal());
+            }
+            if (!executeAnn.lifecycle().isEmpty()) {
+                builder.executeLifecycle(executeAnn.lifecycle());
+            }
+        }
+
+        builder.parameters(collectParameters(mojoType));
+        builder.resolutions(collectResolutions(mojoType));
+        builder.afterLinks(collectAfterLinks(mojoType));
+
+        return builder.build();
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Parameter collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Parameter> 
collectParameters(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Parameter> 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;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter paramAnn =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (paramAnn == null) {
+                    continue;
+                }
+                String paramName =
+                        paramAnn.name().isEmpty() ? 
enclosed.getSimpleName().toString() : paramAnn.name();
+                if (seen.contains(paramName)) {
+                    continue;
+                }
+                seen.add(paramName);
+
+                String type;
+                if (enclosed.getKind() == ElementKind.FIELD) {
+                    type = ((VariableElement) enclosed).asType().toString();
+                } else {
+                    ExecutableElement method = (ExecutableElement) enclosed;
+                    type = !method.getParameters().isEmpty()
+                            ? method.getParameters().get(0).asType().toString()
+                            : method.getReturnType().toString();
+                }
+
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+
+                String expression = paramAnn.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(paramName)
+                                .type(type)
+                                .required(paramAnn.required())
+                                .editable(!paramAnn.readonly());
+                if (!paramAnn.alias().isEmpty()) {
+                    pb.alias(paramAnn.alias());
+                }
+                if (!expression.isEmpty()) {
+                    pb.expression(expression);
+                }
+                if (!paramAnn.defaultValue().isEmpty()) {
+                    pb.defaultValue(paramAnn.defaultValue());
+                }
+                if (doc.description != null) {
+                    pb.description(doc.description);
+                }
+                if (doc.since != null) {
+                    pb.since(doc.since);
+                }
+                if (doc.deprecated != null) {
+                    pb.deprecated(doc.deprecated);
+                }
+                result.add(pb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Resolution collection
+    // 
-------------------------------------------------------------------------
+
+    private List<org.apache.maven.api.plugin.descriptor.Resolution> 
collectResolutions(TypeElement mojoType) {
+        List<org.apache.maven.api.plugin.descriptor.Resolution> 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;
+                }
+                Resolution resAnn = enclosed.getAnnotation(Resolution.class);
+                if (resAnn == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (seen.contains(fieldName)) {
+                    continue;
+                }
+                seen.add(fieldName);
+
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                if (!resAnn.pathScope().isEmpty()) {
+                    rb.pathScope(resAnn.pathScope());
+                }
+                if (!resAnn.requestType().isEmpty()) {
+                    rb.requestType(resAnn.requestType());
+                }
+                result.add(rb.build());
+            }
+            current = superTypeElement(current);
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // @After collection
+    // 
-------------------------------------------------------------------------
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>();
+        for (After after : afters) {
+            AfterLink.Builder ab = AfterLink.newBuilder(true)
+                    .phase(after.phase())
+                    .type(after.type().name());
+            if (!after.scope().isEmpty()) {
+                ab.scope(after.scope());
+            }
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Factory class generation
+    // 
-------------------------------------------------------------------------
+
+    private void generateFactory(TypeElement mojoType, String[] coords) throws 
IOException {
+        Mojo mojoAnn = mojoType.getAnnotation(Mojo.class);
+        if (mojoAnn == null || mojoAnn.name().isEmpty()) {
+            return;
+        }
+
+        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 = coords[0] + ":" + coords[1] + ":" + coords[2] + ":" 
+ mojoAnn.name();
+
+        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("@org.apache.maven.api.di.Named(\"" + roleHint + "\")");
+            pw.println("public class " + factorySimpleName + " extends " + 
simpleName + " {");
+            pw.println();
+
+            if (!ctors.isEmpty()
+                    && !(ctors.size() == 1 && 
ctors.get(0).getParameters().isEmpty())) {
+                for (ExecutableElement ctor : ctors) {
+                    if (ctor.getParameters().isEmpty()) {
+                        continue;
+                    }
+                    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);
+                        if (i > 0) {
+                            pw.print(", ");
+                            args.append(", ");
+                        }
+                        pw.print(param.asType() + " " + param.getSimpleName());
+                        args.append(param.getSimpleName());
+                    }
+                    pw.println(") {");
+                    pw.println("        super(" + args + ");");

Review Comment:
   🐛 **Factory constructor missing `throws` clause — still open from prior 
reviews.**
   
   The parameter loop copies parameter names and types but never calls 
`ctor.getThrownTypes()`. If a `@Mojo` class declares a checked exception on its 
constructor (e.g. `MyMojo() throws IOException`), the generated factory 
constructor emits `public MyMojo_MojoFactory(...) {` — which the compiler 
rejects because `super(...)` propagates the checked exception but the subclass 
constructor doesn't declare it.
   
   Fix — emit the `throws` clause before the opening brace:
   
   ```suggestion
                       List<? extends TypeMirror> thrown = 
ctor.getThrownTypes();
                       if (!thrown.isEmpty()) {
                           pw.print(") throws ");
                           pw.print(thrown.stream()
                                   .map(Object::toString)
                                   
.collect(java.util.stream.Collectors.joining(", ")));
                       }
                       pw.println(") {");
   ```



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

To unsubscribe, e-mail: [email protected]

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

Reply via email to