You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/07/25 06:32:03 UTC

(maven-plugin-tools) 01/01: Wire up @After annotation scanning in the annotation-based descriptor extractor

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch wire-up-after-annotation-scanning-in-maven-plugin
in repository https://gitbox.apache.org/repos/asf/maven-plugin-tools.git

commit dfdae35dcb1b5aec399fae6a6f2e75c4539808bb
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Sat Jul 25 08:31:50 2026 +0200

    Wire up @After annotation scanning in the annotation-based descriptor extractor
    
    The v4 API @After annotation (and its @Repeatable container @Afters)
    was not recognized by the annotation scanner, so plugins using @After
    had to hand-craft the <afterLink> descriptor entries.  This commit adds
    end-to-end support:
    
    - MojoAnnotationsScanner: register @After / @Afters in CLASS_LEVEL_ANNOTATIONS
    - AfterAnnotationContent: new datamodel POJO holding phase/type/scope
    - MojoAnnotatedClass: add List<AfterAnnotationContent> field
    - MojoAnnotationVisitor: track nested annotation visitors returned from
      visitAnnotation() and array sub-visitors from visitArray(), so the
      @Afters repeatable container can be unpacked
    - DefaultMojoAnnotationsScanner.analyzeVisitors(): extract single @After
      and @Afters container into AfterAnnotationContent objects
    - ExtendedMojoDescriptor: add AfterLink inner class and addAfterLink()
    - JavaAnnotationsMojoDescriptorExtractor.toMojoDescriptors(): walk the
      class hierarchy collecting @After annotations and map them to AfterLink
    - PluginDescriptorFilesGenerator: emit <afterLinks>/<afterLink> elements
      in the v4 plugin descriptor
    
    Complements apache/maven#12535 which adds the model (AfterLink), runtime
    processing (BuildPlanExecutor.applyAfterLinks), and an IT that currently
    hand-crafts the descriptor.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 .../JavaAnnotationsMojoDescriptorExtractor.java    | 35 ++++++++++
 .../datamodel/AfterAnnotationContent.java          | 80 ++++++++++++++++++++++
 .../scanner/DefaultMojoAnnotationsScanner.java     | 35 ++++++++++
 .../annotations/scanner/MojoAnnotatedClass.java    | 24 ++++++-
 .../scanner/MojoAnnotationsScanner.java            |  4 +-
 .../scanner/visitors/MojoAnnotationVisitor.java    | 47 ++++++++++++-
 .../maven/tools/plugin/ExtendedMojoDescriptor.java | 64 +++++++++++++++++
 .../generator/PluginDescriptorFilesGenerator.java  | 26 +++++++
 8 files changed, 310 insertions(+), 5 deletions(-)

diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
index 04347349..6df5e19a 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/JavaAnnotationsMojoDescriptorExtractor.java
@@ -67,6 +67,7 @@ import org.apache.maven.tools.plugin.extractor.annotations.converter.ConverterCo
 import org.apache.maven.tools.plugin.extractor.annotations.converter.JavaClassConverterContext;
 import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter;
 import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter;
+import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
@@ -729,6 +730,14 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
                 }
             }
 
+            // @After annotations — collect from class hierarchy
+            List<AfterAnnotationContent> afterAnnotations =
+                    getAfterAnnotationsFromHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
+            for (AfterAnnotationContent after : afterAnnotations) {
+                mojoDescriptor.addAfterLink(
+                        new ExtendedMojoDescriptor.AfterLink(after.phase(), after.type(), after.scope()));
+            }
+
             mojoDescriptor.setExecutionStrategy(mojo.executionStrategy());
             // ???
             // mojoDescriptor.alwaysExecute(mojo.a)
@@ -819,6 +828,32 @@ public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExt
         return findClassWithExecuteAnnotationInParentHierarchy(parent, mojoAnnotatedClasses);
     }
 
+    /**
+     * Collects all {@code @After} annotations from the class hierarchy, starting from the
+     * most-derived class.  Unlike {@code @Execute}, {@code @After} is repeatable, so
+     * multiple entries may exist on a single class and across the hierarchy.
+     */
+    protected List<AfterAnnotationContent> getAfterAnnotationsFromHierarchy(
+            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
+        List<AfterAnnotationContent> result = new ArrayList<>();
+        collectAfterAnnotations(mojoAnnotatedClass, mojoAnnotatedClasses, result);
+        return result;
+    }
+
+    private void collectAfterAnnotations(
+            MojoAnnotatedClass mojoAnnotatedClass,
+            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
+            List<AfterAnnotationContent> result) {
+        result.addAll(mojoAnnotatedClass.getAfterAnnotations());
+        String parentClassName = mojoAnnotatedClass.getParentClassName();
+        if (parentClassName != null && !parentClassName.isEmpty()) {
+            MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
+            if (parent != null) {
+                collectAfterAnnotations(parent, mojoAnnotatedClasses, result);
+            }
+        }
+    }
+
     protected Map<String, ParameterAnnotationContent> getParametersParentHierarchy(
             MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
         List<ParameterAnnotationContent> parameterAnnotationContents = new ArrayList<>();
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/datamodel/AfterAnnotationContent.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/datamodel/AfterAnnotationContent.java
new file mode 100644
index 00000000..cd5d81b9
--- /dev/null
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/datamodel/AfterAnnotationContent.java
@@ -0,0 +1,80 @@
+/*
+ * 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.tools.plugin.extractor.annotations.datamodel;
+
+/**
+ * Holds scanned data from a {@code @After} annotation
+ * ({@code org.apache.maven.api.plugin.annotations.After}).
+ * <p>
+ * The {@code @After} annotation declares that the annotated mojo must run
+ * after another phase has completed.  It carries three attributes:
+ * <ul>
+ *   <li>{@code phase}  &ndash; the phase this mojo must run after</li>
+ *   <li>{@code type}   &ndash; the scope type ({@code PROJECT}, {@code DEPENDENCIES}, {@code CHILDREN})</li>
+ *   <li>{@code scope}  &ndash; the dependency scope</li>
+ * </ul>
+ * <p>
+ * Setter method names match the annotation attribute names so that
+ * {@link org.apache.maven.tools.plugin.extractor.annotations.scanner.DefaultMojoAnnotationsScanner#populateAnnotationContent}
+ * can fill them via reflection.
+ *
+ * @since 4.0.0
+ */
+public class AfterAnnotationContent {
+    private String phase;
+
+    private String type;
+
+    private String scope;
+
+    public String phase() {
+        return this.phase;
+    }
+
+    public void phase(String phase) {
+        this.phase = phase;
+    }
+
+    public String type() {
+        return this.type;
+    }
+
+    public void type(String type) {
+        this.type = type;
+    }
+
+    public String scope() {
+        return this.scope;
+    }
+
+    public void scope(String scope) {
+        this.scope = scope;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("AfterAnnotationContent");
+        sb.append("{phase='").append(phase).append('\'');
+        sb.append(", type='").append(type).append('\'');
+        sb.append(", scope='").append(scope).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/DefaultMojoAnnotationsScanner.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/DefaultMojoAnnotationsScanner.java
index fad64132..e9d8dca3 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/DefaultMojoAnnotationsScanner.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/DefaultMojoAnnotationsScanner.java
@@ -26,6 +26,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -42,6 +43,7 @@ import org.apache.maven.plugins.annotations.Execute;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.tools.plugin.extractor.ExtractionException;
+import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
@@ -73,6 +75,8 @@ public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
     public static final String MOJO_V4 = MVN4_API + "Mojo";
     public static final String EXECUTE_V4 = MVN4_API + "Execute";
     public static final String PARAMETER_V4 = MVN4_API + "Parameter";
+    public static final String AFTER_V4 = MVN4_API + "After";
+    public static final String AFTERS_V4 = MVN4_API + "Afters";
 
     public static final String MOJO_V3 = Mojo.class.getName();
     public static final String EXECUTE_V3 = Execute.class.getName();
@@ -308,6 +312,37 @@ public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
                 mojoAnnotatedClass.setExecute(executeAnnotationContent);
             }
 
+            // @After annotation(s) — v4 API only
+            List<AfterAnnotationContent> afterAnnotations = new ArrayList<>();
+
+            // single @After
+            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTER_V4);
+            if (mojoAnnotationVisitor != null) {
+                AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent();
+                populateAnnotationContent(afterAnnotationContent, mojoAnnotationVisitor);
+                afterAnnotations.add(afterAnnotationContent);
+            }
+
+            // @Afters repeatable container wraps multiple @After in its "value" array attribute
+            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTERS_V4);
+            if (mojoAnnotationVisitor != null) {
+                // The @Afters annotation has a "value" array attribute; visitArray("value")
+                // creates a sub-visitor, and each nested @After is visited via visitAnnotation
+                // on that sub-visitor.
+                MojoAnnotationVisitor arrayVisitor = mojoAnnotationVisitor.getArrayVisitor("value");
+                if (arrayVisitor != null) {
+                    for (MojoAnnotationVisitor nestedVisitor : arrayVisitor.getNestedAnnotationVisitors()) {
+                        AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent();
+                        populateAnnotationContent(afterAnnotationContent, nestedVisitor);
+                        afterAnnotations.add(afterAnnotationContent);
+                    }
+                }
+            }
+
+            if (!afterAnnotations.isEmpty()) {
+                mojoAnnotatedClass.setAfterAnnotations(afterAnnotations);
+            }
+
             // @Parameter annotations
             List<MojoParameterVisitor> mojoParameterVisitors =
                     mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4)));
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotatedClass.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotatedClass.java
index f408d8dc..703abefc 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotatedClass.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotatedClass.java
@@ -18,10 +18,13 @@
  */
 package org.apache.maven.tools.plugin.extractor.annotations.scanner;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.apache.maven.artifact.Artifact;
+import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
 import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
@@ -42,6 +45,8 @@ public class MojoAnnotatedClass {
 
     private ExecuteAnnotationContent execute;
 
+    private List<AfterAnnotationContent> afterAnnotations;
+
     /**
      * key is field name
      */
@@ -99,6 +104,18 @@ public class MojoAnnotatedClass {
         return this;
     }
 
+    public List<AfterAnnotationContent> getAfterAnnotations() {
+        if (this.afterAnnotations == null) {
+            this.afterAnnotations = new ArrayList<>();
+        }
+        return afterAnnotations;
+    }
+
+    public MojoAnnotatedClass setAfterAnnotations(List<AfterAnnotationContent> afterAnnotations) {
+        this.afterAnnotations = afterAnnotations;
+        return this;
+    }
+
     public Map<String, ParameterAnnotationContent> getParameters() {
         if (this.parameters == null) {
             this.parameters = new HashMap<>();
@@ -141,7 +158,11 @@ public class MojoAnnotatedClass {
     }
 
     public boolean hasAnnotations() {
-        return !(getComponents().isEmpty() && getParameters().isEmpty() && execute == null && mojo == null);
+        return !(getComponents().isEmpty()
+                && getParameters().isEmpty()
+                && execute == null
+                && mojo == null
+                && getAfterAnnotations().isEmpty());
     }
 
     public boolean isV4Api() {
@@ -161,6 +182,7 @@ public class MojoAnnotatedClass {
         sb.append(", parentClassName='").append(parentClassName).append('\'');
         sb.append(", mojo=").append(mojo);
         sb.append(", execute=").append(execute);
+        sb.append(", afterAnnotations=").append(afterAnnotations);
         sb.append(", parameters=").append(parameters);
         sb.append(", components=").append(components);
         sb.append(", v4api=").append(v4Api);
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotationsScanner.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotationsScanner.java
index 9495f0b9..aced9059 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotationsScanner.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/MojoAnnotationsScanner.java
@@ -44,7 +44,9 @@ public interface MojoAnnotationsScanner {
             Execute.class.getName(),
             Deprecated.class.getName(),
             V4_API_ANNOTATIONS_PACKAGE + ".Mojo",
-            V4_API_ANNOTATIONS_PACKAGE + ".Execute");
+            V4_API_ANNOTATIONS_PACKAGE + ".Execute",
+            V4_API_ANNOTATIONS_PACKAGE + ".After",
+            V4_API_ANNOTATIONS_PACKAGE + ".Afters");
 
     List<String> FIELD_LEVEL_ANNOTATIONS = Arrays.asList(
             Parameter.class.getName(),
diff --git a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/visitors/MojoAnnotationVisitor.java b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/visitors/MojoAnnotationVisitor.java
index 9eb420da..2e535a07 100644
--- a/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/visitors/MojoAnnotationVisitor.java
+++ b/maven-plugin-tools-annotations/src/main/java/org/apache/maven/tools/plugin/extractor/annotations/scanner/visitors/MojoAnnotationVisitor.java
@@ -18,11 +18,15 @@
  */
 package org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
 
 /**
  * Visitor for annotations.
@@ -35,6 +39,10 @@ public class MojoAnnotationVisitor extends AnnotationVisitor {
 
     private Map<String, Object> annotationValues = new HashMap<>();
 
+    private List<MojoAnnotationVisitor> nestedAnnotationVisitors;
+
+    private Map<String, MojoAnnotationVisitor> arrayVisitors;
+
     MojoAnnotationVisitor(String annotationClassName) {
         super(Opcodes.ASM9);
         this.annotationClassName = annotationClassName;
@@ -56,11 +64,44 @@ public class MojoAnnotationVisitor extends AnnotationVisitor {
 
     @Override
     public AnnotationVisitor visitAnnotation(String name, String desc) {
-        return new MojoAnnotationVisitor(this.annotationClassName);
+        String className = desc != null ? Type.getType(desc).getClassName() : this.annotationClassName;
+        MojoAnnotationVisitor nested = new MojoAnnotationVisitor(className);
+        if (nestedAnnotationVisitors == null) {
+            nestedAnnotationVisitors = new ArrayList<>();
+        }
+        nestedAnnotationVisitors.add(nested);
+        return nested;
     }
 
     @Override
-    public AnnotationVisitor visitArray(String s) {
-        return new MojoAnnotationVisitor(this.annotationClassName);
+    public AnnotationVisitor visitArray(String name) {
+        MojoAnnotationVisitor arrayVisitor = new MojoAnnotationVisitor(this.annotationClassName);
+        if (this.arrayVisitors == null) {
+            this.arrayVisitors = new HashMap<>();
+        }
+        this.arrayVisitors.put(name, arrayVisitor);
+        return arrayVisitor;
+    }
+
+    /**
+     * Returns the sub-visitor created by {@link #visitArray(String)} for the given array attribute.
+     * The sub-visitor's {@link #getNestedAnnotationVisitors()} contains the annotation elements.
+     *
+     * @param name the array attribute name
+     * @return the array sub-visitor, or {@code null} if not visited
+     */
+    public MojoAnnotationVisitor getArrayVisitor(String name) {
+        return arrayVisitors != null ? arrayVisitors.get(name) : null;
+    }
+
+    /**
+     * Returns the nested annotation visitors collected by {@link #visitAnnotation(String, String)}
+     * and by array sub-visitors.  Used to extract elements from repeatable annotation containers
+     * such as {@code @Afters}.
+     *
+     * @return list of nested annotation visitors, never {@code null}
+     */
+    public List<MojoAnnotationVisitor> getNestedAnnotationVisitors() {
+        return nestedAnnotationVisitors != null ? nestedAnnotationVisitors : Collections.emptyList();
     }
 }
diff --git a/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/ExtendedMojoDescriptor.java b/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/ExtendedMojoDescriptor.java
index 7dad0a00..342f0fa1 100644
--- a/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/ExtendedMojoDescriptor.java
+++ b/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/ExtendedMojoDescriptor.java
@@ -18,6 +18,9 @@
  */
 package org.apache.maven.tools.plugin;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Objects;
 
 import org.apache.maven.plugin.descriptor.MojoDescriptor;
@@ -31,6 +34,7 @@ import org.apache.maven.plugin.descriptor.Parameter;
 public class ExtendedMojoDescriptor extends MojoDescriptor {
     private final boolean containsXhtmlTextValues;
     private boolean v4Api;
+    private List<AfterLink> afterLinks;
 
     public ExtendedMojoDescriptor() {
         this(false);
@@ -63,6 +67,66 @@ public class ExtendedMojoDescriptor extends MojoDescriptor {
         this.v4Api = v4Api;
     }
 
+    /**
+     * Returns the after-links declared via {@code @After} annotations.
+     *
+     * @return unmodifiable list of after-links, never {@code null}
+     * @since 4.0.0
+     */
+    public List<AfterLink> getAfterLinks() {
+        return afterLinks != null ? Collections.unmodifiableList(afterLinks) : Collections.emptyList();
+    }
+
+    /**
+     * Adds an after-link.
+     *
+     * @param afterLink the after-link to add
+     * @since 4.0.0
+     */
+    public void addAfterLink(AfterLink afterLink) {
+        if (this.afterLinks == null) {
+            this.afterLinks = new ArrayList<>();
+        }
+        this.afterLinks.add(afterLink);
+    }
+
+    /**
+     * Holds after-link data from the {@code @After} annotation.
+     * <p>
+     * Mirrors the {@code org.apache.maven.api.plugin.descriptor.AfterLink} model class
+     * from the Maven 4 API without introducing a compile-time dependency on it.
+     *
+     * @since 4.0.0
+     */
+    public static class AfterLink {
+        private final String phase;
+        private final String type;
+        private final String scope;
+
+        public AfterLink(String phase, String type, String scope) {
+            this.phase = phase;
+            this.type = type;
+            this.scope = scope;
+        }
+
+        public String getPhase() {
+            return phase;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getScope() {
+            return scope;
+        }
+
+        @Override
+        public String toString() {
+            return "AfterLink{phase='" + phase + "', type='" + type + "', scope='" + scope + "'}";
+        }
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(containsXhtmlTextValues, v4Api);
diff --git a/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/PluginDescriptorFilesGenerator.java b/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/PluginDescriptorFilesGenerator.java
index 07ee9956..e1f6ffdc 100644
--- a/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/PluginDescriptorFilesGenerator.java
+++ b/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/PluginDescriptorFilesGenerator.java
@@ -332,6 +332,32 @@ public class PluginDescriptorFilesGenerator implements Generator {
             GeneratorUtils.element(w, "executeLifecycle", mojoDescriptor.getExecuteLifecycle());
         }
 
+        // ----------------------------------------------------------------------
+        // After links (v4 API only)
+        // ----------------------------------------------------------------------
+
+        if (isV4 && mojoDescriptor instanceof ExtendedMojoDescriptor) {
+            List<ExtendedMojoDescriptor.AfterLink> afterLinks =
+                    ((ExtendedMojoDescriptor) mojoDescriptor).getAfterLinks();
+            if (!afterLinks.isEmpty()) {
+                w.startElement("afterLinks");
+                for (ExtendedMojoDescriptor.AfterLink afterLink : afterLinks) {
+                    w.startElement("afterLink");
+                    if (afterLink.getPhase() != null) {
+                        GeneratorUtils.element(w, "phase", afterLink.getPhase());
+                    }
+                    if (afterLink.getType() != null) {
+                        GeneratorUtils.element(w, "type", afterLink.getType());
+                    }
+                    if (afterLink.getScope() != null) {
+                        GeneratorUtils.element(w, "scope", afterLink.getScope());
+                    }
+                    w.endElement(); // afterLink
+                }
+                w.endElement(); // afterLinks
+            }
+        }
+
         // ----------------------------------------------------------------------
         //
         // ----------------------------------------------------------------------