Skip to content

Fix incremental detection of empty sources, 3.x - #1075

Open
wilx wants to merge 4 commits into
apache:maven-compiler-plugin-3.xfrom
wilx:issue-1000-empty-source-3.x
Open

Fix incremental detection of empty sources, 3.x#1075
wilx wants to merge 4 commits into
apache:maven-compiler-plugin-3.xfrom
wilx:issue-1000-empty-source-3.x

Conversation

@wilx

@wilx wilx commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

The stale source scanner assumes every Java source produces a mapped output, causing zero-byte compilation units to trigger recompilation on every invocation.

This change filters 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 while leaving aggregate-output compilers unchanged.

A regression test compiles an empty source twice and verifies that the second invocation reports all classes as up to date.

Fixes #1000.

Following this checklist to help us incorporate your
contribution quickly and easily:

  • Your pull request should address just one issue, without pulling in other changes.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body.
    Note that commits might be squashed by a maintainer on merge.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied.
    This may not always be possible but is a best-practice.
  • Run mvn verify to make sure basic checks pass.
    A more thorough check will be performed on your pull request automatically.
  • You have run the integration tests successfully (mvn -Prun-its verify).

If your pull request is about ~20 lines of code you don't need to sign an
Individual Contributor License Agreement if you are unsure
please ask on the developers list.

To make clear that you license your contribution under
the Apache License Version 2.0, January 2004
you have to acknowledge this by using the following check-box.

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 apache#1000.
@wilx wilx changed the title Fix incremental detection of empty sources Fix incremental detection of empty sources, 3.x Jul 12, 2026
@elharo
elharo requested a review from Copilot July 28, 2026 11:47
Files.createDirectories(source.getParentFile().toPath());
Files.write(source.toPath(), new byte[0]);

Log log = mock(Log.class);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing vs log messages is fragile. Any other way to verify this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same way of verification is used in two other places in the test. It is good enough.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes incremental compilation change detection so that truly empty Java sources (0 bytes) don’t cause perpetual recompilation when no mapped output exists, while preserving detection for truncated sources that previously produced outputs.

Changes:

  • Filter stale-source candidates for ONE_OUTPUT_FILE_PER_INPUT_FILE compilers to ignore 0-byte sources with no mapped output.
  • Add a regression test that compiles an empty source twice and asserts the second run reports “up to date”.
  • Add a dedicated test plugin configuration for the new empty-source change-detection scenario.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java Filters stale sources to avoid recompile loops for empty sources without mapped outputs.
src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java Adds regression test ensuring second compilation is up-to-date for empty source.
src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml Provides test-specific compiler plugin configuration and directories.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1566 to +1576
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);
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines +1567 to +1569
String relativePath =
rootFile.toPath().relativize(source.toPath()).toString();
boolean outputExists = mapping.getTargetFiles(outputDirectory, relativePath).stream()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string is passed to the getTargetFiles method which uses it to construct File instance. OS specific separators are appropriate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While java.io.File (and Java's NIO layer on Windows) can parse backslashes, Maven plugins like maven-war-plugin or maven-ear-plugin do not just write files to disk—they map them into archives (ZIP, JAR, WAR, EAR).

  1. Archive Entry Standards: ZIP and JAR specifications mandate that entry names use forward slashes (/) as path separators, regardless of the host operating system.
  2. Map/Cache Lookups: If target paths are stored as keys in a Map or Set (e.g., to track packaged or copied files, check for staleness, or prevent duplicates), a path string containing \ on Windows will not match an entry lookup using /. This leads to cross-platform bugs where builds behave differently on Windows versus Unix.
  3. Java's File Flexibility: Ironically, Java's File constructor on Windows accepts forward slashes (/) natively and normalizes them internally. Therefore, forcing platform-specific separators via Path#toString() introduces risks for map keys and archive matching without providing any functional benefit to the File constructor itself.

The finding is valid; relying on Path#toString() for target mappings in archive-building plugins frequently causes Windows-specific regressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While java.io.File (and Java's NIO layer on Windows) can parse backslashes, Maven plugins like maven-war-plugin or maven-ear-plugin do not just write files to disk—they map them into archives (ZIP, JAR, WAR, EAR).

1. **Archive Entry Standards:** ZIP and JAR specifications mandate that entry names use forward slashes (`/`) as path separators, regardless of the host operating system.

2. **Map/Cache Lookups:** If target paths are stored as keys in a `Map` or `Set` (e.g., to track packaged or copied files, check for staleness, or prevent duplicates), a path string containing `\` on Windows will not match an entry lookup using `/`. This leads to cross-platform bugs where builds behave differently on Windows versus Unix.

3. **Java's `File` Flexibility:** Ironically, Java's `File` constructor on Windows accepts forward slashes (`/`) natively and normalizes them internally. Therefore, forcing platform-specific separators via `Path#toString()` introduces risks for map keys and archive matching without providing any functional benefit to the `File` constructor itself.

The finding is valid; relying on Path#toString() for target mappings in archive-building plugins frequently causes Windows-specific regressions.

I am sorry, but this is irrelevant in the face of what the org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping implementations actually do. The AI is wrong.

<compileSourceRoot>${basedir}/target/test-classes/unit/compiler-empty-source-change-detection-test/src/main/java</compileSourceRoot>
</compileSourceRoots>
<compilerId>javac</compilerId>
<release>17</release>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@elharo
elharo requested a review from Copilot July 28, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java:132

  • Using a bare Mockito mock for Log can prevent info() from ever being called if the production code checks log.isInfoEnabled() (default boolean return from mocks is false). To make this test reliable, stub when(log.isInfoEnabled()).thenReturn(true) (and similarly for other levels if used), or use a real Log implementation suitable for tests.
        Log log = mock(Log.class);
        compilerMojo.setLog(log);
        compilerMojo.execute();

src/test/java/org/apache/maven/plugin/compiler/CompilerMojoTest.java:136

  • This assertion is tightly coupled to the exact log message text, which makes the test brittle to harmless wording changes. Consider verifying with a matcher (e.g., startsWith / contains) or asserting the underlying behavior (e.g., no compilation triggered / no sources considered stale) if you can observe it from the test harness.
        verify(log).info("Nothing to compile - all classes are up to date.");

src/main/java/org/apache/maven/plugin/compiler/AbstractCompilerMojo.java:1569

  • Using Files.size(...) introduces checked-IO handling (and an extra filesystem call) for a simple empty-file check. Since source is a File, source.length() != 0 is typically sufficient here and would let you avoid IOException-driven control flow in this block.
                    for (File source : includedSources) {
                        if (Files.size(source.toPath()) != 0) {
                            staleSources.add(source);
                        } else {

src/test/resources/unit/compiler-empty-source-change-detection-test/plugin-config.xml:24

  • This file is used as the pom input to @InjectMojo, but it lacks typical required Maven POM coordinates (e.g., modelVersion, groupId, artifactId, version). If the test harness parses it as a Maven Model, missing coordinates can cause parsing/validation issues or environment-dependent defaults. Consider making it a minimal valid POM by adding those elements to ensure deterministic test setup.
<project>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants