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

ramanathan1504 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/main by this push:
     new c4c75fdf97 [main] Warn when `@PluginBuilderAttribute` field lacks 
public setter (#3195 port) (#4156)
c4c75fdf97 is described below

commit c4c75fdf97a06689f0940f15bbdffcb37ae6410f
Author: Vasily Pelikh <[email protected]>
AuthorDate: Fri Aug 21 19:52:04 2026 +0300

    [main] Warn when `@PluginBuilderAttribute` field lacks public setter (#3195 
port) (#4156)
    
    * Warn when @PluginBuilderAttribute field lacks public setter
    
    The annotation processor now validates that every 
@PluginBuilderAttributevfield in a plugin builder class has an accessible 
public setter method (setXxx or withXxx). If no setter is found, a compilation 
ERROR is emitted.
    
    Use @SuppressWarnings("log4j.public.setter") on the field to suppress.
    
    Added @SuppressWarnings to known false positives in:
    - SocketPerformancePreferences (3 fields: setters return void, not builder)
    - StringMatchFilter (field 'text' has setter named 'setMatchString')
    - Rfc5424Layout (field 'enterpriseNumber' has no setter at all)
    
    * Fix JdbcAppender builder setters to return fluent builder type
    
    The annotation processor requires @PluginBuilderAttribute setters to return 
a type assignable to the enclosing builder class. Changed setImmediateFail() 
and setReconnectIntervalMillis() from void to B, following the same pattern as 
all other setters in the Builder class.
    
    Fixes compilation errors caught by the new annotation processor validation:
    - The field immediateFail does not have a public setter.
    - The field reconnectIntervalMillis does not have a public setter.
    
    Signed-off-by: Vasily Pelikh <[email protected]>
    Co-authored-by: Ramanathan <[email protected]>
---
 .../logging/log4j/core/layout/Rfc5424Layout.java   |   5 +
 .../core/net/SocketPerformancePreferences.java     |   3 +
 .../logging/log4j/jdbc/appender/JdbcAppender.java  |   6 +-
 .../log4j/plugin/processor/PluginProcessor.java    |  78 +++++++++++++++
 .../processor/PluginProcessorPublicSetterTest.java | 106 +++++++++++++++++++++
 .../setter-test/FakePluginPublicSetter.java        |  45 +++++++++
 6 files changed, 241 insertions(+), 2 deletions(-)

diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
index 11bbccb8c5..c758e14b75 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java
@@ -702,6 +702,11 @@ public final class Rfc5424Layout extends 
AbstractStringLayout {
             return this;
         }
 
+        public Rfc5424LayoutBuilder setEnterpriseNumber(final Integer 
enterpriseNumber) {
+            this.enterpriseNumber = enterpriseNumber;
+            return this;
+        }
+
         public Rfc5424LayoutBuilder setIncludeMDC(final boolean includeMDC) {
             this.includeMDC = includeMDC;
             return this;
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SocketPerformancePreferences.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SocketPerformancePreferences.java
index 8f00192aef..1739e9cd55 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SocketPerformancePreferences.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SocketPerformancePreferences.java
@@ -41,14 +41,17 @@ public class SocketPerformancePreferences implements 
Builder<SocketPerformancePr
 
     @PluginBuilderAttribute
     @Required
+    @SuppressWarnings("log4j.public.setter")
     private int bandwidth;
 
     @PluginBuilderAttribute
     @Required
+    @SuppressWarnings("log4j.public.setter")
     private int connectionTime;
 
     @PluginBuilderAttribute
     @Required
+    @SuppressWarnings("log4j.public.setter")
     private int latency;
 
     public void apply(final Socket socket) {
diff --git 
a/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcAppender.java
 
b/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcAppender.java
index 80122ffd61..e169d240a9 100644
--- 
a/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcAppender.java
+++ 
b/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcAppender.java
@@ -158,12 +158,14 @@ public final class JdbcAppender extends 
AbstractDatabaseAppender<JdbcDatabaseMan
             return asBuilder();
         }
 
-        public void setImmediateFail(final boolean immediateFail) {
+        public B setImmediateFail(final boolean immediateFail) {
             this.immediateFail = immediateFail;
+            return asBuilder();
         }
 
-        public void setReconnectIntervalMillis(final long 
reconnectIntervalMillis) {
+        public B setReconnectIntervalMillis(final long 
reconnectIntervalMillis) {
             this.reconnectIntervalMillis = reconnectIntervalMillis;
+            return asBuilder();
         }
 
         /**
diff --git 
a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
 
b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
index ff41a4fdc9..bbfa5aea09 100644
--- 
a/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
+++ 
b/log4j-plugin-processor/src/main/java/org/apache/logging/log4j/plugin/processor/PluginProcessor.java
@@ -40,9 +40,14 @@ import javax.annotation.processing.RoundEnvironment;
 import javax.annotation.processing.SupportedAnnotationTypes;
 import javax.lang.model.SourceVersion;
 import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+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.util.ElementFilter;
 import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
 import javax.tools.Diagnostic.Kind;
 import javax.tools.FileObject;
 import javax.tools.JavaFileObject;
@@ -92,6 +97,12 @@ public class PluginProcessor extends AbstractProcessor {
      */
     public static final String PLUGIN_PACKAGE = "log4j.plugin.package";
 
+    private static final String SUPPRESS_WARNING_PUBLIC_SETTER = 
"log4j.public.setter";
+
+    private static final List<String> BUILDER_ATTRIBUTE_ANNOTATIONS = List.of(
+            "org.apache.logging.log4j.plugins.PluginBuilderAttribute",
+            
"org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute");
+
     private static final String SERVICE_FILE_NAME =
             
"META-INF/services/org.apache.logging.log4j.plugins.model.PluginService";
 
@@ -124,6 +135,7 @@ public class PluginProcessor extends AbstractProcessor {
         if (!annotations.isEmpty()) {
             
processPluginAnnotatedClasses(ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Plugin.class)));
         }
+        processBuilderAttributeFields(roundEnv);
         // Write the generated code
         if (roundEnv.processingOver() && !pluginIndex.isEmpty()) {
             try {
@@ -164,6 +176,72 @@ public class PluginProcessor extends AbstractProcessor {
         }
     }
 
+    private void processBuilderAttributeFields(final RoundEnvironment 
roundEnv) {
+        final Elements elements = processingEnv.getElementUtils();
+        for (final String annotationFqn : BUILDER_ATTRIBUTE_ANNOTATIONS) {
+            final TypeElement annotationType = 
elements.getTypeElement(annotationFqn);
+            if (annotationType == null) {
+                continue;
+            }
+            for (final Element element : 
roundEnv.getElementsAnnotatedWith(annotationType)) {
+                if (element instanceof VariableElement) {
+                    processBuilderAttributeField((VariableElement) element);
+                }
+            }
+        }
+    }
+
+    private void processBuilderAttributeField(final VariableElement element) {
+        final String fieldName = element.getSimpleName().toString();
+        final SuppressWarnings suppress = 
element.getAnnotation(SuppressWarnings.class);
+        if (suppress != null && 
Arrays.asList(suppress.value()).contains(SUPPRESS_WARNING_PUBLIC_SETTER)) {
+            return;
+        }
+        final Element enclosingElement = element.getEnclosingElement();
+        if (enclosingElement instanceof TypeElement) {
+            final TypeElement typeElement = (TypeElement) enclosingElement;
+            for (final Element enclosedElement : 
typeElement.getEnclosedElements()) {
+                if (enclosedElement instanceof ExecutableElement) {
+                    final ExecutableElement methodElement = 
(ExecutableElement) enclosedElement;
+                    final String methodName = 
methodElement.getSimpleName().toString();
+                    if ((methodName.toLowerCase(Locale.ROOT).startsWith("set")
+                                    || 
methodName.toLowerCase(Locale.ROOT).startsWith("with"))
+                            && methodElement.getParameters().size() == 1) {
+                        final Types typeUtils = processingEnv.getTypeUtils();
+                        final boolean followsNamePattern = methodName.equals(
+                                        String.format("set%s", 
expectedFieldNameInASetter(fieldName)))
+                                || methodName.equals(String.format("with%s", 
expectedFieldNameInASetter(fieldName)));
+                        final boolean isPublicMethod =
+                                
methodElement.getModifiers().contains(Modifier.PUBLIC);
+                        final boolean checkForAssignable = 
typeUtils.isAssignable(
+                                methodElement.getReturnType(),
+                                methodElement.getEnclosingElement().asType());
+                        final boolean foundPublicSetter = followsNamePattern 
&& checkForAssignable && isPublicMethod;
+                        if (foundPublicSetter) {
+                            return;
+                        }
+                    }
+                }
+            }
+            processingEnv
+                    .getMessager()
+                    .printMessage(
+                            javax.tools.Diagnostic.Kind.ERROR,
+                            String.format(
+                                    "The field `%s` does not have a public 
setter. "
+                                            + "Note that 
@SuppressWarnings(\"%s\") can be used on the field to suppress this error.",
+                                    fieldName, SUPPRESS_WARNING_PUBLIC_SETTER),
+                            element);
+        }
+    }
+
+    private static String expectedFieldNameInASetter(String fieldName) {
+        if (fieldName.startsWith("is")) {
+            fieldName = fieldName.substring(2);
+        }
+        return fieldName.isEmpty() ? fieldName : 
Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
+    }
+
     private static void processConfigurableAnnotation(TypeElement pluginClass, 
PluginEntry.Builder builder) {
         var configurable = pluginClass.getAnnotation(Configurable.class);
         if (configurable != null) {
diff --git 
a/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorPublicSetterTest.java
 
b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorPublicSetterTest.java
new file mode 100644
index 0000000000..5e59854503
--- /dev/null
+++ 
b/log4j-plugin-processor/src/test/java/org/apache/logging/log4j/plugin/processor/PluginProcessorPublicSetterTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.logging.log4j.plugin.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class PluginProcessorPublicSetterTest {
+
+    private static final String FAKE_PLUGIN_SOURCE = 
"/setter-test/FakePluginPublicSetter.java";
+
+    private DiagnosticCollector<JavaFileObject> diagnosticCollector;
+    private List<Diagnostic<? extends JavaFileObject>> errorDiagnostics;
+
+    @TempDir
+    private Path outputDir;
+
+    @BeforeEach
+    void setup() throws Exception {
+        final URL fakePluginUrl = 
PluginProcessorTest.class.getResource(FAKE_PLUGIN_SOURCE);
+        assertThat(fakePluginUrl).isNotNull();
+        final Path fakePluginPath = Paths.get(fakePluginUrl.toURI());
+        diagnosticCollector = new DiagnosticCollector<>();
+        try {
+            final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+            final StandardJavaFileManager fileManager =
+                    compiler.getStandardFileManager(diagnosticCollector, 
Locale.ROOT, UTF_8);
+            try {
+                fileManager.setLocation(StandardLocation.CLASS_OUTPUT, 
Set.of(outputDir.toFile()));
+                fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, 
Set.of(outputDir.toFile()));
+                final JavaCompiler.CompilationTask task = compiler.getTask(
+                        null,
+                        fileManager,
+                        diagnosticCollector,
+                        List.of("-proc:only", "-processor", 
PluginProcessor.class.getName()),
+                        null,
+                        fileManager.getJavaFileObjects(fakePluginPath));
+                task.call();
+            } finally {
+                fileManager.close();
+            }
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+        errorDiagnostics = diagnosticCollector.getDiagnostics().stream()
+                .filter(diagnostic -> diagnostic.getKind() == 
Diagnostic.Kind.ERROR)
+                .collect(Collectors.toList());
+    }
+
+    @Test
+    void warnWhenPluginBuilderAttributeLacksPublicSetter() {
+        assertThat(errorDiagnostics).hasSize(1);
+        assertThat(errorDiagnostics).anyMatch(errorMessage -> errorMessage
+                .getMessage(Locale.ROOT)
+                .contains("The field `attributeWithoutPublicSetter` does not 
have a public setter"));
+    }
+
+    @Test
+    void ignoreWarningWhenSuppressWarningsIsPresent() {
+        assertThat(errorDiagnostics).hasSize(1);
+        assertThat(errorDiagnostics).allMatch(errorMessage -> !errorMessage
+                .getMessage(Locale.ROOT)
+                .contains("The field 
`attributeWithoutPublicSetterButWithSuppressAnnotation`"
+                        + " does not have a public setter"));
+    }
+
+    @Test
+    void noWarningWhenPublicSetterExists() {
+        assertThat(errorDiagnostics).hasSize(1);
+        assertThat(errorDiagnostics).allMatch(errorMessage -> !errorMessage
+                .getMessage(Locale.ROOT)
+                .contains("The field `attribute` does not have a public 
setter"));
+    }
+}
diff --git 
a/log4j-plugin-processor/src/test/resources/setter-test/FakePluginPublicSetter.java
 
b/log4j-plugin-processor/src/test/resources/setter-test/FakePluginPublicSetter.java
new file mode 100644
index 0000000000..92a87bd324
--- /dev/null
+++ 
b/log4j-plugin-processor/src/test/resources/setter-test/FakePluginPublicSetter.java
@@ -0,0 +1,45 @@
+/*
+ * 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 example;
+
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
+
+/**
+ * Test plugin class for unit tests of public setter validation.
+ */
+@Plugin("FakePluginPublicSetter")
+public class FakePluginPublicSetter {
+
+    public static class Builder {
+
+        @PluginBuilderAttribute
+        private int attribute;
+
+        @PluginBuilderAttribute
+        @SuppressWarnings("log4j.public.setter")
+        private int attributeWithoutPublicSetterButWithSuppressAnnotation;
+
+        @PluginBuilderAttribute
+        private int attributeWithoutPublicSetter;
+
+        public Builder setAttribute(final int attribute) {
+            this.attribute = attribute;
+            return this;
+        }
+    }
+}

Reply via email to