diff --git a/src/main/java/org/openrewrite/java/migrate/jakarta/HasNoJakartaAnnotations.java b/src/main/java/org/openrewrite/java/migrate/jakarta/HasNoJakartaAnnotations.java new file mode 100644 index 0000000000..528200eaa9 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/jakarta/HasNoJakartaAnnotations.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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.jakarta; + +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.marker.JavaProject; +import org.openrewrite.java.search.FindAnnotations; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +import java.util.HashSet; +import java.util.Set; + +public class HasNoJakartaAnnotations extends ScanningRecipe { + @Override + public String getDisplayName() { + return "Project has no Jakarta annotations"; + } + + @Override + public String getDescription() { + return "Mark all source as found per `JavaProject` where no Jakarta annotations are found. " + + "This is useful mostly as a precondition for recipes that require Jakarta annotations to be present"; + } + + @Value + public static class Accumulator { + Set projectsWithDependency; + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(new HashSet<>()); + } + + @Override + public TreeVisitor getScanner(HasNoJakartaAnnotations.Accumulator acc) { + return new TreeVisitor() { + @Override + public Tree preVisit(Tree tree, ExecutionContext ctx) { + stopAfterPreVisit(); + if (tree instanceof J) { + tree.getMarkers().findFirst(JavaProject.class) + .filter(jp -> !acc.getProjectsWithDependency().contains(jp)) + .filter(jp -> !FindAnnotations.find((J) tree, "@jakarta.annotation.*", true).isEmpty()) + .ifPresent(jp -> acc.getProjectsWithDependency().add(jp)); + } + return tree; + } + }; + } + + @Override + public TreeVisitor getVisitor(HasNoJakartaAnnotations.Accumulator acc) { + return new TreeVisitor() { + @Override + public Tree preVisit(Tree tree, ExecutionContext ctx) { + stopAfterPreVisit(); + return tree.getMarkers().findFirst(JavaProject.class) + .filter(it -> !acc.getProjectsWithDependency().contains(it)) + .map(__ -> SearchResult.found(tree, "Project has no Jakarta annotations")) + .orElse(tree); + } + }; + } +} diff --git a/src/main/resources/META-INF/rewrite/jakarta-ee-9.yml b/src/main/resources/META-INF/rewrite/jakarta-ee-9.yml index f9783487b7..69d3657524 100644 --- a/src/main/resources/META-INF/rewrite/jakarta-ee-9.yml +++ b/src/main/resources/META-INF/rewrite/jakarta-ee-9.yml @@ -1083,6 +1083,7 @@ name: org.openrewrite.java.migrate.jakarta.RemoveJakartaAnnotationDependency displayName: Remove `jakarta.annotation-api` dependency when managed by Spring Boot description: Counteract the `jakarta.annotation-api` added by `org.openrewrite.java.migrate.javax.AddCommonAnnotationsDependencies` for Spring Boot applications. preconditions: + - org.openrewrite.java.migrate.jakarta.HasNoJakartaAnnotations - org.openrewrite.java.dependencies.DependencyInsight: groupIdPattern: org.springframework.boot artifactIdPattern: spring-boot-starter diff --git a/src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java b/src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java index 94b2f2fe17..f57b4f7f24 100644 --- a/src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jakarta/JavaxToJakartaTest.java @@ -24,6 +24,9 @@ import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import static org.openrewrite.gradle.Assertions.buildGradle; +import static org.openrewrite.gradle.Assertions.settingsGradle; +import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; import static org.openrewrite.java.Assertions.*; import static org.openrewrite.maven.Assertions.pomXml; import static org.openrewrite.xml.Assertions.xml; @@ -60,6 +63,16 @@ public void foo() {} } """; + @Language("java") + private static final String jakartaAnnotation = + """ + package jakarta.annotation; + public @interface Nonnull { + } + public @interface Nullable { + } + """; + @Override public void defaults(RecipeSpec spec) { spec.recipe( @@ -442,9 +455,7 @@ void projectWithSpringBootStarterWeb() { //language=xml pomXml( """ - - + 4.0.0 org.springframework.boot @@ -511,9 +522,7 @@ void projectWithSpringBoot3StarterWebShouldRemoveJakartaDependency() { //language=xml pomXml( """ - - + 4.0.0 org.springframework.boot @@ -538,9 +547,7 @@ void projectWithSpringBoot3StarterWebShouldRemoveJakartaDependency() { """, """ - - + 4.0.0 org.springframework.boot @@ -574,6 +581,242 @@ public class TestApplication { ); } + @Test + void projectWithSpringBoot3StarterWebShouldNotRemoveJakartaDependencyWhenUsingNonnullAnnotation() { + rewriteRun( + spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)), + mavenProject( + "Sample", + //language=xml + pomXml( + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.example + demo + 0.0.1-SNAPSHOT + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + + + org.springframework.boot + spring-boot-starter-web + + + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.example + demo + 0.0.1-SNAPSHOT + + + jakarta.annotation + jakarta.annotation-api + 2.0.0 + + + org.springframework.boot + spring-boot-starter-web + + + + """ + ), + srcMainJava( + //language=java + java( + """ + import jakarta.annotation.Nonnull; + + public class TestApplication { + @Nonnull + public String upperCase(@Nonnull String input) { + return input.toUpperCase(); + } + } + """ + ) + ) + ) + ); + } + + @Test + void projectWithSpringBoot3StarterWebShouldNotRemoveJakartaDependencyWhenUsingNullableAnnotation() { + rewriteRun( + spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)), + mavenProject( + "Sample", + //language=xml + pomXml( + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.example + demo + 0.0.1-SNAPSHOT + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + + + org.springframework.boot + spring-boot-starter-web + + + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.example + demo + 0.0.1-SNAPSHOT + + + jakarta.annotation + jakarta.annotation-api + 2.0.0 + + + org.springframework.boot + spring-boot-starter-web + + + + """ + ), + srcMainJava( + //language=java + java( + """ + import jakarta.annotation.Nullable; + + public class TestApplication { + @Nullable + public String safeUpperCase(@Nullable String input) { + return input == null ? null : input.toUpperCase(); + } + } + """ + ) + ) + ) + ); + } + + @Test + void multiProjectWithSpringBoot3StarterWebShouldRemoveJakartaDependencyWhenUsingNullableAnnotationWhenApplicable() { + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()).parser(JavaParser.fromJavaVersion().dependsOn(javaxServlet, jakartaAnnotation)), + mavenProject("multi-project-build", + //language=groovy + settingsGradle(""" + include 'project-with-null-annotations' + include 'project-without-null-annotations' + """), + mavenProject("project-with-null-annotations", + //language=groovy + buildGradle( + """ + plugins { + id 'java' + } + + repositories { + mavenCentral() + } + + dependencies { + implementation 'jakarta.annotation:jakarta.annotation-api:1.3.5' + implementation 'org.springframework.boot:spring-boot-starter-web' + } + """, + """ + plugins { + id 'java' + } + + repositories { + mavenCentral() + } + + dependencies { + implementation 'jakarta.annotation:jakarta.annotation-api:2.0.0' + implementation 'org.springframework.boot:spring-boot-starter-web' + } + """ + ), + srcMainJava( + //language=java + java( + """ + import jakarta.annotation.Nullable; + + public class TestApplication { + @Nullable + public String safeUpperCase(@Nullable String input) { + return input == null ? null : input.toUpperCase(); + } + } + """ + ) + ) + ), + mavenProject("project-without-null-annotations", + //language=groovy + buildGradle( + """ + plugins { + id 'java' + } + + repositories { + mavenCentral() + } + + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + } + """ + ) + ) + ) + ); + } + @Test void upgradeAnnotationApiFromV1ToV2() { rewriteRun(