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


##########
impl/maven-plugin-mixin/src/main/java/org/apache/maven/plugin/processor/MojoDescriptorProcessor.java:
##########
@@ -0,0 +1,687 @@
+/*
+ * 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.2.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,
+    MojoDescriptorProcessor.OPT_NAME
+})
+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";
+    static final String OPT_NAME = "maven.plugin.name";
+
+    private final Map<String, TypeElement> mojoElements = new 
LinkedHashMap<>();
+    private final Set<String> generatedFactoryNames = new LinkedHashSet<>();
+    private String[] resolvedCoordinates;
+    private boolean coordinateErrorEmitted = false;
+
+    @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 (IOException e) {
+                    error("Failed to generate plugin.xml", e);
+                }
+                try {
+                    generateHelpMojo(coords);
+                } catch (IOException e) {
+                    error("Failed to generate HelpMojo", 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);
+        String name = getOption(OPT_NAME);
+        if (isBlank(goalPrefix)) {
+            goalPrefix = deriveGoalPrefix(artifactId);
+        }
+        if (isBlank(name)) {
+            name = artifactId;
+        }
+        if (isBlank(groupId) || isBlank(artifactId) || isBlank(version)) {
+            if (!coordinateErrorEmitted) {
+                coordinateErrorEmitted = true;
+                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, name};
+        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[4])
+                .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());
+                }
+                List<? extends TypeMirror> thrown = ctor.getThrownTypes();
+                if (!thrown.isEmpty()) {
+                    pw.print(") throws ");
+                    pw.print(thrown.stream()
+                            .map(Object::toString)
+                            .reduce((a, b) -> a + ", " + b)
+                            .orElse(""));
+                    pw.println(" { super(" + args + "); }");
+                } else {
+                    pw.println(") { super(" + args + "); }");
+                }
+            }
+            pw.println("}");
+        }
+        generatedFactoryNames.add(factoryFqn);
+        note("Generated factory " + factoryFqn);
+    }
+
+    private void generateHelpMojo(String[] coords) throws IOException {
+        // Use the first discovered mojo's package for the generated HelpMojo.
+        String pkg = mojoElements.values().stream()
+                .map(te -> {
+                    String fqn = te.getQualifiedName().toString();
+                    int dot = fqn.lastIndexOf('.');
+                    return dot > 0 ? fqn.substring(0, dot) : "";
+                })
+                .findFirst()
+                .orElse("");
+
+        String helpFqn = pkg.isEmpty() ? "HelpMojo" : pkg + ".HelpMojo";
+        // Idempotency guard: JSR 269 fires a final round with 
processingOver()==true
+        // even when no new files were generated; without this guard, 
createSourceFile()
+        // would throw FilerException on the second call.
+        if (generatedFactoryNames.contains(helpFqn)) {
+            return;
+        }
+
+        String packageDecl = pkg.isEmpty() ? "" : "package " + pkg + ";\n\n";
+        String goalPrefix = coords[3];
+        String artifactId = coords[1];
+        String version = coords[2];
+
+        String source = """
+                %simport org.apache.maven.api.Session;
+                import org.apache.maven.api.di.Inject;
+                import org.apache.maven.api.plugin.Log;
+                import org.apache.maven.api.plugin.MojoException;
+                import org.apache.maven.api.plugin.annotations.Mojo;
+                import org.apache.maven.api.plugin.annotations.Parameter;
+                import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
+                import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
+                import org.apache.maven.api.services.MessageBuilder;
+                import org.apache.maven.api.services.MessageBuilderFactory;
+                import org.apache.maven.api.services.xml.PluginXmlFactory;
+                import org.apache.maven.api.services.xml.XmlReaderException;
+                import java.io.IOException;
+                import java.io.InputStream;
+                import java.util.List;
+
+                /**
+                 * Display help information on %s.<br>
+                 * Call <code>mvn %s:help -Ddetail=true 
-Dgoal=&lt;goal-name&gt;</code>
+                 * to display parameter details.
+                 *
+                 * @since %s
+                 */
+                @Mojo(name = "help", projectRequired = false)
+                public class HelpMojo implements 
org.apache.maven.api.plugin.Mojo {
+
+                    @Inject
+                    private Session session;
+
+                    @Inject
+                    private Log logger;
+
+                    /** If {@code true}, display all settable properties for 
each goal. */
+                    @Parameter(property = "detail", defaultValue = "false")
+                    private boolean detail;
+
+                    /** The name of the goal for which to show help. If 
unspecified, all goals will be displayed. */
+                    @Parameter(property = "goal")
+                    private String goal;
+
+                    /** The number of spaces per indentation level, should be 
positive. */
+                    @Parameter(property = "indentSize", defaultValue = "2")
+                    private int indentSize;
+
+                    private static final String PLUGIN_XML_PATH = 
"/META-INF/maven/plugin.xml";
+
+                    @Override
+                    public void execute() throws MojoException {
+                        if (indentSize <= 0) {
+                            logger.warn("The parameter 'indentSize' should be 
positive, using '2' as default.");
+                            indentSize = 2;
+                        }
+                        MessageBuilderFactory mbf = 
session.getService(MessageBuilderFactory.class);
+                        int lineLength = mbf.getTerminalWidth();
+                        if (lineLength <= 0) {
+                            lineLength = 80;
+                        }
+                        PluginDescriptor descriptor = loadDescriptor();
+                        MessageBuilder mb = mbf.builder();
+                        String name = descriptor.getName();
+                        String ver = descriptor.getVersion();
+                        String id = descriptor.getGroupId() + ":" + 
descriptor.getArtifactId() + ":" + ver;
+                        String title = (name != null && !name.isEmpty() && 
!name.contains(id))
+                                ? name + " " + ver : (name != null && 
!name.isEmpty() ? name : id);
+                        mb.strong(title).newline();
+                        if (descriptor.getDescription() != null) {
+                            appendWrapped(mb, descriptor.getDescription(), 1, 
lineLength);
+                        }
+                        mb.newline();
+                        List<MojoDescriptor> mojos = descriptor.getMojos();
+                        if (goal == null || goal.isEmpty()) {
+                            mb.a("This plugin has 
").strong(String.valueOf(mojos.size()))
+                                    .a(mojos.size() > 1 ? " goals:" : " 
goal:").newline();
+                            mb.newline();
+                        }
+                        for (MojoDescriptor mojo : mojos) {
+                            writeGoal(mb, descriptor.getGoalPrefix(), mojo, 
lineLength);
+                        }
+                        if (logger.isInfoEnabled()) {
+                            logger.info(mb.build());
+                        }
+                    }
+
+                    private PluginDescriptor loadDescriptor() throws 
MojoException {
+                        try (InputStream is = 
getClass().getResourceAsStream(PLUGIN_XML_PATH)) {
+                            if (is == null) {
+                                throw new MojoException("Could not find plugin 
descriptor at " + PLUGIN_XML_PATH);
+                            }
+                            return 
session.getService(PluginXmlFactory.class).read(is);
+                        } catch (IOException | XmlReaderException e) {
+                            throw new MojoException("Failed to parse " + 
PLUGIN_XML_PATH + ": " + e.getMessage(), e);
+                        }
+                    }
+
+                    private void writeGoal(MessageBuilder mb, String gp, 
MojoDescriptor mojo, int lineLength) {
+                        if (goal != null && !goal.isEmpty() && 
!goal.equals(mojo.getGoal())) {
+                            return;
+                        }
+                        mb.mojo(gp + ":" + mojo.getGoal()).newline();
+                        if (mojo.getDeprecated() != null && 
!mojo.getDeprecated().isEmpty()) {
+                            mb.a(indent(1)).warning("Deprecated.").a(" 
").a(mojo.getDeprecated()).newline();
+                            if (detail && mojo.getDescription() != null) {
+                                mb.newline();
+                                appendWrapped(mb, mojo.getDescription(), 1, 
lineLength);
+                            }
+                        } else if (mojo.getDescription() != null) {
+                            appendWrapped(mb, mojo.getDescription(), 1, 
lineLength);
+                        }
+                        mb.newline();
+                        if (detail) {
+                            
List<org.apache.maven.api.plugin.descriptor.Parameter> params = 
mojo.getParameters()
+                                    .stream()
+                                    
.filter(org.apache.maven.api.plugin.descriptor.Parameter::isEditable)
+                                    .toList();
+                            if (!params.isEmpty()) {
+                                mb.a(indent(1)).strong("Available 
parameters:").newline();
+                                mb.newline();
+                                params.forEach(p -> writeParameter(mb, p, 
lineLength));
+                            }
+                        }
+                    }
+
+                    private void writeParameter(
+                            MessageBuilder mb, 
org.apache.maven.api.plugin.descriptor.Parameter p, int lineLength) {
+                        String suffix = (p.getDefaultValue() != null && 
!p.getDefaultValue().isEmpty())
+                                ? " (Default: " + p.getDefaultValue() + ")" : 
"";
+                        
mb.a(indent(2)).strong(p.getName()).a(suffix).newline();
+                        if (p.getDeprecated() != null && 
!p.getDeprecated().isEmpty()) {
+                            mb.a(indent(3)).warning("Deprecated.").a(" 
").a(p.getDeprecated()).newline();
+                            mb.newline();
+                        }
+                        if (p.getDescription() != null && 
!p.getDescription().isEmpty()) {
+                            appendWrapped(mb, p.getDescription(), 3, 
lineLength);
+                        }
+                        if (p.isRequired()) {
+                            mb.a(indent(3)).a("Required: 
").strong("Yes").newline();
+                        }
+                        String expression = p.getExpression();
+                        if (expression != null && !expression.isEmpty()
+                                && expression.startsWith("${") && 
expression.endsWith("}")
+                                && !expression.substring(2).contains("${")) {
+                            mb.a(indent(3)).a("User property: ")
+                                    .info(expression.substring(2, 
expression.length() - 1)).newline();
+                        }
+                        mb.newline();
+                    }
+
+                    private String indent(int level) {
+                        return " ".repeat(level * indentSize);
+                    }
+
+                    private void appendWrapped(MessageBuilder mb, String text, 
int indent, int lineLength) {
+                        String pad = indent(indent);
+                        int maxWidth = lineLength - pad.length();
+                        for (String rawLine : text.split("(\r\n)|(\r)|(\n)")) {

Review Comment:
   🔎 **Dead variable**: `maxWidth` is computed but never used — the word-wrap 
check below uses `lineLength` directly (which is correct, since `line` already 
includes the `pad` prefix, so the total column position is tracked via 
`line.length()`).
   
   ```suggestion
                           for (String rawLine : text.split("(
   )|( )|(
   )")) {
   ```



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