From 0f9d871a2c7c4ac56825b6eb59026c7e651eb72f Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sun, 12 Jul 2026 15:39:58 +0200 Subject: [PATCH 1/4] Fix incremental detection of empty sources The stale source scanner assumes every Java source produces a mapped output, causing zero-byte compilation units to trigger recompilation on every invocation. Filter zero-byte stale candidates only for one-output-per-input compilers and only when no mapped output exists. This preserves detection when an existing source is truncated and leaves aggregate-output compilers unchanged. Add a regression test that compiles an empty source twice and verifies that the second invocation is up to date. Fixes #1000. --- .../plugin/compiler/AbstractCompilerMojo.java | 21 ++++++++++- .../plugin/compiler/CompilerMojoTest.java | 23 ++++++++++++ .../plugin-config.xml | 37 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml diff --git a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java index edb9ad7ed..b78e9d85f 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1559,8 +1559,25 @@ private Set computeStaleSources( } try { - staleSources.addAll(scanner.getIncludedSources(rootFile, outputDirectory)); - } catch (InclusionScanException e) { + Set includedSources = scanner.getIncludedSources(rootFile, outputDirectory); + // The stale source scanner assumes that every source produces an output file. Filter its result only + // when the compiler provides an individual source-to-output mapping; aggregate outputs are ambiguous. + if (outputStyle == CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE) { + for (File source : includedSources) { + String relativePath = + rootFile.toPath().relativize(source.toPath()).toString(); + boolean outputExists = mapping.getTargetFiles(outputDirectory, relativePath).stream() + .anyMatch(File::exists); + // A zero-byte compilation unit legitimately produces no class. Keep it stale if an output + // exists, however, so that truncating an existing source is still detected as a change. + if (Files.size(source.toPath()) != 0 || outputExists) { + staleSources.add(source); + } + } + } else { + staleSources.addAll(includedSources); + } + } catch (InclusionScanException | IOException e) { throw new MojoExecutionException( "Error scanning source root: \'" + sourceRoot + "\' for stale files to recompile.", e); } diff --git a/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java b/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java index 260fd941e..101d6a3f8 100644 --- a/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java +++ b/src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java @@ -19,6 +19,7 @@ package org.apache.maven.plugin.compiler; import java.io.File; +import java.nio.file.Files; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -42,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -113,6 +115,27 @@ void testCompilerEmptySource(CompilerMojo compilerMojo) throws Exception { projectArtifact.getFile(), "MCOMPILER-94: artifact file should be null if there is nothing to compile"); } + /** + * Tests that an empty source file does not cause compilation every time because it has no class file. + */ + @Test + @InjectMojo(goal = COMPILE, pom = "classpath:/unit/compiler-empty-source-change-detection-test/plugin-config.xml") + void testCompilerEmptySourceChangeDetection(CompilerMojo compilerMojo) throws Exception { + setUpCompilerMojoTestEnv(compilerMojo); + + File source = new File(compilerMojo.getCompileSourceRoots().get(0), "Empty.java"); + Files.createDirectories(source.getParentFile().toPath()); + Files.write(source.toPath(), new byte[0]); + + Log log = mock(Log.class); + compilerMojo.setLog(log); + compilerMojo.execute(); + + clearInvocations(log); + compilerMojo.execute(); + verify(log).info("Nothing to compile - all classes are up to date."); + } + /** * tests the ability of the plugin to respond to includes and excludes correctly * diff --git a/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml b/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml new file mode 100644 index 000000000..6d9eba85b --- /dev/null +++ b/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml @@ -0,0 +1,37 @@ + + + + + + + maven-compiler-plugin + + + ${basedir}/target/test-classes/unit/compiler-empty-source-change-detection-test/src/main/java + + javac + 17 + ${basedir}/target/test/unit/compiler-empty-source-change-detection-test/target/classes + ${basedir}/target/test/unit/compiler-empty-source-change-detection-test/target + + + + + From 97fcb137c93f6b303243bf5eab75abc7d66a7933 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 14:26:24 +0200 Subject: [PATCH 2/4] Avoid hardcoded release in empty-source test --- .../plugin-config.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml b/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml index 6d9eba85b..c144fc63f 100644 --- a/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml +++ b/src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml @@ -27,7 +27,6 @@ ${basedir}/target/test-classes/unit/compiler-empty-source-change-detection-test/src/main/java javac - 17 ${basedir}/target/test/unit/compiler-empty-source-change-detection-test/target/classes ${basedir}/target/test/unit/compiler-empty-source-change-detection-test/target From afc749aed3f65d1102960364768aa3d79b1db3e1 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 14:28:56 +0200 Subject: [PATCH 3/4] Avoid output lookups for non-empty sources --- .../plugin/compiler/AbstractCompilerMojo.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java index b78e9d85f..da21da3da 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1564,14 +1564,18 @@ private Set computeStaleSources( // when the compiler provides an individual source-to-output mapping; aggregate outputs are ambiguous. if (outputStyle == CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE) { for (File source : includedSources) { - String relativePath = - rootFile.toPath().relativize(source.toPath()).toString(); - boolean outputExists = mapping.getTargetFiles(outputDirectory, relativePath).stream() - .anyMatch(File::exists); - // A zero-byte compilation unit legitimately produces no class. Keep it stale if an output - // exists, however, so that truncating an existing source is still detected as a change. - if (Files.size(source.toPath()) != 0 || outputExists) { + if (Files.size(source.toPath()) != 0) { staleSources.add(source); + } else { + String relativePath = + rootFile.toPath().relativize(source.toPath()).toString(); + boolean outputExists = mapping.getTargetFiles(outputDirectory, relativePath).stream() + .anyMatch(File::exists); + // A zero-byte compilation unit legitimately produces no class. Keep it stale if an output + // exists, however, so that truncating an existing source is still detected as a change. + if (outputExists) { + staleSources.add(source); + } } } } else { From c7b7ee2c3108f0283de08a0b568f94df04af561c Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Tue, 28 Jul 2026 16:06:09 +0200 Subject: [PATCH 4/4] Apply Spotless formatting --- .../apache/maven/plugin/compiler/AbstractCompilerMojo.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java index da21da3da..ee8c88781 100644 --- a/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java +++ b/src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java @@ -1567,8 +1567,9 @@ private Set computeStaleSources( if (Files.size(source.toPath()) != 0) { staleSources.add(source); } else { - String relativePath = - rootFile.toPath().relativize(source.toPath()).toString(); + String relativePath = rootFile.toPath() + .relativize(source.toPath()) + .toString(); boolean outputExists = mapping.getTargetFiles(outputDirectory, relativePath).stream() .anyMatch(File::exists); // A zero-byte compilation unit legitimately produces no class. Keep it stale if an output