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);
+ }
+ }
}