Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class PluginProcessorPublicSetterTest {

Expand All @@ -50,6 +52,10 @@ public class PluginProcessorPublicSetterTest {

@BeforeEach
void setup() {
setupWithOptions();
}

private void setupWithOptions(final String... extraOptions) {
// Instantiate the tooling
final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
diagnosticCollector = new DiagnosticCollector<>();
Expand All @@ -67,13 +73,12 @@ void setup() {
// 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);
final List<String> args =
new java.util.ArrayList<>(Arrays.asList("-proc:only", "-processor", PluginProcessor.class.getName()));
args.addAll(Arrays.asList(extraOptions));

JavaCompiler.CompilationTask task =
compiler.getTask(null, fileManager, diagnosticCollector, args, null, compilationUnits);
task.call();

errorDiagnostics = diagnosticCollector.getDiagnostics().stream()
Expand All @@ -92,7 +97,6 @@ void tearDown() {

@Test
void warnWhenPluginBuilderAttributeLacksPublicSetter() {

assertThat(errorDiagnostics).anyMatch(errorMessage -> errorMessage
.getMessage(Locale.ROOT)
.contains("The field `attribute` does not have a public setter"));
Expand All @@ -107,4 +111,56 @@ void ignoreWarningWhenSuppressWarningsIsPresent() {
.contains(
"The field `attributeWithoutPublicSetterButWithSuppressAnnotation` does not have a public setter"));
}

@Test
void noteEmittedByDefault() {
final List<Diagnostic<? extends JavaFileObject>> noteDiagnostics = diagnosticCollector.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.NOTE)
.collect(Collectors.toList());
assertThat(noteDiagnostics).anyMatch(d -> d.getMessage(Locale.ROOT).contains("writing plugin descriptor"));
}

@Test
void notesSuppressedWhenMinKindIsError() {
setupWithOptions("-A" + PluginProcessor.MIN_ALLOWED_MESSAGE_KIND_OPTION + "=ERROR");

final List<Diagnostic<? extends JavaFileObject>> noteDiagnostics = diagnosticCollector.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.NOTE)
.collect(Collectors.toList());
assertThat(noteDiagnostics).noneMatch(d -> d.getMessage(Locale.ROOT).contains("writing plugin descriptor"));
}

@Test
void errorsStillEmittedWhenMinKindIsError() {
setupWithOptions("-A" + PluginProcessor.MIN_ALLOWED_MESSAGE_KIND_OPTION + "=ERROR");

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

@ParameterizedTest
@ValueSource(strings = {"NOTE", "note"})
void explicitNoteKindBehavesLikeDefault(final String kindValue) {
setupWithOptions("-A" + PluginProcessor.MIN_ALLOWED_MESSAGE_KIND_OPTION + "=" + kindValue);

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

final List<Diagnostic<? extends JavaFileObject>> noteDiagnostics = diagnosticCollector.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.NOTE)
.collect(Collectors.toList());
assertThat(noteDiagnostics).anyMatch(d -> d.getMessage(Locale.ROOT).contains("writing plugin descriptor"));
}

@Test
void invalidKindValueEmitsWarning() {
setupWithOptions("-A" + PluginProcessor.MIN_ALLOWED_MESSAGE_KIND_OPTION + "=INVALID");

final List<Diagnostic<? extends JavaFileObject>> warningDiagnostics =
diagnosticCollector.getDiagnostics().stream()
.filter(d -> d.getKind() == Diagnostic.Kind.WARNING)
.collect(Collectors.toList());
assertThat(warningDiagnostics)
.anyMatch(d -> d.getMessage(Locale.ROOT).contains("unrecognized value `INVALID`"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
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.ElementVisitor;
Expand All @@ -61,6 +63,7 @@
*/
@ServiceProvider(value = Processor.class, resolution = Resolution.OPTIONAL)
@SupportedAnnotationTypes("org.apache.logging.log4j.core.config.plugins.Plugin")
@SupportedOptions("log4j.plugin.processor.minAllowedMessageKind")
public class PluginProcessor extends AbstractProcessor {

// TODO: this could be made more abstract to allow for compile-time and run-time plugin processing
Expand All @@ -69,6 +72,21 @@ public class PluginProcessor extends AbstractProcessor {

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

/**
* Annotation processor option that controls the minimum {@link Diagnostic.Kind} of messages emitted by this
* processor.
* <p>
* Some build environments (e.g. Maven with {@code -Werror}) treat compiler notes or warnings as errors.
* Setting this option to {@code WARNING} or {@code ERROR} suppresses informational notes emitted during
* normal processing.
* </p>
* <p>
* Accepted values (case-insensitive): {@code NOTE}, {@code WARNING}, {@code MANDATORY_WARNING},
* {@code ERROR}, {@code OTHER}. Defaults to {@code NOTE}.
* </p>
*/
static final String MIN_ALLOWED_MESSAGE_KIND_OPTION = "log4j.plugin.processor.minAllowedMessageKind";

/**
* 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 @@ -78,15 +96,58 @@ public class PluginProcessor extends AbstractProcessor {

private final List<Element> processedElements = new ArrayList<>();
private final PluginCache pluginCache = new PluginCache();
private Diagnostic.Kind minAllowedMessageKind = Diagnostic.Kind.NOTE;

@Override
public void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
final String kindValue = processingEnv.getOptions().get(MIN_ALLOWED_MESSAGE_KIND_OPTION);
if (kindValue != null) {
try {
minAllowedMessageKind = Diagnostic.Kind.valueOf(kindValue.toUpperCase(Locale.ROOT));
} catch (final IllegalArgumentException e) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.WARNING,
String.format(
"%s: unrecognized value `%s` for option `%s`, using default `%s`. Valid values: %s",
PluginProcessor.class.getSimpleName(),
kindValue,
MIN_ALLOWED_MESSAGE_KIND_OPTION,
Diagnostic.Kind.NOTE,
Arrays.toString(Diagnostic.Kind.values())));
}
}
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

/**
* Prints a message via the {@link Messager} only if {@code kind} is at least as severe as the configured
* {@link #MIN_ALLOWED_MESSAGE_KIND_OPTION minimum allowed kind}.
*/
private void printMessage(final Diagnostic.Kind kind, final String message) {
if (kind.ordinal() <= minAllowedMessageKind.ordinal()) {
processingEnv.getMessager().printMessage(kind, message);
}
}

/**
* Prints a message via the {@link Messager} only if {@code kind} is at least as severe as the configured
* {@link #MIN_ALLOWED_MESSAGE_KIND_OPTION minimum allowed kind}.
*/
private void printMessage(final Diagnostic.Kind kind, final String message, final Element element) {
if (kind.ordinal() <= minAllowedMessageKind.ordinal()) {
processingEnv.getMessager().printMessage(kind, message, element);
}
}

@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
final Messager messager = processingEnv.getMessager();
// Process the elements for this round
if (!annotations.isEmpty()) {
final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
Expand All @@ -102,7 +163,7 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
// Write the cache file
if (roundEnv.processingOver() && !processedElements.isEmpty()) {
try {
messager.printMessage(
printMessage(
Diagnostic.Kind.NOTE,
String.format(
"%s: writing plugin descriptor for %d Log4j Plugins to `%s`.",
Expand All @@ -115,7 +176,7 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
.append(PLUGIN_CACHE_FILE)
.append("\n");
e.printStackTrace(new PrintWriter(sw));
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, sw.toString());
printMessage(Diagnostic.Kind.ERROR, sw.toString());
}
}
// Do not claim the annotations to allow other annotation processors to run
Expand Down Expand Up @@ -177,14 +238,12 @@ private void processBuilderAttribute(final VariableElement element) {
}
}
// 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);
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);
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/changelog/.2.x.x/plugin_processor_min_allowed_message_kind.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="https://logging.apache.org/xml/ns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="added">
<description format="asciidoc">
Add `log4j.plugin.processor.minAllowedMessageKind` annotation processor option to `PluginProcessor` to filter diagnostic messages by severity.
This allows builds that treat compiler notes as errors (e.g. Maven with `-Werror`) to suppress informational notes emitted during normal plugin processing.
</description>
</entry>
33 changes: 33 additions & 0 deletions src/site/antora/modules/ROOT/pages/manual/plugins.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,39 @@ The `GraalVmProcessor` requires your project's `groupId` and `artifactId` to cor
Provide these values to the processor using the `log4j.graalvm.groupId` and `log4j.graalvm.artifactId` annotation processor options.
====

.Suppressing notes from `PluginProcessor` in strict build environments
[%collapsible]
====
Some build environments treat all compiler notes or warnings as errors (e.g., Maven with `-Werror` or Gradle with `options.compilerArgs << '-Werror'`).
By default, `PluginProcessor` emits a `NOTE`-level diagnostic when it writes the plugin descriptor, which can cause the build to fail in those environments.

To suppress these informational notes, pass the `log4j.plugin.processor.minAllowedMessageKind` annotation processor option with a value of `WARNING` or `ERROR`.
This instructs the processor to only emit diagnostics at or above the specified severity, silencing routine notes while preserving genuine warnings and errors.

Accepted values (case-insensitive): `NOTE` (default), `WARNING`, `MANDATORY_WARNING`, `ERROR`, `OTHER`.

[tabs]
=====
Maven::
+
[source,xml]
----
<compilerArgs>
<arg>-Alog4j.plugin.processor.minAllowedMessageKind=WARNING</arg>
</compilerArgs>
----

Gradle::
+
[source,groovy]
----
compileJava {
options.compilerArgs << '-Alog4j.plugin.processor.minAllowedMessageKind=WARNING'
}
----
=====
====

You need to configure your build tool as follows to use both plugin processors:

[tabs]
Expand Down