Skip to content
Draft
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ under the License.

<properties>
<javaVersion>17</javaVersion>
<mavenVersion>4.0.0-rc-4</mavenVersion>
<mavenVersion>4.1.0-SNAPSHOT</mavenVersion>

<asmVersion>9.10.1</asmVersion>
<guiceVersion>7.0.0</guiceVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Parameter;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverRequest;
import org.apache.maven.api.services.DependencyResolverResult;
Expand Down Expand Up @@ -944,7 +945,11 @@ final void amendincrementalCompilation(EnumSet<IncrementalBuild.Aspect> aspects,

/**
* The logger for reporting information or warnings to the user.
* Currently, this is also used for console output.
* Also used for structured diagnostic reporting via {@link Log#problem(BuilderProblem)},
* which feeds into the build report, {@code mvnlog --diagnostics}, and warning suppression.
*
* <p>Sub-components receive child loggers (e.g. {@code logger.child("diagnostics")},
* {@code logger.child("options")}) for hierarchical naming in the log output.</p>
*
* <h4>Thread safety</h4>
* This logger should be thread-safe if the {@link ToolExecutor} is executed in a background thread.
Expand Down Expand Up @@ -1333,7 +1338,7 @@ public Options parseParameters(final OptionChecker compiler) {
* For example, Maven will check for illegal values in the "-g" option only if the compiler rejected
* the fully formatted option (e.g. "-g:vars,lines") that we provided to it.
*/
final var configuration = new Options(compiler, logger);
final var configuration = new Options(compiler, logger.child("options"));
configuration.addIfNonBlank("--source", getSource());
targetOrReleaseSet = configuration.addIfNonBlank("--target", getTarget());
targetOrReleaseSet |= configuration.setRelease(getRelease());
Expand Down Expand Up @@ -1463,7 +1468,8 @@ private void compile(final JavaCompiler compiler, final Options configuration) t
Path moduleDescriptor = executor.outputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
if (Files.isRegularFile(moduleDescriptor)) {
byte[] oridinal = Files.readAllBytes(moduleDescriptor);
byte[] modified = ByteCodeTransformer.patchJdkModuleVersion(oridinal, getRelease(), logger);
byte[] modified =
ByteCodeTransformer.patchJdkModuleVersion(oridinal, getRelease(), logger.child("bytecode"));
if (modified != null) {
Files.write(moduleDescriptor, modified);
}
Expand Down
164 changes: 106 additions & 58 deletions src/main/java/org/apache/maven/plugin/compiler/DiagnosticLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,24 @@
import java.util.Optional;

import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.services.MessageBuilder;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.api.services.BuilderProblem;

/**
* A Java compiler diagnostic listener which send the messages to the Maven logger.
* A Java compiler diagnostic listener which sends the messages to the Maven logger.
* When the logger supports structured problem reporting ({@link Log#problem(BuilderProblem)}),
* each diagnostic is also reported as a {@link BuilderProblem} with a per-location dedup key,
* enabling the build report, {@code mvnlog --diagnostics}, and warning suppression.
*
* @author Martin Desruisseaux
*/
final class DiagnosticLogger implements DiagnosticListener<JavaFileObject> {
/**
* The logger where to send diagnostics.
* The logger where to send diagnostics and structured problems.
* This should be a child logger (e.g. {@code "compiler:compile.diagnostics"})
* obtained via {@link Log#child(String)}.
*/
private final Log logger;

/**
* The factory for creating message builders.
*/
private final MessageBuilderFactory messageBuilderFactory;

/**
* The locale for compiler message.
*/
Expand Down Expand Up @@ -76,15 +75,16 @@ final class DiagnosticLogger implements DiagnosticListener<JavaFileObject> {

/**
* Creates a listener which will send the diagnostics to the given logger.
* Structured problems are reported via {@link Log#problem(BuilderProblem)},
* which handles dedup, suppression, and thread-safe interaction with the
* {@code BuildReportCollector} automatically.
*
* @param logger the logger where to send diagnostics
* @param messageBuilderFactory the factory for creating message builders
* @param logger the logger where to send diagnostics (typically a child logger)
* @param locale the locale for compiler message
* @param directory the base directory with which to relativize the paths to source files
*/
DiagnosticLogger(Log logger, MessageBuilderFactory messageBuilderFactory, Locale locale, Path directory) {
DiagnosticLogger(Log logger, Locale locale, Path directory) {
this.logger = logger;
this.messageBuilderFactory = messageBuilderFactory;
this.locale = locale;
this.directory = directory;
codeCount = new LinkedHashMap<>();
Expand All @@ -108,74 +108,121 @@ private String relativize(String file) {
}

/**
* Invoked when the compiler emitted a warning.
* Maps a {@link Diagnostic.Kind} to a {@link BuilderProblem.Severity},
* or {@code null} if the kind has no corresponding severity (e.g. {@code NOTE}).
*/
private static BuilderProblem.Severity mapSeverity(Diagnostic.Kind kind) {
return switch (kind) {
case ERROR -> BuilderProblem.Severity.ERROR;
case WARNING, MANDATORY_WARNING -> BuilderProblem.Severity.WARNING;
default -> null;
};
}

/**
* Invoked when the compiler emitted a diagnostic.
* <p>
* When the diagnostic has a code and a mappable severity (error or warning),
* a structured {@link BuilderProblem} is reported via {@link Log#problem(BuilderProblem)}
* with a per-location dedup key ({@code "compiler:<code>:<source>:<line>"}).
* This avoids double-counting with the {@code BuildReportCollector}'s WARN auto-promotion,
* because {@code Log.problem()} sets the structured-problem flag internally.
* <p>
* Informational diagnostics (notes, other) are logged at INFO level without
* creating a structured problem, since they are not actionable warnings.
*
* @param diagnostic the warning emitted by the Java compiler
* @param diagnostic the diagnostic emitted by the Java compiler
*/
@Override
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
String message = diagnostic.getMessage(locale);
if (message == null || message.isBlank()) {
return;
}
MessageBuilder record = messageBuilderFactory.builder();
record.a(message);
JavaFileObject source = diagnostic.getSource();
Diagnostic.Kind kind = diagnostic.getKind();
String style;
JavaFileObject source = diagnostic.getSource();
// Track counts for the summary
switch (kind) {
case ERROR:
style = ".error:-bold,f:red";
if (firstError == null) {
firstError = message;
}
numErrors++;
break;
case MANDATORY_WARNING:
case WARNING:
style = ".warning:-bold,f:yellow";
numWarnings++;
break;
default:
style = ".info:-bold,f:blue";
if (diagnostic.getLineNumber() == Diagnostic.NOPOS) {
source = null; // Some messages are generic, e.g. "Recompile with -Xlint:deprecation".
}
break;
}
// Statistics
String code = diagnostic.getCode();
if (code != null) {
codeCount.merge(code, 1, (old, initial) -> old + 1);
}
// Report as a structured problem when we have a diagnostic code and a mappable severity.
// Log.problem() handles both the console log and the build report,
// setting the STRUCTURED_PROBLEM_ACTIVE flag to prevent double-counting.
BuilderProblem.Severity severity = mapSeverity(kind);
if (code != null && severity != null) {
logger.problem(buildProblem(diagnostic, message, code, source, severity));
} else {
// Informational diagnostic or no code — fall back to plain logging
switch (kind) {
case ERROR -> logger.error(message);
case MANDATORY_WARNING, WARNING -> logger.warn(message);
default -> logger.info(message);
}
}
}

/**
* Builds a structured {@link BuilderProblem} from the given compiler diagnostic.
* Each diagnostic gets a per-location key ({@code "compiler:<code>:<source>:<line>"})
* so each unique file+line keeps its own entry in the build report.
* <p>
* The message includes the source location as plain text (e.g.
* {@code "unchecked cast\n at src/main/java/Foo.java[42,10]"})
* so it remains visible in the console output.
*/
private BuilderProblem buildProblem(
Diagnostic<? extends JavaFileObject> diagnostic,
String message,
String code,
JavaFileObject source,
BuilderProblem.Severity severity) {
BuilderProblem.Builder builder = BuilderProblem.builder().severity(severity);
// Build message with source location and per-location dedup key
var fullMessage = new StringBuilder(message);
var keyBuilder = new StringBuilder("compiler:").append(code);
if (source != null) {
record.newline().a(" at ").a(relativize(source.getName()));
String relPath = relativize(source.getName());
builder.source(relPath);
keyBuilder.append(':').append(relPath);
long line = diagnostic.getLineNumber();
long column = diagnostic.getColumnNumber();
fullMessage.append(System.lineSeparator()).append(" at ").append(relPath);
if (line != Diagnostic.NOPOS || column != Diagnostic.NOPOS) {
record.style(style).a('[');
fullMessage.append('[');
if (line != Diagnostic.NOPOS) {
record.a(line);
fullMessage.append(line);
builder.lineNumber((int) line);
keyBuilder.append(':').append(line);
}
if (column != Diagnostic.NOPOS) {
record.a(',').a(column);
fullMessage.append(',').append(column);
builder.columnNumber((int) column);
}
record.a(']').resetStyle();
fullMessage.append(']');
}
}
String log = record.toString();
switch (kind) {
case ERROR:
if (firstError == null) {
firstError = message;
}
logger.error(log);
numErrors++;
break;
case MANDATORY_WARNING:
case WARNING:
logger.warn(log);
numWarnings++;
break;
default:
logger.info(log);
break;
}
// Statistics
String code = diagnostic.getCode();
if (code != null) {
codeCount.merge(code, 1, (old, initial) -> old + 1);
}
return builder.key(keyBuilder.toString())
.message(fullMessage.toString())
.build();
}

/**
Expand All @@ -191,31 +238,32 @@ Optional<String> firstError(Throwable cause) {
* Reports summary after the compilation finished.
*/
void logSummary() {
MessageBuilder message = messageBuilderFactory.builder();
var message = new StringBuilder();
final String patternForCount;
if (!codeCount.isEmpty()) {
@SuppressWarnings("unchecked")
Map.Entry<String, Integer>[] entries = codeCount.entrySet().toArray(Map.Entry[]::new);
Arrays.sort(entries, (a, b) -> Integer.compare(b.getValue(), a.getValue()));
patternForCount = patternForCount(Math.max(entries[0].getValue(), Math.max(numWarnings, numErrors)));
message.strong("Summary of compiler messages:").newline();
message.append("Summary of compiler messages:").append(System.lineSeparator());
for (Map.Entry<String, Integer> entry : entries) {
int count = entry.getValue();
message.format(patternForCount, count, entry.getKey()).newline();
message.append(String.format(patternForCount, count, entry.getKey()))
.append(System.lineSeparator());
}
} else {
patternForCount = patternForCount(Math.max(numWarnings, numErrors));
}
if ((numWarnings | numErrors) != 0) {
message.strong("Total:");
message.append("Total:");
}
if (numWarnings != 0) {
writeCount(message, patternForCount, numWarnings, "warning");
}
if (numErrors != 0) {
writeCount(message, patternForCount, numErrors, "error");
}
logger.info(message.toString());
logger.info(message);
}

/**
Expand All @@ -230,9 +278,9 @@ private static String patternForCount(int n) {
/**
* Appends the count of warnings or errors, making them plural if needed.
*/
private static void writeCount(MessageBuilder message, String patternForCount, int count, String name) {
message.newline();
message.format(patternForCount, count, name);
private static void writeCount(StringBuilder message, String patternForCount, int count, String name) {
message.append(System.lineSeparator());
message.append(String.format(patternForCount, count, name));
if (count > 1) {
message.append('s');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ protected ToolExecutor(final AbstractCompilerMojo mojo, DiagnosticListener<? sup
logger = mojo.logger;
if (listener == null) {
Path root = mojo.project.getRootDirectory();
listener = new DiagnosticLogger(logger, mojo.messageBuilderFactory, LOCALE, root);
listener = new DiagnosticLogger(logger.child("diagnostics"), LOCALE, root);
}
this.listener = listener;
encoding = mojo.charset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,20 @@
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.testing.Basedir;
import org.apache.maven.api.plugin.testing.InjectMojo;
import org.apache.maven.api.plugin.testing.MojoExtension;
import org.apache.maven.api.plugin.testing.MojoParameter;
import org.apache.maven.api.plugin.testing.MojoTest;
import org.apache.maven.api.plugin.testing.stubs.ProducedArtifactStub;
import org.apache.maven.api.plugin.testing.stubs.ProjectStub;
import org.apache.maven.api.plugin.testing.stubs.SessionMock;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.api.services.ToolchainManager;
import org.apache.maven.impl.DefaultMessageBuilderFactory;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.plugin.compiler.stubs.CompilerStub;
import org.apache.maven.testing.plugin.Basedir;
import org.apache.maven.testing.plugin.InjectMojo;
import org.apache.maven.testing.plugin.MojoExtension;
import org.apache.maven.testing.plugin.MojoParameter;
import org.apache.maven.testing.plugin.MojoTest;
import org.apache.maven.testing.plugin.stubs.ProducedArtifactStub;
import org.apache.maven.testing.plugin.stubs.ProjectStub;
import org.apache.maven.testing.plugin.stubs.SessionMock;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
Expand All @@ -64,6 +64,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.clearInvocations;
Expand All @@ -72,6 +73,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@MojoTest
public class CompilerMojoTestCase {
Expand Down Expand Up @@ -135,6 +137,7 @@ public void testCompilerBasic(
TestCompilerMojo testCompileMojo) {

Log log = mock(Log.class);
when(log.child(anyString())).thenReturn(log);
compileMojo.logger = log;
compileMojo.execute();
verify(log).warn(startsWith("No explicit value set for --release or --target."));
Expand All @@ -158,6 +161,7 @@ public void testCompilerBasicSourceTarget(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {

Log log = mock(Log.class);
when(log.child(anyString())).thenReturn(log);
compileMojo.logger = log;
compileMojo.execute();
verify(log, never()).warn(startsWith("No explicit value set for --release or --target."));
Expand Down Expand Up @@ -196,6 +200,7 @@ public void testCompilerEmptySourceChangeDetection(
Files.write(source, new byte[0]);

Log log = mock(Log.class);
when(log.child(anyString())).thenReturn(log);
compileMojo.logger = log;
compileMojo.execute();

Expand Down