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:13:09 UTC
(maven) 01/01: Fix #12989: mvnup plugin upgrades should respect project JDK version
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch fix-apache-maven-12989-mvnup-plugin-upgrades-sho
in repository https://gitbox.apache.org/repos/asf/maven.git
commit aa0165a16355157c68a51d4e466041323798e040
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Tue Sep 1 11:12:54 2026 +0200
Fix #12989: mvnup plugin upgrades should respect project JDK version
Add JDK compatibility checking to PluginUpgradeStrategy so that plugins
requiring a higher JDK than the project targets are skipped rather than
blindly upgraded. This prevents UnsupportedClassVersionError at build
time (e.g., checkstyle-plugin 3.6.0 pulls Checkstyle 10.x which
requires JDK 21, breaking projects that target JDK 17).
Changes:
- Add minJdk field to PluginUpgrade record and PluginUpgradeInfo class
- Add detectProjectJdkVersion() method that reads maven.compiler.release,
maven.compiler.source, maven.compiler.target, and compiler plugin config
- Add JDK compatibility guard in upgradePluginVersion() (similar to the
existing Quarkus 2.x skip pattern)
- Add JDK compatibility guard in effective model analysis path
- Register maven-checkstyle-plugin 3.6.0 with minJdk=21
- Add comprehensive tests in PluginUpgradeJdkCompatibilityTest
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../cling/invoker/mvnup/goals/PluginUpgrade.java | 16 +-
.../invoker/mvnup/goals/PluginUpgradeStrategy.java | 166 +++++-
.../goals/PluginUpgradeJdkCompatibilityTest.java | 596 +++++++++++++++++++++
3 files changed, 767 insertions(+), 11 deletions(-)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
index e6895cfd57..820a7dd84d 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
@@ -30,14 +30,24 @@
* the plugin has no 4.x pre-release line. Used to upgrade old 4.x alpha/beta/RC versions
* to the latest pre-release rather than downgrading to a 3.x version.
* @param reason the reason why this plugin needs to be upgraded
+ * @param minJdk the minimum JDK version required by this plugin version, or {@code 0} if
+ * the plugin works with any JDK. When the project's detected JDK is below this value,
+ * the upgrade is skipped to avoid {@code UnsupportedClassVersionError}.
*/
public record PluginUpgrade(
- String groupId, String artifactId, String minVersion, String latestPreRelease, String reason) {
+ String groupId, String artifactId, String minVersion, String latestPreRelease, String reason, int minJdk) {
/**
- * Convenience constructor for plugins without a 4.x pre-release line.
+ * Convenience constructor for plugins without a 4.x pre-release line and no JDK requirement.
*/
public PluginUpgrade(String groupId, String artifactId, String minVersion, String reason) {
- this(groupId, artifactId, minVersion, null, reason);
+ this(groupId, artifactId, minVersion, null, reason, 0);
+ }
+
+ /**
+ * Convenience constructor for plugins with a 4.x pre-release line but no JDK requirement.
+ */
+ public PluginUpgrade(String groupId, String artifactId, String minVersion, String latestPreRelease, String reason) {
+ this(groupId, artifactId, minVersion, latestPreRelease, reason, 0);
}
}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
index f294e8b959..fdf6f63d6e 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
@@ -40,6 +40,7 @@
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginManagement;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.apache.maven.impl.JdkSourceLevelSupport;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
@@ -155,7 +156,14 @@ public class PluginUpgradeStrategy extends AbstractUpgradeStrategy {
DEFAULT_MAVEN_PLUGIN_GROUP_ID,
"maven-ear-plugin",
"3.4.0",
- "Older versions use plexus-archiver reflection blocked by JDK 17+ module system"));
+ "Older versions use plexus-archiver reflection blocked by JDK 17+ module system"),
+ new PluginUpgrade(
+ DEFAULT_MAVEN_PLUGIN_GROUP_ID,
+ "maven-checkstyle-plugin",
+ "3.6.0",
+ null,
+ MAVEN_4_COMPATIBILITY_REASON,
+ 21));
private static final List<PluginUpgrade> PLUGIN_DEPENDENCY_UPGRADES = List.of(new PluginUpgrade(
"org.codehaus.mojo",
@@ -316,7 +324,8 @@ private Map<String, PluginUpgradeInfo> getPluginUpgradesMap() {
upgrade.groupId(),
upgrade.artifactId(),
upgrade.minVersion(),
- upgrade.latestPreRelease())));
+ upgrade.latestPreRelease(),
+ upgrade.minJdk())));
}
/**
@@ -414,6 +423,18 @@ private boolean upgradePluginVersion(
}
}
+ // Check JDK compatibility: if the plugin requires a higher JDK than the project
+ // uses, skip the upgrade to avoid UnsupportedClassVersionError at build time.
+ if (upgrade.minJdk > 0) {
+ int projectJdk = detectProjectJdkVersion(pomDocument);
+ if (projectJdk > 0 && projectJdk < upgrade.minJdk) {
+ context.warning("Skipping " + upgrade.groupId + ":" + upgrade.artifactId + " upgrade to "
+ + upgrade.minVersion + ": plugin requires JDK " + upgrade.minJdk + " but project targets JDK "
+ + projectJdk);
+ return false;
+ }
+ }
+
if (isProperty) {
// For Quarkus plugins, check if the property is shared with a Quarkus BOM
if (isQuarkusPlugin(upgrade.groupId, upgrade.artifactId)
@@ -656,6 +677,15 @@ private PluginAnalysisResults analyzePluginsUsingEffectiveModels(
Map<Path, Set<String>> directOverrideResult = new HashMap<>();
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
+ // Detect the project JDK version from any POM in the map (typically the root)
+ int projectJdk = -1;
+ for (Document doc : pomMap.values()) {
+ projectJdk = detectProjectJdkVersion(doc);
+ if (projectJdk > 0) {
+ break;
+ }
+ }
+
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path originalPomPath = entry.getKey();
@@ -666,7 +696,8 @@ private PluginAnalysisResults analyzePluginsUsingEffectiveModels(
Path tempPomPath = tempDir.resolve(relativePath);
// Build effective model using Maven 4 API
- PluginAnalysis analysis = analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades);
+ PluginAnalysis analysis =
+ analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades, projectJdk);
// Determine where to add plugin management (last local parent)
Path targetPom =
@@ -708,9 +739,9 @@ private Map<String, PluginUpgrade> getPluginUpgradesAsMap() {
}
private PluginAnalysis analyzeEffectiveModelForPlugins(
- UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades) {
+ UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades, int projectJdk) {
Model effectiveModel = buildEffectiveModel(tempPomPath);
- return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades);
+ return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades, projectJdk);
}
/**
@@ -719,7 +750,7 @@ private PluginAnalysis analyzeEffectiveModelForPlugins(
* explicitly in an inherited parent's build/plugins, not via pluginManagement).
*/
private PluginAnalysis analyzePluginsFromEffectiveModel(
- UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades) {
+ UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades, int projectJdk) {
Set<String> needsManagement = new HashSet<>();
Set<String> needsDirectOverride = new HashSet<>();
@@ -740,6 +771,13 @@ private PluginAnalysis analyzePluginsFromEffectiveModel(
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
+ // Skip plugins that require a higher JDK than the project targets
+ if (upgrade.minJdk() > 0 && projectJdk > 0 && projectJdk < upgrade.minJdk()) {
+ context.warning("Skipping " + pluginKey + " upgrade to " + upgrade.minVersion()
+ + " in effective model: plugin requires JDK " + upgrade.minJdk()
+ + " but project targets JDK " + projectJdk);
+ continue;
+ }
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
@@ -766,6 +804,13 @@ private PluginAnalysis analyzePluginsFromEffectiveModel(
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null && !needsManagement.contains(pluginKey)) {
+ // Skip plugins that require a higher JDK than the project targets
+ if (upgrade.minJdk() > 0 && projectJdk > 0 && projectJdk < upgrade.minJdk()) {
+ context.warning("Skipping " + pluginKey + " upgrade to " + upgrade.minVersion()
+ + " in effective model: plugin requires JDK " + upgrade.minJdk()
+ + " but project targets JDK " + projectJdk);
+ continue;
+ }
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
@@ -1039,6 +1084,101 @@ private record PluginAnalysis(Set<String> needsManagement, Set<String> needsDire
private record PluginAnalysisResults(
Map<Path, Set<String>> pluginsNeedingManagement, Map<Path, Set<String>> pluginsNeedingDirectOverride) {}
+ /**
+ * Detects the project's JDK version from the POM document.
+ * Checks the following sources in order:
+ * <ol>
+ * <li>{@code maven.compiler.release} property</li>
+ * <li>{@code maven.compiler.source} property</li>
+ * <li>{@code maven.compiler.target} property</li>
+ * <li>Compiler plugin configuration ({@code <release>} or {@code <source>})</li>
+ * </ol>
+ *
+ * @param pomDocument the POM document to inspect
+ * @return the detected JDK major version, or {@code -1} if not found
+ */
+ int detectProjectJdkVersion(Document pomDocument) {
+ Element root = pomDocument.root();
+
+ // Check properties: maven.compiler.release takes precedence, then source, then target
+ Element properties = root.childElement(PROPERTIES).orElse(null);
+ if (properties != null) {
+ for (String propName :
+ List.of("maven.compiler.release", "maven.compiler.source", "maven.compiler.target")) {
+ Element propElement = properties.childElement(propName).orElse(null);
+ if (propElement != null) {
+ String value = propElement.textContentTrimmed();
+ if (value != null && !value.isEmpty() && !value.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(value);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+
+ // Check compiler plugin configuration
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build != null) {
+ int level = detectJdkFromPluginSection(build.childElement(PLUGINS).orElse(null));
+ if (level > 0) {
+ return level;
+ }
+
+ Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pluginManagement != null) {
+ level = detectJdkFromPluginSection(
+ pluginManagement.childElement(PLUGINS).orElse(null));
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ private int detectJdkFromPluginSection(Element pluginsElement) {
+ if (pluginsElement == null) {
+ return -1;
+ }
+
+ for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
+ String artifactId = getChildText(plugin, ARTIFACT_ID);
+ String groupId = getChildText(plugin, GROUP_ID);
+ if ("maven-compiler-plugin".equals(artifactId)
+ && (groupId == null || groupId.isEmpty() || DEFAULT_MAVEN_PLUGIN_GROUP_ID.equals(groupId))) {
+ Element config = plugin.childElement("configuration").orElse(null);
+ if (config != null) {
+ // <release> takes precedence
+ Element releaseNode = config.childElement("release").orElse(null);
+ if (releaseNode != null) {
+ String text = releaseNode.textContentTrimmed();
+ if (text != null && !text.isEmpty() && !text.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(text);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ Element sourceNode = config.childElement("source").orElse(null);
+ if (sourceNode != null) {
+ String text = sourceNode.textContentTrimmed();
+ if (text != null && !text.isEmpty() && !text.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(text);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
/**
* Checks if the given plugin is a Quarkus Maven plugin.
*/
@@ -1302,6 +1442,9 @@ public static class PluginUpgradeInfo {
/** The latest available 4.x pre-release version, or null if none exists */
final String latestPreRelease;
+ /** The minimum JDK version required by this plugin, or 0 if any JDK works */
+ final int minJdk;
+
/**
* Creates a new plugin upgrade information holder.
*
@@ -1313,16 +1456,23 @@ public static class PluginUpgradeInfo {
* the minimum version required for Maven 4 compatibility
* @param latestPreRelease
* the latest 4.x pre-release version, or null
+ * @param minJdk
+ * the minimum JDK version required by this plugin, or 0 if any JDK works
*/
- PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease) {
+ PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease, int minJdk) {
this.groupId = groupId;
this.artifactId = artifactId;
this.minVersion = minVersion;
this.latestPreRelease = latestPreRelease;
+ this.minJdk = minJdk;
+ }
+
+ PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease) {
+ this(groupId, artifactId, minVersion, latestPreRelease, 0);
}
PluginUpgradeInfo(String groupId, String artifactId, String minVersion) {
- this(groupId, artifactId, minVersion, null);
+ this(groupId, artifactId, minVersion, null, 0);
}
}
}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java
new file mode 100644
index 0000000000..6cb8936f54
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java
@@ -0,0 +1,596 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.cling.invoker.mvnup.goals;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Editor;
+import eu.maveniverse.domtrip.Element;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for JDK compatibility checking in {@link PluginUpgradeStrategy}.
+ * Verifies that plugin upgrades requiring a higher JDK than the project targets
+ * are skipped to avoid {@code UnsupportedClassVersionError}.
+ *
+ * @see <a href="https://github.com/apache/maven/issues/12989">apache/maven#12989</a>
+ */
+@DisplayName("PluginUpgradeStrategy — JDK Compatibility")
+class PluginUpgradeJdkCompatibilityTest {
+
+ private PluginUpgradeStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new PluginUpgradeStrategy();
+ }
+
+ private UpgradeContext createMockContext() {
+ return TestUtils.createMockContext();
+ }
+
+ @Nested
+ @DisplayName("Project JDK Detection")
+ class ProjectJdkDetectionTests {
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.release property")
+ void shouldDetectFromCompilerRelease() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.source property")
+ void shouldDetectFromCompilerSource() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(11, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.target property")
+ void shouldDetectFromCompilerTarget() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should prefer maven.compiler.release over source/target")
+ void shouldPreferReleaseOverSource() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ <maven.compiler.source>17</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(21, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should normalize old-style version like 1.8 to 8")
+ void shouldNormalizeOldStyleVersion() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(8, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from compiler plugin configuration")
+ void shouldDetectFromCompilerPluginConfig() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>17</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should return -1 when no JDK version is configured")
+ void shouldReturnNegativeOneWhenNoJdkConfigured() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(-1, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should skip property references (${...}) and return -1")
+ void shouldSkipPropertyReferences() {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>${java.version}</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(-1, strategy.detectProjectJdkVersion(document));
+ }
+ }
+
+ @Nested
+ @DisplayName("JDK Compatibility Skip")
+ class JdkCompatibilitySkipTests {
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade when project targets JDK 17")
+ void shouldSkipCheckstyleUpgradeForJdk17() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.4.0</version>"),
+ "checkstyle-plugin should NOT be upgraded when project targets JDK 17");
+ assertFalse(xml.contains("<version>3.6.0</version>"), "checkstyle-plugin should NOT be set to 3.6.0");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade when project targets JDK 11")
+ void shouldSkipCheckstyleUpgradeForJdk11() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ <maven.compiler.target>11</maven.compiler.target>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.3.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.3.0</version>"),
+ "checkstyle-plugin should NOT be upgraded when project targets JDK 11");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when project targets JDK 21")
+ void shouldUpgradeCheckstyleForJdk21() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded to 3.6.0 for JDK 21");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when project targets JDK 23")
+ void shouldUpgradeCheckstyleForJdk23() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>23</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded to 3.6.0 for JDK 23");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when no JDK version is configured")
+ void shouldUpgradeCheckstyleWhenNoJdkConfigured() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded when no JDK version is configured");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade via property version for JDK 17")
+ void shouldSkipCheckstylePropertyUpgradeForJdk17() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ <checkstyle-plugin.version>3.4.0</checkstyle-plugin.version>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>${checkstyle-plugin.version}</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<checkstyle-plugin.version>3.4.0</checkstyle-plugin.version>"),
+ "checkstyle-plugin property should NOT be upgraded for JDK 17");
+ }
+
+ @Test
+ @DisplayName("should not skip plugins without minJdk requirement")
+ void shouldNotSkipPluginsWithoutMinJdk() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>11</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.0.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.5.2", version, "surefire-plugin should still be upgraded (no minJdk requirement)");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin in pluginManagement for JDK 17")
+ void shouldSkipCheckstyleInPluginManagementForJdk17() throws Exception {
+ 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>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.4.0</version>"),
+ "checkstyle-plugin in pluginManagement should NOT be upgraded for JDK 17");
+ }
+ }
+
+ @Nested
+ @DisplayName("PluginUpgrade Record")
+ class PluginUpgradeRecordTests {
+
+ @Test
+ @DisplayName("should have minJdk=0 for convenience constructor without JDK")
+ void shouldHaveDefaultMinJdk() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", "reason");
+ assertEquals(0, upgrade.minJdk(), "Default minJdk should be 0");
+ }
+
+ @Test
+ @DisplayName("should have minJdk=0 for five-arg convenience constructor")
+ void shouldHaveDefaultMinJdkForFiveArgConstructor() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", "2.0-beta-1", "reason");
+ assertEquals(0, upgrade.minJdk(), "Default minJdk should be 0 for five-arg constructor");
+ }
+
+ @Test
+ @DisplayName("should preserve minJdk value in full constructor")
+ void shouldPreserveMinJdk() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", null, "reason", 21);
+ assertEquals(21, upgrade.minJdk(), "minJdk should be preserved");
+ }
+
+ @Test
+ @DisplayName("checkstyle plugin upgrade should have minJdk=21")
+ void checkstylePluginShouldHaveMinJdk21() {
+ PluginUpgrade checkstyleUpgrade = PluginUpgradeStrategy.getPluginUpgrades().stream()
+ .filter(u -> "maven-checkstyle-plugin".equals(u.artifactId()))
+ .findFirst()
+ .orElse(null);
+
+ assertTrue(checkstyleUpgrade != null, "checkstyle-plugin should be in PLUGIN_UPGRADES");
+ assertEquals(21, checkstyleUpgrade.minJdk(), "checkstyle-plugin should require JDK 21");
+ assertEquals("3.6.0", checkstyleUpgrade.minVersion(), "checkstyle-plugin minVersion should be 3.6.0");
+ }
+ }
+}