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, ExecutionContext> 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, ExecutionContext> 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(