diff --git a/src/main/java/org/openrewrite/java/migrate/search/ModuleOrParentHasDependency.java b/src/main/java/org/openrewrite/java/migrate/search/ModuleOrParentHasDependency.java new file mode 100644 index 0000000000..14c18538ea --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/search/ModuleOrParentHasDependency.java @@ -0,0 +1,125 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.openrewrite.java.migrate.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.SourceFile; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.gradle.marker.GradleDependencyConfiguration; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.java.marker.JavaProject; +import org.openrewrite.marker.Markers; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.maven.tree.MavenResolutionResult; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +@EqualsAndHashCode(callSuper = false) +@Value +public class ModuleOrParentHasDependency extends ScanningRecipe { + + @Option(displayName = "Group pattern", + description = "Group glob pattern used to match dependencies.", + example = "org.projectlombok") + String groupIdPattern; + + @Option(displayName = "Artifact pattern", + description = "Artifact glob pattern used to match dependencies.", + example = "lombok") + String artifactIdPattern; + + String displayName = "Module or its reactor parent has dependency"; + + String description = "Marks all files in modules that have a matching dependency, and the in-reactor parent poms " + + "those modules inherit from. Intended as a precondition for recipes that configure a module through its " + + "parent's `pluginManagement`, which a per-module precondition would otherwise keep from being visited."; + + public static class Accumulator { + Set modules = new HashSet<>(); + Set parentPoms = new HashSet<>(); + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof SourceFile && hasDependency(tree.getMarkers())) { + tree.getMarkers().findFirst(JavaProject.class).ifPresent(acc.modules::add); + tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(mrr -> { + for (MavenResolutionResult p = mrr; p.parentPomIsProjectPom() && p.getParent() != null; p = p.getParent()) { + Path parentPath = p.getParent().getPom().getRequested().getSourcePath(); + if (parentPath == null) { + break; + } + acc.parentPoms.add(parentPath); + } + }); + } + return tree; + } + }; + } + + private boolean hasDependency(Markers markers) { + Optional mrr = markers.findFirst(MavenResolutionResult.class); + if (mrr.isPresent()) { + return !mrr.get().findDependencies(groupIdPattern, artifactIdPattern, null).isEmpty(); + } + Optional gp = markers.findFirst(GradleProject.class); + if (gp.isPresent()) { + for (GradleDependencyConfiguration configuration : gp.get().getConfigurations()) { + if (configuration.findRequestedDependency(groupIdPattern, artifactIdPattern) != null || + configuration.findResolvedDependency(groupIdPattern, artifactIdPattern) != null) { + return true; + } + } + } + return false; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new TreeVisitor() { + @Override + public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + assert tree != null; + Optional jp = tree.getMarkers().findFirst(JavaProject.class); + if (jp.isPresent() && acc.modules.contains(jp.get())) { + return SearchResult.found(tree, "Module has dependency"); + } + if (tree instanceof SourceFile && acc.parentPoms.contains(((SourceFile) tree).getSourcePath())) { + return SearchResult.found(tree, "Parent of a module with the dependency"); + } + return tree; + } + }; + } +} diff --git a/src/main/resources/META-INF/rewrite/java-version-25.yml b/src/main/resources/META-INF/rewrite/java-version-25.yml index 32cb941f6f..fca3a2b635 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -174,7 +174,8 @@ description: >- With Java 23 the encapsulation of JDK internals made it necessary to configure annotation processors like Lombok explicitly. The change is valid for older versions as well. preconditions: - - org.openrewrite.java.dependencies.search.ModuleHasDependency: + # Marks the reactor parent too: `AddAnnotationProcessor` configures a module through its parent's `pluginManagement`. + - org.openrewrite.java.migrate.search.ModuleOrParentHasDependency: groupIdPattern: org.projectlombok artifactIdPattern: lombok recipeList: @@ -204,6 +205,8 @@ recipeList: - org.openrewrite.gradle.UpdateGradleWrapper: version: 9.1.0 addIfMissing: false + # Gradle 9 removed the JavaPluginConvention, so the wrapper bump above turns a project-level `sourceCompatibility` into an error. + - org.openrewrite.gradle.gradle9.UseJavaExtensionBlock - org.openrewrite.maven.UpgradePluginVersion: groupId: org.apache.maven.plugins artifactId: maven-compiler-plugin @@ -217,6 +220,29 @@ recipeList: groupId: org.apache.maven.plugins artifactId: maven-failsafe-plugin newVersion: 3.5.x + # Plugins whose older lines fail to load on JDK 25 (plexus-utils reflection, ASM without class file version 69). + - org.openrewrite.maven.UpgradePluginVersion: + groupId: org.apache.maven.plugins + artifactId: maven-enforcer-plugin + newVersion: 3.x + - org.openrewrite.maven.UpgradePluginVersion: + groupId: org.apache.maven.plugins + artifactId: maven-assembly-plugin + newVersion: 3.x + - org.openrewrite.maven.UpgradePluginVersion: + groupId: org.apache.maven.plugins + artifactId: maven-plugin-plugin + newVersion: 3.15.x + addVersionIfMissing: true + - org.openrewrite.maven.UpgradePluginVersion: + groupId: org.apache.maven.plugins + artifactId: maven-checkstyle-plugin + newVersion: 3.6.x + # A checkstyle pinned on the plugin must be able to parse the pattern-matching `switch` the Java 21 migration writes. + - org.openrewrite.java.dependencies.UpgradeDependencyVersion: + groupId: com.puppycrawl.tools + artifactId: checkstyle + newVersion: 10.x - org.openrewrite.java.dependencies.UpgradeDependencyVersion: groupId: net.bytebuddy artifactId: byte-buddy* diff --git a/src/test/java/org/openrewrite/java/migrate/UpgradeJavaVersionTest.java b/src/test/java/org/openrewrite/java/migrate/UpgradeJavaVersionTest.java index 3ff033069a..284b321195 100644 --- a/src/test/java/org/openrewrite/java/migrate/UpgradeJavaVersionTest.java +++ b/src/test/java/org/openrewrite/java/migrate/UpgradeJavaVersionTest.java @@ -26,8 +26,10 @@ import java.util.List; import java.util.UUID; +import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.gradle.Assertions.buildGradle; import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.mavenProject; import static org.openrewrite.java.Assertions.version; import static org.openrewrite.maven.Assertions.pomXml; @@ -128,6 +130,242 @@ void mavenUpgradeFromJava8ToJava17ViaConfiguration() { ) ); } + + @Test + void mavenReleaseThroughCustomPropertyInParentPluginManagement() { + rewriteRun( + spec -> spec.recipe(new UpgradeJavaVersion(25)), + mavenProject("parent", + pomXml( + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + 8 + + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${project.build.release.version} + + + + + + + """, + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + 25 + + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${project.build.release.version} + + + + + + + """ + ), + mavenProject("child", + pomXml( + //language=xml + """ + + 4.0.0 + + com.mycompany.app + parent + 1 + + child + + """ + ) + ) + ) + ); + } + + @Test + void mavenSourceTargetInParentPluginManagement() { + rewriteRun( + spec -> spec.recipe(new UpgradeJavaVersion(25)), + mavenProject("parent", + pomXml( + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + + + + + + + """, + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 25 + + + + + + + """ + ), + mavenProject("child", + pomXml( + //language=xml + """ + + 4.0.0 + + com.mycompany.app + parent + 1 + + child + + """ + ) + ) + ) + ); + } + + @Test + void mavenCompilerPropertiesInParentWithTestSourceTarget() { + rewriteRun( + spec -> spec.recipe(new UpgradeJavaVersion(25)), + mavenProject("parent", + pomXml( + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + 1.8 + 1.8 + 1.8 + 1.8 + + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.testSource} + ${maven.compiler.testTarget} + + + + + + + """, + spec -> spec.after(actual -> assertThat(actual) + .doesNotContain("1.8") + .contains("25") + .actual()) + ), + mavenProject("child", + pomXml( + //language=xml + """ + + 4.0.0 + + com.mycompany.app + parent + 1 + + child + + """ + ) + ) + ) + ); + } } @Nested diff --git a/src/test/java/org/openrewrite/java/migrate/UpgradeToJava25Test.java b/src/test/java/org/openrewrite/java/migrate/UpgradeToJava25Test.java index 6b7fd95cd0..e561c40383 100644 --- a/src/test/java/org/openrewrite/java/migrate/UpgradeToJava25Test.java +++ b/src/test/java/org/openrewrite/java/migrate/UpgradeToJava25Test.java @@ -23,6 +23,7 @@ import org.openrewrite.test.RewriteTest; import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.gradle.Assertions.buildGradle; import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; import static org.openrewrite.java.Assertions.mavenProject; import static org.openrewrite.maven.Assertions.pomXml; @@ -156,6 +157,92 @@ void upgradesGradleWrapperForJava25() { ); } + @Test + void upgradesBuildPluginsThatCannotRunOnJava25() { + rewriteRun( + spec -> spec.recipeFromResources("org.openrewrite.java.migrate.UpgradePluginsForJava25"), + mavenProject("project", + pomXml( + //language=xml + """ + + com.mycompany.app + my-app + 1 + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.6 + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.1 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + com.puppycrawl.tools + checkstyle + 8.45 + + + + + + + """, + spec -> spec.after(actual -> + assertThat(actual) + .containsPattern("maven-enforcer-plugin\\s*3\\.") + .containsPattern("maven-assembly-plugin\\s*3\\.") + .containsPattern("maven-plugin-plugin\\s*3\\.15\\.") + .containsPattern("maven-checkstyle-plugin\\s*3\\.6\\.") + .containsPattern("checkstyle\\s*10\\.") + .actual()) + ) + ) + ); + } + + @Test + void movesProjectLevelCompatibilityIntoJavaBlockForGradle9() { + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()), + buildGradle( + //language=groovy + """ + plugins { id 'java' } + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + """, + //language=groovy + """ + plugins { id 'java' } + + java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + } + """ + ) + ); + } + @Test void kotlin1xCapsJavaVersionAt24WithComment() { // Kotlin 1.x is left untouched: crossing the K2 compiler default introduced in Kotlin 2.0 is source-breaking, @@ -475,6 +562,72 @@ void kotlinNewerThan2_3UpgradesToJava25() { ); } + @Test + void addsLombokAnnotationProcessorToReactorParent() { + rewriteRun( + spec -> spec.recipeFromResources("org.openrewrite.java.migrate.EnableLombokAnnotationProcessor"), + mavenProject("parent", + pomXml( + //language=xml + """ + + 4.0.0 + com.mycompany.app + parent + 1 + pom + + child + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 25 + + + + + + + """, + spec -> spec.after(actual -> + assertThat(actual) + .containsPattern("(.|\\n)*(.|\\n)*lombok") + .actual() + ) + ), + mavenProject("child", + pomXml( + //language=xml + """ + + 4.0.0 + + com.mycompany.app + parent + 1 + + child + + + org.projectlombok + lombok + 1.18.40 + + + + """ + ) + ) + ) + ); + } + @Test void addsLombokAnnotationProcessor() { rewriteRun(