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


##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,447 @@
+/*
+ * 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.StandardLocation;
+import javax.xml.stream.XMLStreamException;
+
+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 java.util.function.Consumer;
+
+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;
+import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxWriter;
+
+/**
+ * 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>Plugin coordinates are read from compiler {@code -A} options, typically 
provided by the
+ * {@code maven-plugin-mixin}.
+ *
+ * @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 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) {
+                    error("@Mojo must not be placed on an interface", element);
+                    continue;
+                }
+                if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
+                    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) {
+                            error("Failed to generate factory for " + fqn, e);
+                        }
+                    }
+                }
+            }
+        }
+        if (roundEnv.processingOver() && !mojoElements.isEmpty()) {
+            String[] coords = resolveCoordinates();
+            if (coords != null) {
+                try {
+                    writePluginXml(coords);
+                } catch (Exception e) {

Review Comment:
   โš ๏ธ **`catch (Exception e)` is too broad โ€” still open from prior reviews.**
   
   `writePluginXml()` declares `throws IOException` only. The 
`XMLStreamException` it can throw is already caught inside and re-wrapped as 
`IOException`. Catching `Exception` here silently converts `RuntimeException`s 
(e.g. `NullPointerException` from a corrupt builder state) into compiler 
diagnostics instead of letting them propagate as real errors.
   
   ```suggestion
                   } catch (IOException e) {
   ```



##########
impl/maven-plugin-mixin/pom.xml:
##########
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven</groupId>
+    <artifactId>maven-impl-modules</artifactId>
+    <version>4.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>maven-plugin-mixin</artifactId>
+  <name>Maven Plugin Mixin</name>
+  <description>Mixin and annotation processor for Maven 4 plugin projects. The 
mixin provides
+    zero-config plugin descriptor generation at compile time from 
@Mojo-annotated classes.</description>
+
+  <properties>
+    <javaModuleName>org.apache.maven.plugin.processor</javaModuleName>
+    <maven.compiler.proc>none</maven.compiler.proc>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-api-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-api-plugin</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-support</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.woodstox</groupId>
+          <artifactId>woodstox-core</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.codehaus.woodstox</groupId>
+          <artifactId>stax2-api</artifactId>
+        </exclusion>
+      </exclusions>

Review Comment:
   ๐Ÿ› **Woodstox exclusion breaks annotation processing for all consumers.**
   
   `PluginDescriptorStaxWriter` (Modello-generated) hard-codes:
   ```java
   XMLOutputFactory factory = new com.ctc.wstx.stax.WstxOutputFactory();
   ```
   This is not SPI-based โ€” it instantiates `WstxOutputFactory` directly. 
Excluding `woodstox-core` from the `maven-support` dependency means Woodstox is 
not on the annotation-processor classpath. When `writePluginXml()` runs, any 
plugin project using this mixin will fail with:
   ```
   java.lang.NoClassDefFoundError: com/ctc/wstx/stax/WstxOutputFactory
   ```
   
   The unit test 
(`MojoDescriptorProcessorTest.minimalMojoGeneratesPluginXmlAndFactory`) also 
exercises `writePluginXml()` and will fail in isolation (it passes in the full 
reactor build only because other Maven modules bring Woodstox onto the JVM).
   
   **Fix:** remove the `woodstox-core` and `stax2-api` exclusions. Woodstox is 
needed at annotation-processing time, not at plugin runtime, so the footprint 
concern is moot.



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,447 @@
+/*
+ * 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.StandardLocation;
+import javax.xml.stream.XMLStreamException;
+
+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 java.util.function.Consumer;
+
+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;
+import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxWriter;
+
+/**
+ * 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>Plugin coordinates are read from compiler {@code -A} options, typically 
provided by the
+ * {@code maven-plugin-mixin}.
+ *
+ * @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 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) {
+                    error("@Mojo must not be placed on an interface", element);
+                    continue;
+                }
+                if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
+                    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) {
+                            error("Failed to generate factory for " + fqn, e);
+                        }
+                    }
+                }
+            }
+        }
+        if (roundEnv.processingOver() && !mojoElements.isEmpty()) {
+            String[] coords = resolveCoordinates();
+            if (coords != null) {
+                try {
+                    writePluginXml(coords);
+                } catch (Exception e) {
+                    error("Failed to generate plugin.xml", e);
+                }
+            }
+        }
+        return false;
+    }
+
+    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 (isBlank(goalPrefix)) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+        if (isBlank(groupId) || isBlank(artifactId) || isBlank(version)) {
+            error("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);
+    }
+
+    private void writePluginXml(String[] coords) throws IOException {
+        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(coords[1])
+                .groupId(coords[0])
+                .artifactId(coords[1])
+                .version(coords[2])
+                .goalPrefix(coords[3])
+                .mojos(mojos)
+                .build();
+        try (Writer w = processingEnv
+                .getFiler()
+                .createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml")
+                .openWriter()) {
+            PluginDescriptorStaxWriter writer = new 
PluginDescriptorStaxWriter();
+            writer.setNamespace("http://maven.apache.org/PLUGIN/2.0.0";);
+            writer.write(w, descriptor);
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write plugin descriptor", e);
+        }
+        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 || mojoAnn.name().isEmpty()) {
+            if (mojoAnn != null) {
+                error("@Mojo.name() must not be empty", mojoType);
+            }
+            return null;
+        }
+        JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+        MojoDescriptor.Builder b = MojoDescriptor.newBuilder(true)
+                .goal(mojoAnn.name())
+                .implementation(mojoType.getQualifiedName().toString())
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+        set(b::phase, mojoAnn.defaultPhase());
+        set(b::configurator, mojoAnn.configurator());
+        set(b::dependencyResolution, mojoAnn.dependencyResolutionPathScopes());
+        if (mojoAnn.dependencyCollection()) {
+            b.dependencyCollection("true");
+        }
+        set(b::description, doc.description);
+        set(b::since, doc.since);
+        set(b::deprecated, doc.deprecated);
+        Execute exec = mojoType.getAnnotation(Execute.class);
+        if (exec != null) {
+            set(b::executePhase, exec.phase());
+            set(b::executeGoal, exec.goal());
+            set(b::executeLifecycle, exec.lifecycle());
+        }
+        b.parameters(collectParameters(mojoType));
+        b.resolutions(collectResolutions(mojoType));
+        b.afterLinks(collectAfterLinks(mojoType));
+        return b.build();
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD && 
enclosed.getKind() != ElementKind.METHOD) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter ann =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (ann == null) {
+                    continue;
+                }
+                String name = ann.name().isEmpty() ? 
enclosed.getSimpleName().toString() : ann.name();
+                if (!seen.add(name)) {
+                    continue;
+                }
+                String type = enclosed.getKind() == ElementKind.FIELD
+                        ? enclosed.asType().toString()
+                        : (((ExecutableElement) 
enclosed).getParameters().isEmpty()
+                                ? ((ExecutableElement) 
enclosed).getReturnType().toString()
+                                : ((ExecutableElement) enclosed)
+                                        .getParameters()
+                                        .get(0)
+                                        .asType()
+                                        .toString());
+                String expression = ann.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(name)
+                                .type(type)
+                                .required(ann.required())
+                                .editable(!ann.readonly());
+                set(pb::alias, ann.alias());
+                set(pb::expression, expression);
+                set(pb::defaultValue, ann.defaultValue());
+                set(pb::description, doc.description);
+                set(pb::since, doc.since);
+                set(pb::deprecated, doc.deprecated);
+                result.add(pb.build());
+            }
+        }
+        return result;
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD) {
+                    continue;
+                }
+                Resolution ann = enclosed.getAnnotation(Resolution.class);
+                if (ann == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (!seen.add(fieldName)) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                set(rb::pathScope, ann.pathScope());
+                set(rb::requestType, ann.requestType());
+                result.add(rb.build());
+            }
+        }
+        return result;
+    }
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>(afters.length);
+        for (After a : afters) {
+            AfterLink.Builder ab =
+                    
AfterLink.newBuilder(true).phase(a.phase()).type(a.type().name());
+            set(ab::scope, a.scope());
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    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 factoryName = simpleName + "$MojoFactory";

Review Comment:
   ๐Ÿ’ก **`$` in generated class names โ€” still open from prior reviews.**
   
   `MyMojo$MojoFactory` uses `$`, which the JLS ยง3.8 reserves for 
compiler-generated identifiers. While javac will compile it, tools (debuggers, 
serialization, class loaders using `Class.forName`) may mis-parse the `$` as an 
inner-class separator. Use `MojoFactory` as a suffix instead:
   
   ```suggestion
           String factoryName = simpleName + "MojoFactory";
   ```



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,447 @@
+/*
+ * 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.StandardLocation;
+import javax.xml.stream.XMLStreamException;
+
+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 java.util.function.Consumer;
+
+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;
+import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxWriter;
+
+/**
+ * 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>Plugin coordinates are read from compiler {@code -A} options, typically 
provided by the
+ * {@code maven-plugin-mixin}.
+ *
+ * @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 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) {
+                    error("@Mojo must not be placed on an interface", element);
+                    continue;
+                }
+                if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
+                    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) {
+                            error("Failed to generate factory for " + fqn, e);
+                        }
+                    }
+                }
+            }
+        }
+        if (roundEnv.processingOver() && !mojoElements.isEmpty()) {
+            String[] coords = resolveCoordinates();
+            if (coords != null) {
+                try {
+                    writePluginXml(coords);
+                } catch (Exception e) {
+                    error("Failed to generate plugin.xml", e);
+                }
+            }
+        }
+        return false;
+    }
+
+    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 (isBlank(goalPrefix)) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+        if (isBlank(groupId) || isBlank(artifactId) || isBlank(version)) {
+            error("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);
+    }
+
+    private void writePluginXml(String[] coords) throws IOException {
+        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(coords[1])
+                .groupId(coords[0])
+                .artifactId(coords[1])
+                .version(coords[2])
+                .goalPrefix(coords[3])
+                .mojos(mojos)
+                .build();
+        try (Writer w = processingEnv
+                .getFiler()
+                .createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml")
+                .openWriter()) {
+            PluginDescriptorStaxWriter writer = new 
PluginDescriptorStaxWriter();
+            writer.setNamespace("http://maven.apache.org/PLUGIN/2.0.0";);
+            writer.write(w, descriptor);
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write plugin descriptor", e);
+        }
+        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 || mojoAnn.name().isEmpty()) {
+            if (mojoAnn != null) {
+                error("@Mojo.name() must not be empty", mojoType);
+            }
+            return null;
+        }
+        JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+        MojoDescriptor.Builder b = MojoDescriptor.newBuilder(true)
+                .goal(mojoAnn.name())
+                .implementation(mojoType.getQualifiedName().toString())
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+        set(b::phase, mojoAnn.defaultPhase());
+        set(b::configurator, mojoAnn.configurator());
+        set(b::dependencyResolution, mojoAnn.dependencyResolutionPathScopes());
+        if (mojoAnn.dependencyCollection()) {
+            b.dependencyCollection("true");
+        }
+        set(b::description, doc.description);
+        set(b::since, doc.since);
+        set(b::deprecated, doc.deprecated);
+        Execute exec = mojoType.getAnnotation(Execute.class);
+        if (exec != null) {
+            set(b::executePhase, exec.phase());
+            set(b::executeGoal, exec.goal());
+            set(b::executeLifecycle, exec.lifecycle());
+        }
+        b.parameters(collectParameters(mojoType));
+        b.resolutions(collectResolutions(mojoType));
+        b.afterLinks(collectAfterLinks(mojoType));
+        return b.build();
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD && 
enclosed.getKind() != ElementKind.METHOD) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter ann =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (ann == null) {
+                    continue;
+                }
+                String name = ann.name().isEmpty() ? 
enclosed.getSimpleName().toString() : ann.name();
+                if (!seen.add(name)) {
+                    continue;
+                }
+                String type = enclosed.getKind() == ElementKind.FIELD
+                        ? enclosed.asType().toString()
+                        : (((ExecutableElement) 
enclosed).getParameters().isEmpty()
+                                ? ((ExecutableElement) 
enclosed).getReturnType().toString()
+                                : ((ExecutableElement) enclosed)
+                                        .getParameters()
+                                        .get(0)
+                                        .asType()
+                                        .toString());
+                String expression = ann.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(name)
+                                .type(type)
+                                .required(ann.required())
+                                .editable(!ann.readonly());
+                set(pb::alias, ann.alias());
+                set(pb::expression, expression);
+                set(pb::defaultValue, ann.defaultValue());
+                set(pb::description, doc.description);
+                set(pb::since, doc.since);
+                set(pb::deprecated, doc.deprecated);
+                result.add(pb.build());
+            }
+        }
+        return result;
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD) {
+                    continue;
+                }
+                Resolution ann = enclosed.getAnnotation(Resolution.class);
+                if (ann == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (!seen.add(fieldName)) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                set(rb::pathScope, ann.pathScope());
+                set(rb::requestType, ann.requestType());
+                result.add(rb.build());
+            }
+        }
+        return result;
+    }
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>(afters.length);
+        for (After a : afters) {
+            AfterLink.Builder ab =
+                    
AfterLink.newBuilder(true).phase(a.phase()).type(a.type().name());
+            set(ab::scope, a.scope());
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    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 factoryName = simpleName + "$MojoFactory";
+        String factoryFqn = pkg.isEmpty() ? factoryName : pkg + "." + 
factoryName;
+        String roleHint = coords[0] + ":" + coords[1] + ":" + coords[2] + ":" 
+ mojoAnn.name();
+
+        List<ExecutableElement> ctors = mojoType.getEnclosedElements().stream()
+                .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR)
+                .map(ExecutableElement.class::cast)
+                .filter(c -> !c.getModifiers().contains(Modifier.PRIVATE))
+                .toList();
+
+        try (PrintWriter pw = new PrintWriter(
+                processingEnv.getFiler().createSourceFile(factoryFqn, 
mojoType).openWriter())) {
+            if (!pkg.isEmpty()) {
+                pw.println("package " + pkg + ";");
+                pw.println();
+            }
+            pw.println("@org.apache.maven.api.di.Named(\"" + roleHint + "\")");
+            pw.println("public class " + factoryName + " extends " + 
simpleName + " {");
+            for (ExecutableElement ctor : ctors) {
+                if (ctor.getParameters().isEmpty()) {
+                    continue;
+                }
+                pw.print("    public " + factoryName + "(");
+                StringBuilder args = new StringBuilder();
+                for (int i = 0; i < ctor.getParameters().size(); i++) {
+                    VariableElement p = ctor.getParameters().get(i);
+                    if (i > 0) {
+                        pw.print(", ");
+                        args.append(", ");
+                    }
+                    pw.print(p.asType() + " " + p.getSimpleName());
+                    args.append(p.getSimpleName());
+                }
+                pw.println(") { super(" + args + "); }");

Review Comment:
   ๐Ÿ› **Factory constructor missing `throws` clause โ€” still open from prior 
reviews.**
   
   `ctor.getThrownTypes()` is never consulted. If a mojo constructor declares a 
checked exception (e.g. `public MyMojo() throws IOException`), the generated 
factory constructor `public MyMojoMojoFactory() { super(); }` will fail to 
compile with:
   ```
   error: unreported exception IOException; must be caught or declared to be 
thrown
   ```
   
   Fix โ€” after writing the parameter list, emit the `throws` clause if the 
original constructor has checked exceptions:
   ```suggestion
                   pw.print(") ");
                   List<? extends TypeMirror> thrown = ctor.getThrownTypes();
                   if (!thrown.isEmpty()) {
                       pw.print("throws ");
                       for (int j = 0; j < thrown.size(); j++) {
                           if (j > 0) pw.print(", ");
                           pw.print(thrown.get(j));
                       }
                       pw.print(" ");
                   }
                   pw.println("{ super(" + args + "); }");
   ```



##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,447 @@
+/*
+ * 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.StandardLocation;
+import javax.xml.stream.XMLStreamException;
+
+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 java.util.function.Consumer;
+
+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;
+import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxWriter;
+
+/**
+ * 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>Plugin coordinates are read from compiler {@code -A} options, typically 
provided by the
+ * {@code maven-plugin-mixin}.
+ *
+ * @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 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) {
+                    error("@Mojo must not be placed on an interface", element);
+                    continue;
+                }
+                if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
+                    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) {
+                            error("Failed to generate factory for " + fqn, e);
+                        }
+                    }
+                }
+            }
+        }
+        if (roundEnv.processingOver() && !mojoElements.isEmpty()) {
+            String[] coords = resolveCoordinates();
+            if (coords != null) {
+                try {
+                    writePluginXml(coords);
+                } catch (Exception e) {
+                    error("Failed to generate plugin.xml", e);
+                }
+            }
+        }
+        return false;
+    }
+
+    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 (isBlank(goalPrefix)) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+        if (isBlank(groupId) || isBlank(artifactId) || isBlank(version)) {
+            error("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);
+    }
+
+    private void writePluginXml(String[] coords) throws IOException {
+        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(coords[1])
+                .groupId(coords[0])
+                .artifactId(coords[1])
+                .version(coords[2])
+                .goalPrefix(coords[3])
+                .mojos(mojos)
+                .build();
+        try (Writer w = processingEnv
+                .getFiler()
+                .createResource(StandardLocation.CLASS_OUTPUT, "", 
"META-INF/maven/plugin.xml")
+                .openWriter()) {
+            PluginDescriptorStaxWriter writer = new 
PluginDescriptorStaxWriter();
+            writer.setNamespace("http://maven.apache.org/PLUGIN/2.0.0";);
+            writer.write(w, descriptor);
+        } catch (XMLStreamException e) {
+            throw new IOException("Failed to write plugin descriptor", e);
+        }
+        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 || mojoAnn.name().isEmpty()) {
+            if (mojoAnn != null) {
+                error("@Mojo.name() must not be empty", mojoType);
+            }
+            return null;
+        }
+        JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(mojoType));
+        MojoDescriptor.Builder b = MojoDescriptor.newBuilder(true)
+                .goal(mojoAnn.name())
+                .implementation(mojoType.getQualifiedName().toString())
+                .projectRequired(mojoAnn.projectRequired())
+                .onlineRequired(mojoAnn.onlineRequired())
+                .aggregator(mojoAnn.aggregator());
+        set(b::phase, mojoAnn.defaultPhase());
+        set(b::configurator, mojoAnn.configurator());
+        set(b::dependencyResolution, mojoAnn.dependencyResolutionPathScopes());
+        if (mojoAnn.dependencyCollection()) {
+            b.dependencyCollection("true");
+        }
+        set(b::description, doc.description);
+        set(b::since, doc.since);
+        set(b::deprecated, doc.deprecated);
+        Execute exec = mojoType.getAnnotation(Execute.class);
+        if (exec != null) {
+            set(b::executePhase, exec.phase());
+            set(b::executeGoal, exec.goal());
+            set(b::executeLifecycle, exec.lifecycle());
+        }
+        b.parameters(collectParameters(mojoType));
+        b.resolutions(collectResolutions(mojoType));
+        b.afterLinks(collectAfterLinks(mojoType));
+        return b.build();
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD && 
enclosed.getKind() != ElementKind.METHOD) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.annotations.Parameter ann =
+                        
enclosed.getAnnotation(org.apache.maven.api.plugin.annotations.Parameter.class);
+                if (ann == null) {
+                    continue;
+                }
+                String name = ann.name().isEmpty() ? 
enclosed.getSimpleName().toString() : ann.name();
+                if (!seen.add(name)) {
+                    continue;
+                }
+                String type = enclosed.getKind() == ElementKind.FIELD
+                        ? enclosed.asType().toString()
+                        : (((ExecutableElement) 
enclosed).getParameters().isEmpty()
+                                ? ((ExecutableElement) 
enclosed).getReturnType().toString()
+                                : ((ExecutableElement) enclosed)
+                                        .getParameters()
+                                        .get(0)
+                                        .asType()
+                                        .toString());
+                String expression = ann.property();
+                if (!expression.isEmpty() && !expression.startsWith("${")) {
+                    expression = "${" + expression + "}";
+                }
+                JavadocInfo doc = 
parseJavadoc(processingEnv.getElementUtils().getDocComment(enclosed));
+                org.apache.maven.api.plugin.descriptor.Parameter.Builder pb =
+                        
org.apache.maven.api.plugin.descriptor.Parameter.newBuilder(true)
+                                .name(name)
+                                .type(type)
+                                .required(ann.required())
+                                .editable(!ann.readonly());
+                set(pb::alias, ann.alias());
+                set(pb::expression, expression);
+                set(pb::defaultValue, ann.defaultValue());
+                set(pb::description, doc.description);
+                set(pb::since, doc.since);
+                set(pb::deprecated, doc.deprecated);
+                result.add(pb.build());
+            }
+        }
+        return result;
+    }
+
+    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<>();
+        for (TypeElement cur = mojoType; cur != null && !isObject(cur); cur = 
superType(cur)) {
+            for (Element enclosed : cur.getEnclosedElements()) {
+                if (enclosed.getKind() != ElementKind.FIELD) {
+                    continue;
+                }
+                Resolution ann = enclosed.getAnnotation(Resolution.class);
+                if (ann == null) {
+                    continue;
+                }
+                String fieldName = enclosed.getSimpleName().toString();
+                if (!seen.add(fieldName)) {
+                    continue;
+                }
+                org.apache.maven.api.plugin.descriptor.Resolution.Builder rb =
+                        
org.apache.maven.api.plugin.descriptor.Resolution.newBuilder(true)
+                                .field(fieldName);
+                set(rb::pathScope, ann.pathScope());
+                set(rb::requestType, ann.requestType());
+                result.add(rb.build());
+            }
+        }
+        return result;
+    }
+
+    private List<AfterLink> collectAfterLinks(TypeElement mojoType) {
+        After[] afters = mojoType.getAnnotationsByType(After.class);
+        if (afters.length == 0) {
+            return List.of();
+        }
+        List<AfterLink> result = new ArrayList<>(afters.length);
+        for (After a : afters) {
+            AfterLink.Builder ab =
+                    
AfterLink.newBuilder(true).phase(a.phase()).type(a.type().name());
+            set(ab::scope, a.scope());
+            result.add(ab.build());
+        }
+        return result;
+    }
+
+    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 factoryName = simpleName + "$MojoFactory";
+        String factoryFqn = pkg.isEmpty() ? factoryName : pkg + "." + 
factoryName;
+        String roleHint = coords[0] + ":" + coords[1] + ":" + coords[2] + ":" 
+ mojoAnn.name();
+
+        List<ExecutableElement> ctors = mojoType.getEnclosedElements().stream()
+                .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR)
+                .map(ExecutableElement.class::cast)
+                .filter(c -> !c.getModifiers().contains(Modifier.PRIVATE))
+                .toList();
+
+        try (PrintWriter pw = new PrintWriter(
+                processingEnv.getFiler().createSourceFile(factoryFqn, 
mojoType).openWriter())) {
+            if (!pkg.isEmpty()) {
+                pw.println("package " + pkg + ";");
+                pw.println();
+            }
+            pw.println("@org.apache.maven.api.di.Named(\"" + roleHint + "\")");
+            pw.println("public class " + factoryName + " extends " + 
simpleName + " {");
+            for (ExecutableElement ctor : ctors) {
+                if (ctor.getParameters().isEmpty()) {
+                    continue;
+                }
+                pw.print("    public " + factoryName + "(");
+                StringBuilder args = new StringBuilder();
+                for (int i = 0; i < ctor.getParameters().size(); i++) {
+                    VariableElement p = ctor.getParameters().get(i);
+                    if (i > 0) {
+                        pw.print(", ");
+                        args.append(", ");
+                    }
+                    pw.print(p.asType() + " " + p.getSimpleName());
+                    args.append(p.getSimpleName());
+                }
+                pw.println(") { super(" + args + "); }");
+            }
+            pw.println("}");
+        }
+        generatedFactoryNames.add(factoryFqn);
+        note("Generated factory " + factoryFqn);
+    }
+
+    static JavadocInfo parseJavadoc(String raw) {
+        if (raw == null || raw.isBlank()) {
+            return new JavadocInfo(null, null, null);
+        }
+        String[] parts = raw.split("(?m)^\\s*@");

Review Comment:
   โš ๏ธ **Greedy Javadoc `@` regex โ€” still open from prior reviews.**
   
   `(?m)^\s*@` splits on **any** `@` at the start of a line โ€” including email 
addresses (`@author [email protected]`) and annotation references inside the 
text body. A Javadoc like:
   ```java
   /**
    * Contact [email protected] for details.
    * @since 1.0
    */
   ```
   would produce a spurious split at `[email protected]` if that line happens 
to start with whitespace + `@`.
   
   Use the standard Javadoc tag pattern that requires the `@` to begin a known 
tag word:
   ```suggestion
           String[] parts = 
raw.split("(?m)^\s*@(?=param|return|throws|exception|since|deprecated|see|author|version|serial)");
   ```



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