You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/01 09:02:12 UTC

(maven) 01/01: Fix #12983: ToolchainPluginStrategy handles inherited source levels from parent POMs

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch fix/12983-toolchain-inherited-source-levels
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 5370303f67ccbcc56403cfab3fe52dd522c69c55
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Tue Sep 1 11:01:59 2026 +0200

    Fix #12983: ToolchainPluginStrategy handles inherited source levels from parent POMs
    
    When maven.compiler.source/target is inherited from a parent POM (e.g.,
    geronimo-genesis sets source=1.5), mvnup now resolves the effective source
    level using Maven's model builder and configures the auto-toolchain for
    JDK 8/11 compilation. Previously it only inspected the local POM.
    
    The fix adds a fallback path in doApply: when the fast DOM-based detection
    finds no source level in the local POM, it builds the effective model to
    resolve inherited properties. This handles the 12 Apache projects that
    fail with "Source option N is no longer supported" under Maven 4.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
 .../mvnup/goals/ToolchainPluginStrategy.java       |  77 ++++++-
 .../mvnup/goals/ToolchainPluginStrategyTest.java   | 253 +++++++++++++++++++++
 2 files changed, 326 insertions(+), 4 deletions(-)

diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
index 9db93a51a2..86765580a0 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategy.java
@@ -29,6 +29,7 @@
 import org.apache.maven.api.di.Named;
 import org.apache.maven.api.di.Priority;
 import org.apache.maven.api.di.Singleton;
+import org.apache.maven.api.model.Model;
 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
 import org.apache.maven.impl.JdkSourceLevelSupport;
 
@@ -48,9 +49,10 @@
  *
  * <p>This strategy detects the project's source level from:
  * <ol>
- *   <li>{@code maven.compiler.release} property</li>
- *   <li>{@code maven.compiler.source} property</li>
- *   <li>Compiler plugin {@code <configuration><release>} or {@code <source>}</li>
+ *   <li>{@code maven.compiler.release} property (local POM)</li>
+ *   <li>{@code maven.compiler.source} property (local POM)</li>
+ *   <li>Compiler plugin {@code <configuration><release>} or {@code <source>} (local POM)</li>
+ *   <li>Effective model resolution (inherited properties from parent POMs)</li>
  * </ol>
  *
  * <p>If the running JDK does not support the detected source level (per JEP 182 retirement
@@ -132,7 +134,11 @@ protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomM
             try {
                 int sourceLevel = detectSourceLevel(pomDocument);
                 if (sourceLevel <= 0) {
-                    context.success("No source level configured");
+                    // Try effective model resolution (handles inherited properties from parent POMs)
+                    sourceLevel = detectEffectiveSourceLevel(context, pomPath);
+                }
+                if (sourceLevel <= 0) {
+                    context.success("No source level configured (local or inherited)");
                     continue;
                 }
 
@@ -339,6 +345,69 @@ void addToolchainsPlugin(Document pomDocument, int maxJdkVersion) {
         DomUtils.insertContentElement(configuration, "version", "(," + maxJdkVersion + "]");
     }
 
+    /**
+     * Detects the project's source level by building the effective Maven model,
+     * which resolves inherited properties from parent POMs.
+     *
+     * <p>This is the fallback path used when the fast DOM-based detection
+     * ({@link #detectSourceLevel(Document)}) finds no source level in the local POM.
+     * It handles cases like projects inheriting {@code maven.compiler.source=1.5}
+     * from a parent POM (e.g., geronimo-genesis).
+     *
+     * @param context the upgrade context for logging
+     * @param pomPath the path to the POM file on disk
+     * @return the source level as a major version, or {@code -1} if none is configured
+     */
+    int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+        try {
+            Model effectiveModel = buildEffectiveModel(pomPath);
+            int level = detectSourceLevelFromEffectiveModel(effectiveModel);
+            if (level > 0) {
+                context.detail("Detected inherited source level " + level + " from effective model");
+            }
+            return level;
+        } catch (Exception e) {
+            context.debug("Could not resolve effective model for " + pomPath + ": " + e.getMessage());
+            return -1;
+        }
+    }
+
+    /**
+     * Extracts the source level from an effective Maven model's properties.
+     *
+     * <p>Checks (in order of precedence):
+     * <ol>
+     *   <li>{@code maven.compiler.release}</li>
+     *   <li>{@code maven.compiler.source}</li>
+     * </ol>
+     *
+     * @param effectiveModel the resolved effective model
+     * @return the source level as a major version, or {@code -1} if none is configured
+     */
+    static int detectSourceLevelFromEffectiveModel(Model effectiveModel) {
+        Map<String, String> properties = effectiveModel.getProperties();
+
+        // Check maven.compiler.release first (takes precedence)
+        String release = properties.get(MAVEN_COMPILER_RELEASE);
+        if (release != null && !release.isBlank()) {
+            int level = JdkSourceLevelSupport.normalizeSourceLevel(release.trim());
+            if (level > 0) {
+                return level;
+            }
+        }
+
+        // Check maven.compiler.source
+        String source = properties.get(MAVEN_COMPILER_SOURCE);
+        if (source != null && !source.isBlank()) {
+            int level = JdkSourceLevelSupport.normalizeSourceLevel(source.trim());
+            if (level > 0) {
+                return level;
+            }
+        }
+
+        return -1;
+    }
+
     /**
      * Returns the major version of the running JDK.
      * Extracted as a method so tests can override it.
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
index 7414d262a1..817e181387 100644
--- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/ToolchainPluginStrategyTest.java
@@ -23,6 +23,7 @@
 import java.util.Map;
 
 import eu.maveniverse.domtrip.Document;
+import org.apache.maven.api.model.Model;
 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -528,4 +529,256 @@ int getRunningJdkMajor() {
             assertEquals(0, result.modifiedPoms().size());
         }
     }
+
+    @Nested
+    @DisplayName("Effective model source level detection")
+    class EffectiveModelSourceLevelTests {
+
+        @Test
+        @DisplayName("should detect source level from effective model maven.compiler.release")
+        void detectFromEffectiveRelease() {
+            Model model = Model.newBuilder()
+                    .properties(Map.of("maven.compiler.release", "6"))
+                    .build();
+            assertEquals(6, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("should detect source level from effective model maven.compiler.source")
+        void detectFromEffectiveSource() {
+            Model model = Model.newBuilder()
+                    .properties(Map.of("maven.compiler.source", "1.5"))
+                    .build();
+            assertEquals(5, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("release takes precedence over source in effective model")
+        void effectiveReleasePrecedence() {
+            Model model = Model.newBuilder()
+                    .properties(Map.of(
+                            "maven.compiler.release", "7",
+                            "maven.compiler.source", "6"))
+                    .build();
+            assertEquals(7, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("should return -1 when effective model has no compiler properties")
+        void noEffectiveSourceLevel() {
+            Model model = Model.newBuilder()
+                    .properties(Map.of("some.other.property", "value"))
+                    .build();
+            assertEquals(-1, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("should return -1 when effective model has empty properties")
+        void emptyEffectiveProperties() {
+            Model model = Model.newBuilder().properties(Map.of()).build();
+            assertEquals(-1, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("should handle legacy 1.x format in effective model")
+        void legacyFormatInEffectiveModel() {
+            Model model = Model.newBuilder()
+                    .properties(Map.of("maven.compiler.source", "1.6"))
+                    .build();
+            assertEquals(6, ToolchainPluginStrategy.detectSourceLevelFromEffectiveModel(model));
+        }
+
+        @Test
+        @DisplayName("should add plugin when source level is inherited from parent POM via effective model")
+        void addsPluginWhenSourceInherited() {
+            // Simulate running JDK 21, source level inherited from parent (not in local POM)
+            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+                @Override
+                int getRunningJdkMajor() {
+                    return 21;
+                }
+
+                @Override
+                int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+                    // Simulate parent POM setting maven.compiler.source=1.5
+                    return 5;
+                }
+            };
+
+            String pomXml = """
+                    <?xml version="1.0" encoding="UTF-8"?>
+                    <project xmlns="http://maven.apache.org/POM/4.0.0">
+                        <modelVersion>4.0.0</modelVersion>
+                        <parent>
+                            <groupId>org.apache.geronimo.genesis</groupId>
+                            <artifactId>genesis</artifactId>
+                            <version>1.0</version>
+                        </parent>
+                        <artifactId>test-child</artifactId>
+                    </project>
+                    """;
+            Document doc = Document.of(pomXml);
+            UpgradeContext context = TestUtils.createMockContext();
+
+            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+            assertEquals(1, result.modifiedPoms().size());
+            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+            String output = doc.toXml();
+            assertTrue(output.contains("<version>(,8]</version>"), "Expected JDK 8 constraint for source 5: " + output);
+        }
+
+        @Test
+        @DisplayName("should not modify POM when inherited source level is compatible with running JDK")
+        void noModificationWhenInheritedLevelCompatible() {
+            // Simulate running JDK 17, inherited source level 11 (still supported)
+            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+                @Override
+                int getRunningJdkMajor() {
+                    return 17;
+                }
+
+                @Override
+                int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+                    return 11;
+                }
+            };
+
+            String pomXml = """
+                    <?xml version="1.0" encoding="UTF-8"?>
+                    <project xmlns="http://maven.apache.org/POM/4.0.0">
+                        <modelVersion>4.0.0</modelVersion>
+                        <parent>
+                            <groupId>com.example</groupId>
+                            <artifactId>parent</artifactId>
+                            <version>1.0</version>
+                        </parent>
+                        <artifactId>child</artifactId>
+                    </project>
+                    """;
+            Document doc = Document.of(pomXml);
+            UpgradeContext context = TestUtils.createMockContext();
+
+            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+            assertEquals(0, result.modifiedPoms().size());
+        }
+
+        @Test
+        @DisplayName("should fall back gracefully when effective model resolution fails")
+        void fallbackWhenEffectiveModelFails() {
+            // Simulate effective model resolution failure
+            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+                @Override
+                int getRunningJdkMajor() {
+                    return 21;
+                }
+
+                @Override
+                int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+                    return -1; // Simulate failure
+                }
+            };
+
+            String pomXml = """
+                    <?xml version="1.0" encoding="UTF-8"?>
+                    <project xmlns="http://maven.apache.org/POM/4.0.0">
+                        <modelVersion>4.0.0</modelVersion>
+                        <parent>
+                            <groupId>com.example</groupId>
+                            <artifactId>parent</artifactId>
+                            <version>1.0</version>
+                        </parent>
+                        <artifactId>child</artifactId>
+                    </project>
+                    """;
+            Document doc = Document.of(pomXml);
+            UpgradeContext context = TestUtils.createMockContext();
+
+            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+            // Should not modify POM and not fail
+            assertEquals(0, result.modifiedPoms().size());
+            assertEquals(0, result.errorPoms().size());
+        }
+
+        @Test
+        @DisplayName("local POM source level takes precedence over effective model")
+        void localTakesPrecedenceOverEffective() {
+            // Local POM has source=11, but effective model would return 6
+            // Local should win — effective model is only consulted when local returns -1
+            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+                @Override
+                int getRunningJdkMajor() {
+                    return 21;
+                }
+
+                @Override
+                int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+                    // This should NOT be consulted because local detection succeeds
+                    return 6;
+                }
+            };
+
+            String pomXml = """
+                    <?xml version="1.0" encoding="UTF-8"?>
+                    <project xmlns="http://maven.apache.org/POM/4.0.0">
+                        <modelVersion>4.0.0</modelVersion>
+                        <groupId>com.example</groupId>
+                        <artifactId>test</artifactId>
+                        <version>1.0</version>
+                        <properties>
+                            <maven.compiler.release>11</maven.compiler.release>
+                        </properties>
+                    </project>
+                    """;
+            Document doc = Document.of(pomXml);
+            UpgradeContext context = TestUtils.createMockContext();
+
+            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+            // Source 11 is supported by JDK 21, so no modification
+            assertEquals(0, result.modifiedPoms().size());
+        }
+
+        @Test
+        @DisplayName("should add plugin with correct JDK constraint for inherited source 6")
+        void correctJdkConstraintForSource6() {
+            ToolchainPluginStrategy strategy = new ToolchainPluginStrategy() {
+                @Override
+                int getRunningJdkMajor() {
+                    return 21;
+                }
+
+                @Override
+                int detectEffectiveSourceLevel(UpgradeContext context, Path pomPath) {
+                    return 6; // Inherited source level 6 (e.g., geronimo-genesis)
+                }
+            };
+
+            String pomXml = """
+                    <?xml version="1.0" encoding="UTF-8"?>
+                    <project xmlns="http://maven.apache.org/POM/4.0.0">
+                        <modelVersion>4.0.0</modelVersion>
+                        <parent>
+                            <groupId>org.apache.geronimo.genesis</groupId>
+                            <artifactId>genesis</artifactId>
+                            <version>2.0</version>
+                        </parent>
+                        <artifactId>test-module</artifactId>
+                    </project>
+                    """;
+            Document doc = Document.of(pomXml);
+            UpgradeContext context = TestUtils.createMockContext();
+
+            UpgradeResult result = strategy.doApply(context, Map.of(POM_PATH, doc));
+
+            assertEquals(1, result.modifiedPoms().size());
+            assertTrue(strategy.hasToolchainsPluginWithSelectGoal(doc));
+            String output = doc.toXml();
+            // Source level 6 requires JDK <= 11
+            assertTrue(
+                    output.contains("<version>(,11]</version>"), "Expected JDK 11 constraint for source 6: " + output);
+        }
+    }
 }