diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorPublicSetterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorPublicSetterTest.java index 587490e2c92..2b6a03d1fea 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorPublicSetterTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessorPublicSetterTest.java @@ -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 { @@ -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<>(); @@ -67,13 +73,12 @@ void setup() { // get compilation units Iterable compilationUnits = fileManager.getJavaFileObjects(createdFile); - JavaCompiler.CompilationTask task = compiler.getTask( - null, - fileManager, - diagnosticCollector, - Arrays.asList("-proc:only", "-processor", PluginProcessor.class.getName()), - null, - compilationUnits); + final List 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() @@ -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")); @@ -107,4 +111,56 @@ void ignoreWarningWhenSuppressWarningsIsPresent() { .contains( "The field `attributeWithoutPublicSetterButWithSuppressAnnotation` does not have a public setter")); } + + @Test + void noteEmittedByDefault() { + final List> 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> 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> 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> 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`")); + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java index 6d07f91101f..e236a8b4f2e 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginProcessor.java @@ -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; @@ -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 @@ -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. + *

+ * 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. + *

+ *

+ * Accepted values (case-insensitive): {@code NOTE}, {@code WARNING}, {@code MANDATORY_WARNING}, + * {@code ERROR}, {@code OTHER}. Defaults to {@code NOTE}. + *

+ */ + 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}. @@ -78,15 +96,58 @@ public class PluginProcessor extends AbstractProcessor { private final List 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 annotations, final RoundEnvironment roundEnv) { - final Messager messager = processingEnv.getMessager(); // Process the elements for this round if (!annotations.isEmpty()) { final Set elements = roundEnv.getElementsAnnotatedWith(Plugin.class); @@ -102,7 +163,7 @@ public boolean process(final Set 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`.", @@ -115,7 +176,7 @@ public boolean process(final Set 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 @@ -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); } } diff --git a/src/changelog/.2.x.x/plugin_processor_min_allowed_message_kind.xml b/src/changelog/.2.x.x/plugin_processor_min_allowed_message_kind.xml new file mode 100644 index 00000000000..fb137f00a75 --- /dev/null +++ b/src/changelog/.2.x.x/plugin_processor_min_allowed_message_kind.xml @@ -0,0 +1,12 @@ + + + + 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. + + diff --git a/src/site/antora/modules/ROOT/pages/manual/plugins.adoc b/src/site/antora/modules/ROOT/pages/manual/plugins.adoc index 6b751f07366..2f598df10e7 100644 --- a/src/site/antora/modules/ROOT/pages/manual/plugins.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/plugins.adoc @@ -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] +---- + + -Alog4j.plugin.processor.minAllowedMessageKind=WARNING + +---- + +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]