Expose compiler warnings in CompilationException

This commit improves TestCompiler to expose both errors and warnings
instead of an opaque message. When compilation fails, both errors and
warnings are displayed.

This is particularly useful when combined with the `-Werror` option
that turns the presence of a warning into an error.

Closes gh-36037
This commit is contained in:
Stéphane Nicoll
2025-12-17 11:45:20 +01:00
parent 86e89d53a9
commit e9a4b93477
4 changed files with 176 additions and 45 deletions
@@ -16,38 +16,105 @@
package org.springframework.core.test.tools;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.tools.Diagnostic;
/**
* Exception thrown when code cannot compile.
* Exception thrown when code cannot compile. Expose the {@linkplain Problem
* problems} for further inspection.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 6.0
*/
@SuppressWarnings("serial")
public class CompilationException extends RuntimeException {
private final List<Problem> problems;
CompilationException(String errors, SourceFiles sourceFiles, ResourceFiles resourceFiles) {
super(buildMessage(errors, sourceFiles, resourceFiles));
CompilationException(List<Problem> problems, SourceFiles sourceFiles, ResourceFiles resourceFiles) {
super(buildMessage(problems, sourceFiles, resourceFiles));
this.problems = problems;
}
private static String buildMessage(String errors, SourceFiles sourceFiles,
private static String buildMessage(List<Problem> problems, SourceFiles sourceFiles,
ResourceFiles resourceFiles) {
StringBuilder message = new StringBuilder();
message.append("Unable to compile source\n\n");
message.append(errors);
message.append("\n\n");
for (SourceFile sourceFile : sourceFiles) {
message.append("---- source: ").append(sourceFile.getPath()).append("\n\n");
message.append(sourceFile.getContent());
message.append("\n\n");
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
writer.println("Unable to compile source");
Function<List<Problem>, String> createBulletList = elements -> elements.stream()
.map(warning -> "- %s".formatted(warning.message()))
.collect(Collectors.joining("\n"));
List<Problem> errors = problems.stream()
.filter(problem -> problem.kind == Diagnostic.Kind.ERROR).toList();
if (!errors.isEmpty()) {
writer.println();
writer.println("Errors:");
writer.println(createBulletList.apply(errors));
}
for (ResourceFile resourceFile : resourceFiles) {
message.append("---- resource: ").append(resourceFile.getPath()).append("\n\n");
message.append(resourceFile.getContent());
message.append("\n\n");
List<Problem> warnings = problems.stream()
.filter(problem -> problem.kind == Diagnostic.Kind.WARNING ||
problem.kind == Diagnostic.Kind.MANDATORY_WARNING).toList();
if (!warnings.isEmpty()) {
writer.println();
writer.println("Warnings:");
writer.println(createBulletList.apply(warnings));
}
return message.toString();
if (!sourceFiles.isEmpty()) {
for (SourceFile sourceFile : sourceFiles) {
writer.println();
writer.printf("---- source: %s%n".formatted(sourceFile.getPath()));
writer.println(sourceFile.getContent());
}
}
if (!resourceFiles.isEmpty()) {
for (ResourceFile resourceFile : resourceFiles) {
writer.println();
writer.printf("---- resource: %s%n".formatted(resourceFile.getPath()));
writer.println(resourceFile.getContent());
}
}
return out.toString();
}
/**
* Return the {@linkplain Problem problems} that lead to this exception.
* @return the problems
* @since 7.0.3
*/
public List<Problem> getProblems() {
return this.problems;
}
/**
* Return the {@linkplain Problem problems} of the given {@code kinds}.
* @param kinds the {@linkplain Diagnostic.Kind kinds} to filter on
* @return the problems with the given kinds, or an empty list
* @since 7.0.3
*/
public List<Problem> getProblems(Diagnostic.Kind... kinds) {
List<Diagnostic.Kind> toMatch = Arrays.asList(kinds);
return this.problems.stream().filter(problem -> toMatch.contains(problem.kind())).toList();
}
/**
* Description of a problem that lead to a compilation failure.
* <p>{@linkplain Diagnostic.Kind#ERROR errors} are the most important, but
* they might not be enough in case an error is triggered by the presence
* of a warning, see {@link Diagnostic.Kind#MANDATORY_WARNING}.
* @since 7.0.3
* @param kind the kind of problem
* @param message the description of the problem
*/
public record Problem(Diagnostic.Kind kind, String message) {
}
}
@@ -307,13 +307,13 @@ public final class TestCompiler {
DynamicJavaFileManager fileManager = new DynamicJavaFileManager(
standardFileManager, classLoaderToUse, this.classFiles, this.resourceFiles);
if (!this.sourceFiles.isEmpty()) {
Errors errors = new Errors();
CompilationTask task = this.compiler.getTask(null, fileManager, errors,
Problems problems = new Problems();
CompilationTask task = this.compiler.getTask(null, fileManager, problems,
this.compilerOptions, null, compilationUnits);
task.setProcessors(this.processors);
boolean result = task.call();
if (!result || errors.hasReportedErrors()) {
throw new CompilationException(errors.toString(), this.sourceFiles, this.resourceFiles);
if (!result || problems.hasReportedErrors()) {
throw new CompilationException(problems.elements, this.sourceFiles, this.resourceFiles);
}
}
return new DynamicClassLoader(classLoaderToUse, this.classFiles, this.resourceFiles,
@@ -342,34 +342,38 @@ public final class TestCompiler {
/**
* {@link DiagnosticListener} used to collect errors.
* {@link DiagnosticListener} used to collect errors and warnings.
*/
static class Errors implements DiagnosticListener<JavaFileObject> {
static class Problems implements DiagnosticListener<JavaFileObject> {
private final StringBuilder message = new StringBuilder();
private static final List<Diagnostic.Kind> HANDLED_DIAGNOSTICS = List.of(
Diagnostic.Kind.ERROR, Diagnostic.Kind.MANDATORY_WARNING, Diagnostic.Kind.WARNING);
private final List<CompilationException.Problem> elements = new ArrayList<>();
@Override
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
this.message.append('\n');
this.message.append(diagnostic.getMessage(Locale.getDefault()));
if (diagnostic.getSource() != null) {
this.message.append(' ');
this.message.append(diagnostic.getSource().getName());
this.message.append(' ');
this.message.append(diagnostic.getLineNumber()).append(':')
.append(diagnostic.getColumnNumber());
}
Diagnostic.Kind kind = diagnostic.getKind();
if (HANDLED_DIAGNOSTICS.contains(kind)) {
this.elements.add(new CompilationException.Problem(kind, toMessage(diagnostic)));
}
}
boolean hasReportedErrors() {
return !this.message.isEmpty();
private String toMessage(Diagnostic<? extends JavaFileObject> diagnostic) {
StringBuilder message = new StringBuilder();
message.append(diagnostic.getMessage(Locale.getDefault()));
if (diagnostic.getSource() != null) {
message.append(' ');
message.append(diagnostic.getSource().getName());
message.append(' ');
message.append(diagnostic.getLineNumber()).append(':')
.append(diagnostic.getColumnNumber());
}
return message.toString();
}
@Override
public String toString() {
return this.message.toString();
boolean hasReportedErrors() {
return this.elements.stream().anyMatch(problem -> problem.kind() == Diagnostic.Kind.ERROR);
}
}
@@ -16,21 +16,72 @@
package org.springframework.core.test.tools;
import java.util.List;
import javax.tools.Diagnostic;
import org.junit.jupiter.api.Test;
import org.springframework.core.test.tools.CompilationException.Problem;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CompilationException}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
class CompilationExceptionTests {
@Test
void getMessageReturnsMessage() {
CompilationException exception = new CompilationException("message", SourceFiles.none(), ResourceFiles.none());
assertThat(exception).hasMessageContaining("message");
void exceptionMessageReportsSingleError() {
CompilationException exception = new CompilationException(
List.of(new Problem(Diagnostic.Kind.ERROR, "error message")),
SourceFiles.none(), ResourceFiles.none());
assertThat(exception.getMessage().lines()).containsExactly(
"Unable to compile source", "", "Errors:", "- error message");
}
@Test
void exceptionMessageReportsSingleWarning() {
CompilationException exception = new CompilationException(
List.of(new Problem(Diagnostic.Kind.MANDATORY_WARNING, "warning message")),
SourceFiles.none(), ResourceFiles.none());
assertThat(exception.getMessage().lines()).containsExactly(
"Unable to compile source", "", "Warnings:", "- warning message");
}
@Test
void exceptionMessageReportsProblems() {
CompilationException exception = new CompilationException(List.of(
new Problem(Diagnostic.Kind.MANDATORY_WARNING, "warning message"),
new Problem(Diagnostic.Kind.ERROR, "error message"),
new Problem(Diagnostic.Kind.WARNING, "warning message2"),
new Problem(Diagnostic.Kind.ERROR, "error message2")), SourceFiles.none(), ResourceFiles.none());
assertThat(exception.getMessage().lines()).containsExactly(
"Unable to compile source", "", "Errors:", "- error message", "- error message2", "" ,
"Warnings:", "- warning message","- warning message2");
}
@Test
void exceptionMessageReportsSourceCode() {
CompilationException exception = new CompilationException(
List.of(new Problem(Diagnostic.Kind.ERROR, "error message")),
SourceFiles.of(SourceFile.of("public class Hello {}")), ResourceFiles.none());
assertThat(exception.getMessage().lines()).containsExactly(
"Unable to compile source", "", "Errors:", "- error message", "",
"---- source: Hello.java", "public class Hello {}");
}
@Test
void exceptionMessageReportsResource() {
CompilationException exception = new CompilationException(
List.of(new Problem(Diagnostic.Kind.ERROR, "error message")),
SourceFiles.none(), ResourceFiles.of(ResourceFile.of("application.properties", "test=value")));
assertThat(exception.getMessage().lines()).containsExactly(
"Unable to compile source", "", "Errors:", "- error message", "",
"---- resource: application.properties", "test=value");
}
}
@@ -30,6 +30,7 @@ import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
@@ -136,7 +137,8 @@ class TestCompilerTests {
assertThatExceptionOfType(CompilationException.class).isThrownBy(
() -> TestCompiler.forSystem().withSources(
SourceFile.of(HELLO_BAD)).compile(compiled -> {
}));
})).satisfies(ex -> assertThat(ex.getProblems()).singleElement()
.satisfies(problem -> assertThat(problem.message()).contains("Supplier")));
}
@Test
@@ -177,7 +179,14 @@ class TestCompilerTests {
assertThatExceptionOfType(CompilationException.class).isThrownBy(
() -> TestCompiler.forSystem().failOnWarning().withSources(
SourceFile.of(HELLO_DEPRECATED), main).compile(compiled -> {
}));
})).satisfies(compilationException -> {
assertThat(compilationException.getProblems(Diagnostic.Kind.ERROR)).singleElement()
.satisfies(error -> assertThat(error.message())
.contains("-Werror"));
assertThat(compilationException.getProblems(Diagnostic.Kind.MANDATORY_WARNING)).singleElement()
.satisfies(warning -> assertThat(warning.message())
.contains("get()", "com.example.Hello"));
});
}
@Test