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;
+ }
+ }
+}