Skip to content

Commit

Permalink
Warn if plugin attribute has no public setter (apache#3195)
Browse files Browse the repository at this point in the history
The following commit modifies the annotation processor in 2.x to shell out a warning if the plugin builder attribute does not have a public setter.

A  `@SuppressWarnings("log4j.public.setter")` annotation can be used to ignore this compilation warning in case it is a known issue.
  • Loading branch information
jaykataria1111 authored Nov 29, 2024
1 parent be2d145 commit e1715dc
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ public static Builder newBuilder() {
public static class Builder implements org.apache.logging.log4j.core.util.Builder<FakePlugin> {

@PluginBuilderAttribute
@SuppressWarnings("log4j.public.setter")
private int attribute;

@PluginBuilderAttribute
@SuppressWarnings("log4j.public.setter")
private int attributeWithoutPublicSetterButWithSuppressAnnotation;

@PluginElement("layout")
private Layout<? extends Serializable> layout;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.core.config.plugins.processor;

import java.io.Serializable;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAliases;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext;
import org.apache.logging.log4j.core.config.plugins.PluginNode;
import org.apache.logging.log4j.core.config.plugins.PluginValue;

/**
* Test plugin class for unit tests.
*/
@Plugin(name = "Fake", category = "Test")
@PluginAliases({"AnotherFake", "StillFake"})
public class FakePluginPublicSetter {

@Plugin(name = "Nested", category = "Test")
public static class Nested {}

@PluginFactory
public static FakePluginPublicSetter newPlugin(
@PluginAttribute("attribute") int attribute,
@PluginElement("layout") Layout<? extends Serializable> layout,
@PluginConfiguration Configuration config,
@PluginNode Node node,
@PluginLoggerContext LoggerContext loggerContext,
@PluginValue("value") String value) {
return null;
}

public static Builder newBuilder() {
return new Builder();
}

public static class Builder implements org.apache.logging.log4j.core.util.Builder<FakePluginPublicSetter> {

@PluginBuilderAttribute
private int attribute;

@PluginBuilderAttribute
@SuppressWarnings("log4j.public.setter")
private int attributeWithoutPublicSetterButWithSuppressAnnotation;

@PluginElement("layout")
private Layout<? extends Serializable> layout;

@PluginConfiguration
private Configuration config;

@PluginNode
private Node node;

@PluginLoggerContext
private LoggerContext loggerContext;

@PluginValue("value")
private String value;

@Override
public FakePluginPublicSetter build() {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class GraalVmProcessorTest {
"fields",
asList(
asMap("name", "attribute"),
asMap("name", "attributeWithoutPublicSetterButWithSuppressAnnotation"),
asMap("name", "config"),
asMap("name", "layout"),
asMap("name", "loggerContext"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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.core.config.plugins.processor;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
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.ToolProvider;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class PluginProcessorPublicSetterTest {

private static final String FAKE_PLUGIN_CLASS_PATH =
"src/test/java/org/apache/logging/log4j/core/config/plugins/processor/" + FakePlugin.class.getSimpleName()
+ "PublicSetter.java.source";

private File createdFile;
private DiagnosticCollector<JavaFileObject> diagnosticCollector;
private List<Diagnostic<? extends JavaFileObject>> errorDiagnostics;

@BeforeEach
void setup() {
// Instantiate the tooling
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
diagnosticCollector = new DiagnosticCollector<>();
final StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, Locale.ROOT, UTF_8);

// Get the source files
Path sourceFile = Paths.get(FAKE_PLUGIN_CLASS_PATH);

assertThat(sourceFile).exists();

final File orig = sourceFile.toFile();
createdFile = new File(orig.getParentFile(), "FakePluginPublicSetter.java");
assertDoesNotThrow(() -> FileUtils.copyFile(orig, createdFile));

// get compilation units
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(createdFile);

JavaCompiler.CompilationTask task = compiler.getTask(
null,
fileManager,
diagnosticCollector,
Arrays.asList("-proc:only", "-processor", PluginProcessor.class.getName()),
null,
compilationUnits);
task.call();

errorDiagnostics = diagnosticCollector.getDiagnostics().stream()
.filter(diagnostic -> diagnostic.getKind() == Diagnostic.Kind.ERROR)
.collect(Collectors.toList());
}

@AfterEach
void tearDown() {
assertDoesNotThrow(() -> FileUtils.delete(createdFile));
File pluginsDatFile = Paths.get("Log4j2Plugins.dat").toFile();
if (pluginsDatFile.exists()) {
pluginsDatFile.delete();
}
}

@Test
void warnWhenPluginBuilderAttributeLacksPublicSetter() {

assertThat(errorDiagnostics).anyMatch(errorMessage -> errorMessage
.getMessage(Locale.ROOT)
.contains("The field `attribute` does not have a public setter"));
}

@Test
void ignoreWarningWhenSuppressWarningsIsPresent() {
assertThat(errorDiagnostics)
.allMatch(
errorMessage -> !errorMessage
.getMessage(Locale.ROOT)
.contains(
"The field `attributeWithoutPublicSetterButWithSuppressAnnotation` does not have a public setter"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static class Builder<B extends Builder<B>> extends AbstractDatabaseAppend
private ConnectionSource connectionSource;

@PluginBuilderAttribute
@SuppressWarnings("log4j.public.setter")
private boolean immediateFail;

@PluginBuilderAttribute
Expand All @@ -80,6 +81,7 @@ public static class Builder<B extends Builder<B>> extends AbstractDatabaseAppend

// TODO Consider moving up to AbstractDatabaseAppender.Builder.
@PluginBuilderAttribute
@SuppressWarnings("log4j.public.setter")
private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand All @@ -39,14 +41,19 @@
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementVisitor;
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.Elements;
import javax.lang.model.util.SimpleElementVisitor7;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAliases;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.util.Strings;

/**
Expand All @@ -60,6 +67,8 @@ public class PluginProcessor extends AbstractProcessor {

private static final Element[] EMPTY_ELEMENT_ARRAY = {};

private static final String SUPPRESS_WARNING_PUBLIC_SETTER_STRING = "log4j.public.setter";

/**
* The location of the plugin cache data file. This file is written to by this processor, and read from by
* {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}.
Expand All @@ -83,6 +92,12 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
collectPlugins(elements);
processedElements.addAll(elements);

// process plugin builder Attributes
final Set<? extends Element> pluginAttributeBuilderElements =
roundEnv.getElementsAnnotatedWith(PluginBuilderAttribute.class);
processBuilderAttribute(pluginAttributeBuilderElements);
processedElements.addAll(pluginAttributeBuilderElements);
}
// Write the cache file
if (roundEnv.processingOver() && !processedElements.isEmpty()) {
Expand All @@ -107,6 +122,88 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
return false;
}

private void processBuilderAttribute(final Iterable<? extends Element> elements) {
for (final Element element : elements) {
if (element instanceof VariableElement) {
processBuilderAttribute((VariableElement) element);
}
}
}

private void processBuilderAttribute(final VariableElement element) {
final String fieldName = element.getSimpleName().toString(); // Getting the name of the field
SuppressWarnings suppress = element.getAnnotation(SuppressWarnings.class);
if (suppress != null && Arrays.asList(suppress.value()).contains(SUPPRESS_WARNING_PUBLIC_SETTER_STRING)) {
// Suppress the warning due to annotation
return;
}
final Element enclosingElement = element.getEnclosingElement();
// `element is a field
if (enclosingElement instanceof TypeElement) {
final TypeElement typeElement = (TypeElement) enclosingElement;
// Check the siblings of the field
for (final Element enclosedElement : typeElement.getEnclosedElements()) {
// `enclosedElement is a method or constructor
if (enclosedElement instanceof ExecutableElement) {
final ExecutableElement methodElement = (ExecutableElement) enclosedElement;
final String methodName = methodElement.getSimpleName().toString();

if ((methodName.toLowerCase(Locale.ROOT).startsWith("set") // Typical pattern for setters
|| methodName
.toLowerCase(Locale.ROOT)
.startsWith("with")) // Typical pattern for setters
&& methodElement.getParameters().size()
== 1 // It is a weird pattern to not have public setter
) {

Types typeUtils = processingEnv.getTypeUtils();

boolean followsNamePattern = methodName.equals(
String.format("set%s", expectedFieldNameInASetter(fieldName)))
|| methodName.equals(String.format("with%s", expectedFieldNameInASetter(fieldName)));

// Check if method is public
boolean isPublicMethod = methodElement.getModifiers().contains(Modifier.PUBLIC);

// Check if the return type of the method element is Assignable.
// Assuming it is a builder class the type of it should be assignable to its parent
boolean checkForAssignable = typeUtils.isAssignable(
methodElement.getReturnType(),
methodElement.getEnclosingElement().asType());

boolean foundPublicSetter = followsNamePattern && checkForAssignable && isPublicMethod;
if (foundPublicSetter) {
// Hurray we found a public setter for the field!
return;
}
}
}
}
// If the setter was not found generate a compiler warning.
processingEnv
.getMessager()
.printMessage(
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 the compilation error. ",
fieldName, SUPPRESS_WARNING_PUBLIC_SETTER_STRING),
element);
}
}

/**
* Helper method to get the expected Method name in a field.
* For example if the field name is 'isopen', then the expected setter would be 'setOpen' or 'withOpen'
* This method is supposed to return the capitalized 'Open', fieldName which is expected in the setter.
* @param fieldName who's setter we are checking.
* @return The expected fieldName that will come after withxxxx or setxxxx
*/
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 void collectPlugins(final Iterable<? extends Element> elements) {
final Elements elementUtils = processingEnv.getElementUtils();
final ElementVisitor<PluginEntry, Plugin> pluginVisitor = new PluginElementVisitor(elementUtils);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ public static SocketPerformancePreferences newBuilder() {

@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) {
Expand Down
Loading

0 comments on commit e1715dc

Please sign in to comment.