diff --git a/rewrite-java-test/build.gradle.kts b/rewrite-java-test/build.gradle.kts index c349744c5f0..9ec9a8271fb 100644 --- a/rewrite-java-test/build.gradle.kts +++ b/rewrite-java-test/build.gradle.kts @@ -9,10 +9,20 @@ dependencies { testImplementation("io.github.classgraph:classgraph:latest.release") testImplementation("org.junit-pioneer:junit-pioneer:2.0.0") + testRuntimeOnly(project(":rewrite-java-17")) testRuntimeOnly("junit:junit:4.13.2") { because("Used for RemoveUnneededAssertionTest") } + testRuntimeOnly("com.google.code.findbugs:jsr305:3.0.2") { + because("Used for StandardizeNullabilityAnnotationsTest, UseJavaxNullabilityAnnotations and UseOpenRewriteNullabilityAnnotations") + } + testRuntimeOnly("org.springframework:spring-core:6.0.7") { + because("Used for StandardizeNullabilityAnnotationsTest and UseSpringNullabilityAnnotations") + } + testRuntimeOnly("jakarta.annotation:jakarta.annotation-api:2.1.1") { + because("Used for UseJakartaNullabilityAnnotations") + } testRuntimeOnly("org.apache.hbase:hbase-shaded-client:2.4.11") testRuntimeOnly("com.google.guava:guava:latest.release") testRuntimeOnly("org.mapstruct:mapstruct:latest.release") diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsTest.java new file mode 100644 index 00000000000..f817558714e --- /dev/null +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsTest.java @@ -0,0 +1,516 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openrewrite.internal.lang.NonNull; +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.util.List; + +import static org.openrewrite.java.Assertions.java; + +class StandardizeNullabilityAnnotationsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("rewrite-core", "jsr305", "spring-core")); + } + + @Test + void removesImportIfPossible() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.internal.lang; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """, + + """ + package org.openrewrite.internal.lang; + + class Test { + @NonNull + String variable = ""; + } + """)); + } + + @Test + void addsImportIfNecessary() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of("javax.annotation.Nullable", "javax.annotation.Nonnull"))), java(""" + package org.openrewrite.internal.lang; + + class Test { + @NonNull + String variable = ""; + } + """, + + """ + package org.openrewrite.internal.lang; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """)); + } + + @Test + void doesNotAddImportIfUnnecessary() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.internal.lang; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """, + + """ + package org.openrewrite.internal.lang; + + class Test { + @NonNull + String variable = ""; + } + """)); + } + + @Test + void unchangedWhenNoNullabilityAnnotationWasUsed() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + class Test { + String variable = ""; + } + """)); + } + + @Test + void unchangedWhenOnlyTheConfiguredNullabilityAnnotationsWereUsed() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + import org.openrewrite.internal.lang.Nullable; + + class Test { + @NonNull + String nonNullVariable = ""; + + @Nullable + String nullableVariable; + } + """)); + } + + @Test + void replacesAllAnnotationsIfDifferentNonConfiguredAnnotationWereUsed() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + import org.springframework.lang.Nullable; + + class Test { + @Nonnull + String nonNullVariable = ""; + + @Nullable + String nullableVariable; + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + import org.openrewrite.internal.lang.Nullable; + + class Test { + @NonNull + String nonNullVariable = ""; + + @Nullable + String nullableVariable; + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnPackage() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(NonNullApi.class.getName()))), java(""" + @NonNullApi + package org.openrewrite.java; + + import org.springframework.lang.NonNullApi; + """, """ + @NonNullApi + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNullApi; + """)); + } + + @Test + void shouldReplaceLeadingAnnotationsOnClass() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + @Nonnull + public class Test { + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + @NonNull + public class Test { + } + """)); + } + + @Test + @Disabled("annotation on kind not yet supported") + void shouldReplaceAnnotationsOnClass() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + public @Nonnull class Test { + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + public @NonNull class Test { + } + """)); + } + + @Test + @Disabled("annotations on modifiers not yet support") + void shouldReplaceAnnotationsBetweenModifiersOnClass() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + public @Nonnull final class Test { + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + public @NonNull final class Test { + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnMethod() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + public String getString() { + return ""; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + @NonNull + public String getString() { + return ""; + } + } + """)); + } + + @Test + @Disabled("annotations on modifiers not yet support") + void shouldReplaceAnnotationsBetweenModifiersOnMethod() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public @Nonnull static String getString() { + return ""; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public @NonNull static String getString() { + return ""; + } + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnReturnType() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public @Nonnull String getString() { + return ""; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public @NonNull String getString() { + return ""; + } + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnParameter() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public String getString(@Nonnull String parameter) { + return parameter; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public String getString(@NonNull String parameter) { + return parameter; + } + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnField() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String nonNullVariable = ""; + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + @NonNull + String nonNullVariable = ""; + } + """)); + } + + @Test + @Disabled("annotations on modifiers not yet support") + void shouldReplaceAnnotationsBetweenModifiersOnField() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public @Nonnull final String nonNullVariable = ""; + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public @NonNull final String nonNullVariable = ""; + } + """)); + } + + @Test + void shouldReplaceAnnotationsOnLocalField() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public String getString() { + @Nonnull + String parameter = ""; + return parameter; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public String getString() { + @NonNull + String parameter = ""; + return parameter; + } + } + """)); + } + + @Test + @Disabled("annotations on modifiers not yet support") + void shouldReplaceAnnotationsBeforeModifierOnLocalField() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + public String getString() { + @Nonnull final String parameter = ""; + return parameter; + } + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + public String getString() { + @NonNull final String parameter = ""; + return parameter; + } + } + """)); + } + + @Test + void shouldReplaceTwoAnnotationsWithOne() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of("javax.annotation.Nonnull"))), java(""" + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNullApi; + import org.openrewrite.internal.lang.NonNullFields; + + @NonNullApi + @NonNullFields + class Test { + } + """, """ + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + @Nonnull + class Test { + } + """)); + } + + @Test + void shouldReplaceOneAnnotationsWithTwo() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(NonNullApi.class.getName(), NonNullFields.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + @Nonnull + class Test { + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNullApi; + import org.openrewrite.internal.lang.NonNullFields; + + @NonNullApi + @NonNullFields + class Test { + } + """)); + } + + @Test + void shouldReplaceUsingFqnWhenCollidingImportExistsToNotBreakCode() { + rewriteRun(spec -> spec.recipe(new StandardizeNullabilityAnnotations(List.of(Nullable.class.getName(), NonNull.class.getName()))), java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + import org.springframework.lang.NonNull; + + @Nonnull + class Test { + } + """, """ + package org.openrewrite.java; + + import org.springframework.lang.NonNull; + + @org.openrewrite.internal.lang.NonNull + class Test { + } + """)); + } +} diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJakartaNullabilityAnnotationsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJakartaNullabilityAnnotationsTest.java new file mode 100644 index 00000000000..15231eabea0 --- /dev/null +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJakartaNullabilityAnnotationsTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.junit.jupiter.api.Test; +import org.openrewrite.config.Environment; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseJakartaNullabilityAnnotationsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("jsr305", "jakarta.annotation-api")) + .recipe(Environment.builder().scanRuntimeClasspath("org.openrewrite.java").build().activateRecipes("org.openrewrite.java.nullability.UseJakartaNullabilityAnnotations")); + } + + @Test + void replaceJavaxWithJakartaAnnotationsNonNull() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """, + """ + package org.openrewrite.java; + + import jakarta.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """)); + } + + @Test + void replaceJavaxWithJakartaAnnotationsNullable() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nullable; + + class Test { + @Nullable + String variable; + } + """, + + """ + package org.openrewrite.java; + + import jakarta.annotation.Nullable; + + class Test { + @Nullable + String variable; + } + """)); + } +} diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJavaxNullabilityAnnotationsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJavaxNullabilityAnnotationsTest.java new file mode 100644 index 00000000000..7bb2fa5644f --- /dev/null +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseJavaxNullabilityAnnotationsTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.junit.jupiter.api.Test; +import org.openrewrite.config.Environment; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseJavaxNullabilityAnnotationsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("rewrite-core", "jsr305")) + .recipe(Environment.builder().scanRuntimeClasspath("org.openrewrite.java").build().activateRecipes("org.openrewrite.java.nullability.UseJavaxNullabilityAnnotations")); + } + + @Test + void replaceOpenRewriteWithJavaxAnnotationsNonNull() { + rewriteRun(java(""" + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + @NonNull + String variable = ""; + } + """, + + """ + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """)); + } + + @Test + void replaceOpenRewriteWithJavaxAnnotationsNullable() { + rewriteRun(java(""" + package org.openrewrite.java; + + import org.openrewrite.internal.lang.Nullable; + + class Test { + @Nullable + String variable; + } + """, + + """ + package org.openrewrite.java; + + import javax.annotation.Nullable; + + class Test { + @Nullable + String variable; + } + """)); + } +} diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseOpenRewriteNullabilityAnnotationsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseOpenRewriteNullabilityAnnotationsTest.java new file mode 100644 index 00000000000..17a247b84d6 --- /dev/null +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseOpenRewriteNullabilityAnnotationsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.junit.jupiter.api.Test; +import org.openrewrite.config.Environment; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseOpenRewriteNullabilityAnnotationsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("rewrite-core", "jsr305")).recipe(Environment.builder().scanRuntimeClasspath("org.openrewrite.java").build().activateRecipes("org.openrewrite.java.nullability.UseOpenRewriteNullabilityAnnotations")); + } + + @Test + void replaceJavaxWithOpenRewriteAnnotationsNonNull() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + class Test { + @NonNull + String variable = ""; + } + """)); + } + + @Test + void replaceJavaxWithOpenRewriteAnnotationsNullable() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nullable; + + class Test { + @Nullable + String variable; + } + """, + + """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.Nullable; + + class Test { + @Nullable + String variable; + } + """)); + } + + @Test + void replaceJavaxWithOpenRewriteAnnotationNonNullClass() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + @Nonnull + class Test { + } + """, """ + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNull; + + @NonNull + class Test { + } + """)); + } + + @Test + void replaceJavaxWithOpenRewriteAnnotationsNonNullPackage() { + rewriteRun(java(""" + @Nonnull + package org.openrewrite.java; + + import javax.annotation.Nonnull; + """, """ + @NonNullApi + @NonNullFields + package org.openrewrite.java; + + import org.openrewrite.internal.lang.NonNullApi; + import org.openrewrite.internal.lang.NonNullFields; + """)); + } +} diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseSpringNullabilityAnnotationsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseSpringNullabilityAnnotationsTest.java new file mode 100644 index 00000000000..3274fabbbfc --- /dev/null +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/nullability/UseSpringNullabilityAnnotationsTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.junit.jupiter.api.Test; +import org.openrewrite.config.Environment; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class UseSpringNullabilityAnnotationsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("jsr305", "spring-core")) + .recipe(Environment.builder().scanRuntimeClasspath("org.openrewrite.java").build().activateRecipes("org.openrewrite.java.nullability.UseSpringNullabilityAnnotations")); + } + + @Test + void replaceJavaxWithSpringAnnotationsNonNull() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + class Test { + @Nonnull + String variable = ""; + } + """, + """ + package org.openrewrite.java; + + import org.springframework.lang.NonNull; + + class Test { + @NonNull + String variable = ""; + } + """)); + } + + @Test + void replaceJavaxWithSpringAnnotationsNullable() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nullable; + + class Test { + @Nullable + String variable; + } + """, + + """ + package org.openrewrite.java; + + import org.springframework.lang.Nullable; + + class Test { + @Nullable + String variable; + } + """)); + } + + @Test + void shouldNotReplaceJavaxWithSpringAnnotationsNonNullClass() { + rewriteRun(java(""" + package org.openrewrite.java; + + import javax.annotation.Nonnull; + + @Nonnull + class Test { + } + """ + )); + } + + @Test + void replaceJavaxWithSpringAnnotationsNonNullPackage() { + rewriteRun(java(""" + @Nonnull + package org.openrewrite.java; + + import javax.annotation.Nonnull; + """, """ + @NonNullApi + @NonNullFields + package org.openrewrite.java; + + import org.springframework.lang.NonNullApi; + import org.springframework.lang.NonNullFields; + """)); + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/format/WrappingAndBracesVisitor.java b/rewrite-java/src/main/java/org/openrewrite/java/format/WrappingAndBracesVisitor.java index c6f9d3bacdb..1a946c81cb9 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/format/WrappingAndBracesVisitor.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/format/WrappingAndBracesVisitor.java @@ -148,6 +148,12 @@ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, P return j; } + @Override + public J.Package visitPackage(J.Package pkg, P p) { + J.Package j = super.visitPackage(pkg, p); + return j.withAnnotations(withNewlines(j.getAnnotations())); + } + private List withNewlines(List annotations) { if (annotations.isEmpty()) { return annotations; diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/AnnotationTemplateGenerator.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/AnnotationTemplateGenerator.java index 50345fd75da..1a34c37ea9a 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/AnnotationTemplateGenerator.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/AnnotationTemplateGenerator.java @@ -130,7 +130,8 @@ private void template(Cursor cursor, J prior, StringBuilder before, StringBuilde before.insert(0, cu.getPackageDeclaration().withPrefix(Space.EMPTY).printTrimmed(cursor) + ";\n"); } List classes = cu.getClasses(); - if (!classes.get(classes.size() - 1).getName().getSimpleName().equals("$Placeholder")) { + boolean hasTrailingPlaceholderInterface = !classes.isEmpty() && classes.get(classes.size() - 1).getName().getSimpleName().equals("$Placeholder"); + if (!hasTrailingPlaceholderInterface){ after.append("@interface $Placeholder {}"); } return; diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/JavaTemplateJavaExtension.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/JavaTemplateJavaExtension.java index b821b60b376..c8d550cb098 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/JavaTemplateJavaExtension.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/JavaTemplateJavaExtension.java @@ -24,10 +24,7 @@ import org.openrewrite.java.tree.*; import org.openrewrite.marker.Markers; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.emptyList; @@ -57,6 +54,27 @@ public JavaTemplateJavaExtension(JavaTemplateParser templateParser, Substitution @Override public TreeVisitor getMixin() { return new JavaVisitor() { + + @Override + public J visitAnnotatedType(J.AnnotatedType annotatedType, Integer integer) { + if (annotatedType.isScope(insertionPoint) && loc == ANNOTATIONS) { + List generatedAnnotations = substitutions.unsubstitute(templateParser.parseAnnotations(getCursor(), substitutedTemplate)); + if (mode == JavaCoordinates.Mode.REPLACEMENT) { + J.AnnotatedType after = annotatedType.withAnnotations(generatedAnnotations); + return autoFormat(after, integer); + } else { + List newAnnotations = generatedAnnotations.stream().reduce( + annotatedType.getAnnotations(), + (currentAnnotations, a) -> ListUtils.insertInOrder(annotatedType.getAnnotations(), a, getComparatorOrThrow()), + (before, after) -> after + ); + J.AnnotatedType after = annotatedType.withAnnotations(newAnnotations); + return autoFormat(after, integer); + } + } + return super.visitAnnotatedType(annotatedType, integer); + } + @Override public J visitAnnotation(J.Annotation annotation, Integer integer) { if (loc.equals(ANNOTATION_PREFIX) && mode.equals(JavaCoordinates.Mode.REPLACEMENT) && @@ -437,8 +455,24 @@ public J visitMethodInvocation(J.MethodInvocation method, Integer integer) { @Override public J visitPackage(J.Package pkg, Integer integer) { - if (loc.equals(PACKAGE_PREFIX) && pkg.isScope(insertionPoint)) { - return pkg.withExpression(substitutions.unsubstitute(templateParser.parsePackage(substitutedTemplate))); + if (pkg.isScope(insertionPoint)) { + if (loc == PACKAGE_PREFIX) { + return pkg.withExpression(substitutions.unsubstitute(templateParser.parsePackage(substitutedTemplate))); + } else if (loc == ANNOTATIONS) { + List generatedAnnotations = substitutions.unsubstitute(templateParser.parseAnnotations(getCursor(), substitutedTemplate)); + if (mode == JavaCoordinates.Mode.REPLACEMENT) { + J.Package after = pkg.withAnnotations(generatedAnnotations); + return autoFormat(after, integer); + } else { + List newAnnotations = generatedAnnotations.stream().reduce( + pkg.getAnnotations(), + (currentAnnotations, a) -> ListUtils.insertInOrder(pkg.getAnnotations(), a, getComparatorOrThrow()), + (before, after) -> after + ); + J.Package after = pkg.withAnnotations(newAnnotations); + return autoFormat(after, integer); + } + } } return super.visitPackage(pkg, integer); } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/nullability/KnownNullabilityAnnotations.java b/rewrite-java/src/main/java/org/openrewrite/java/nullability/KnownNullabilityAnnotations.java new file mode 100644 index 00000000000..1c7fe0096a0 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/nullability/KnownNullabilityAnnotations.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.EnumSet; +import java.util.Set; + +@Getter +@AllArgsConstructor +public enum KnownNullabilityAnnotations implements NullabilityAnnotation { + + ANDROID_SUPPORT_NULLABLE("android.support.annotation.Nullable", Nullability.NULLABLE, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.LOCAL_FIELD, Target.PACKAGE), EnumSet.allOf(Scope.class)), + ANDROID_SUPPORT_NON_NULL("android.support.annotation.NonNull", Nullability.NONNULL, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.LOCAL_FIELD, Target.PACKAGE), EnumSet.allOf(Scope.class)), + + ANDROIDX_SUPPORT_NULLABLE("androidx.annotation.Nullable", Nullability.NULLABLE, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.LOCAL_FIELD, Target.PACKAGE), EnumSet.allOf(Scope.class)), + ANDROIDX_SUPPORT_NON_NULL("androidx.annotation.NonNull", Nullability.NONNULL, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.LOCAL_FIELD, Target.PACKAGE), EnumSet.allOf(Scope.class)), + + CHECKER_FRAMEWORK_NULLABLE("org.checkerframework.checker.nullness.qual.Nullable", Nullability.NULLABLE, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + CHECKER_FRAMEWORK_NON_NULL("org.checkerframework.checker.nullness.qual.NonNull", Nullability.NONNULL, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + + ECLIPSE_JDT_NULLABLE("org.eclipse.jdt.annotation.Nullable", Nullability.NULLABLE, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + ECLIPSE_JDT_NON_NULL("org.eclipse.jdt.annotation.NonNull", Nullability.NONNULL, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + /* + * The following annotation is commented out, because scopes it applies to are dynamically configurable with parameters. + * To support this, we would need to convert the static scopes attribute to a provider function which takes the parsed annotation to work with its argument. + * ECLIPSE_JDT_NON_NULL_BY_DEFAULT("org.eclipse.jdt.annotation.NonNullByDefault", Nullability.NONNULL, EnumSet.allOf(Target.class), annotation -> ...), + */ + + FINDBUGS_CHECK_FOR_NULL("edu.umd.cs.findbugs.annotations.CheckForNull", Nullability.NULLABLE, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + FINDBUGS_NULLABLE("edu.umd.cs.findbugs.annotations.Nullable", Nullability.NULLABLE, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + FINDBUGS_POSSIBLY_NULL("edu.umd.cs.findbugs.annotations.PossiblyNull", Nullability.NULLABLE, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + FINDBUGS_NON_NULL("edu.umd.cs.findbugs.annotations.NonNull", Nullability.NONNULL, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + FINDBUGS_UNKNOWN_NULLNESS("edu.umd.cs.findbugs.annotations.UnknownNullness", Nullability.UNKNOWN, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + + JAKARTA_NULLABLE("jakarta.annotation.Nullable", Nullability.NULLABLE, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + JAKARTA_NON_NULL("jakarta.annotation.Nonnull", Nullability.NONNULL, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + + JAVAX_NULLABLE("javax.annotation.Nullable", Nullability.NULLABLE, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + JAVAX_NON_NULL("javax.annotation.Nonnull", Nullability.NONNULL, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + + JETBRAINS_NULLABLE("org.jetbrains.annotations.Nullable", Nullability.NULLABLE, EnumSet.of(Target.METHOD, Target.FIELD, Target.PARAMETER, Target.LOCAL_FIELD, Target.TYPE_USE), EnumSet.allOf(Scope.class)), + JETBRAINS_NON_NULL("org.jetbrains.annotations.NotNull", Nullability.NONNULL, EnumSet.of(Target.METHOD, Target.FIELD, Target.PARAMETER, Target.LOCAL_FIELD, Target.TYPE_USE), EnumSet.allOf(Scope.class)), + JETBRAINS_UNKNOWN_NULLABILITY("org.jetbrains.annotations.UnknownNullability", Nullability.UNKNOWN, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + + JML_SPECS_NULLABLE("org.jmlspecs.annotation.Nullable", Nullability.NULLABLE, EnumSet.of(Target.TYPE_USE, Target.FIELD, Target.METHOD, Target.LOCAL_FIELD, Target.PARAMETER), EnumSet.allOf(Scope.class)), + JML_SPECS_NULLABLE_BY_DEFAULT("org.jmlspecs.annotation.NullableByDefault", Nullability.NULLABLE, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + JML_SPECS_NON_NULL("org.jmlspecs.annotation.NonNull", Nullability.NONNULL, EnumSet.of(Target.TYPE_USE, Target.FIELD, Target.METHOD, Target.LOCAL_FIELD, Target.PARAMETER), EnumSet.allOf(Scope.class)), + JML_SPECS_NON_NULL_BY_DEFAULT("org.jmlspecs.annotation.NonNullByDefault", Nullability.NONNULL, EnumSet.allOf(Target.class), EnumSet.allOf(Scope.class)), + + JSPECIFY_NULLABLE("org.jspecify.annotations.Nullable", Nullability.NULLABLE, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + JSPECIFY_NON_NULL("org.jspecify.annotations.NonNull", Nullability.NONNULL, EnumSet.of(Target.TYPE_USE), EnumSet.allOf(Scope.class)), + JSPECIFY_NULL_MARKED("org.jspecify.annotations.NullMarked", Nullability.NONNULL, EnumSet.of(Target.MODULE, Target.PACKAGE, Target.TYPE, Target.METHOD), EnumSet.allOf(Scope.class)), + JSPECIFY_NULL_UNMARKED("org.jspecify.annotations.NullUnmarked", Nullability.UNKNOWN, EnumSet.of(Target.PACKAGE, Target.TYPE, Target.METHOD), EnumSet.allOf(Scope.class)), + + LOMBOK_NON_NULL("lombok.NonNull", Nullability.NONNULL, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD, Target.TYPE_USE), EnumSet.allOf(Scope.class)), + + NETBEANS_CHECK_FOR_NULL("org.netbeans.api.annotations.common.CheckForNull", Nullability.NULLABLE, EnumSet.of(Target.METHOD), EnumSet.of(Scope.METHOD)), + NETBEANS_NULL_ALLOWED("org.netbeans.api.annotations.common.NullAllowed", Nullability.NULLABLE, EnumSet.of(Target.FIELD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.of(Scope.FIELD, Scope.PARAMETER)), + NETBEANS_NON_NULL("org.netbeans.api.annotations.common.NonNull", Nullability.NONNULL, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + NETBEANS_NULL_UNKNOWN("org.netbeans.api.annotations.common.NullUnknown", Nullability.UNKNOWN, EnumSet.of(Target.FIELD, Target.METHOD, Target.PARAMETER, Target.LOCAL_FIELD), EnumSet.allOf(Scope.class)), + + OPEN_REWRITE_NULLABLE("org.openrewrite.internal.lang.Nullable", Nullability.NULLABLE, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.TYPE, Target.TYPE_USE), EnumSet.allOf(Scope.class)), + OPEN_REWRITE_NULL_FIELDS("org.openrewrite.internal.lang.NullFields", Nullability.NULLABLE, EnumSet.of(Target.PACKAGE, Target.TYPE), EnumSet.of(Scope.FIELD)), + OPEN_REWRITE_NON_NULL("org.openrewrite.internal.lang.NonNull", Nullability.NONNULL, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD, Target.TYPE, Target.TYPE_USE), EnumSet.allOf(Scope.class)), + OPEN_REWRITE_NON_NULL_FIELDS("org.openrewrite.internal.lang.NonNullFields", Nullability.NONNULL, EnumSet.of(Target.PACKAGE, Target.TYPE), EnumSet.of(Scope.FIELD)), + OPEN_REWRITE_NON_NULL_API("org.openrewrite.internal.lang.NonNullApi", Nullability.NONNULL, EnumSet.of(Target.PACKAGE, Target.TYPE), EnumSet.of(Scope.METHOD, Scope.PARAMETER)), + + SPRING_NULLABLE("org.springframework.lang.Nullable", Nullability.NULLABLE, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD), EnumSet.allOf(Scope.class)), + SPRING_NON_NULL("org.springframework.lang.NonNull", Nullability.NONNULL, EnumSet.of(Target.METHOD, Target.PARAMETER, Target.FIELD), EnumSet.allOf(Scope.class)), + SPRING_NON_NULL_FIELDS("org.springframework.lang.NonNullFields", Nullability.NONNULL, EnumSet.of(Target.PACKAGE), EnumSet.of(Scope.FIELD)), + SPRING_NON_NULL_API("org.springframework.lang.NonNullApi", Nullability.NONNULL, EnumSet.of(Target.PACKAGE), EnumSet.of(Scope.METHOD, Scope.PARAMETER)) + ; + + private final String fqn; + private final Nullability nullability; + private final Set targets; + private final Set scopes; +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/nullability/NullabilityAnnotation.java b/rewrite-java/src/main/java/org/openrewrite/java/nullability/NullabilityAnnotation.java new file mode 100644 index 00000000000..7a6b85e4b76 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/nullability/NullabilityAnnotation.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import org.apache.commons.lang3.StringUtils; + +import java.lang.annotation.ElementType; +import java.util.Set; + +public interface NullabilityAnnotation { + + enum Nullability { + NULLABLE, + NONNULL, + UNKNOWN; + } + + enum Target { + TYPE, + PACKAGE, + MODULE, + TYPE_USE, + METHOD, + PARAMETER, + FIELD, + LOCAL_FIELD; + } + + enum Scope { + FIELD, + METHOD, + PARAMETER; + } + + /** + * Fully qualified name of this annotation + */ + String getFqn(); + + default String getSimpleName() { + return StringUtils.substringAfterLast(getFqn(), "."); + } + + /** + * Whether this annotation indicates whether an element can be null or not. + */ + Nullability getNullability(); + + /** + * Defines on what elements this nullability annotation ca be used. + * @see ElementType + */ + Set getTargets(); + + /** + * Defines on what elements this nullability annotation applies. + */ + Set getScopes(); +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotations.java b/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotations.java new file mode 100644 index 00000000000..812a466c8e8 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotations.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.cleanup.ShortenFullyQualifiedTypeReferences; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +@Value +@EqualsAndHashCode(callSuper = false) +public class StandardizeNullabilityAnnotations extends Recipe { + + @Option(displayName = "Nullability annotations to use", + description = "All other nullability annotations will be replaced with these.", + example = "javax.annotation.Nullable") + List nullabilityAnnotationsFqn; + + @Option(displayName = "Additional nullability annotations that will be considered known to this recipe", + description = "This option enables the recipe user to migrate to or away from nullability annotations that are not contained within the base set of known nullability annotations.", + required = false) + Set additionalNullabilityAnnotations; + + public StandardizeNullabilityAnnotations(List nullabilityAnnotationsFqn) { + this(nullabilityAnnotationsFqn, new HashSet<>()); + } + + @JsonCreator + public StandardizeNullabilityAnnotations(List nullabilityAnnotationsFqn, @Nullable Set additionalNullabilityAnnotations) { + this.nullabilityAnnotationsFqn = nullabilityAnnotationsFqn; + this.additionalNullabilityAnnotations = additionalNullabilityAnnotations != null ? additionalNullabilityAnnotations : new HashSet<>(); + // During replacement we insert annotation using their fully qualified name + // To cleanup whereever possible afterwards we chain this recipe + doNext(new ShortenFullyQualifiedTypeReferences()); + } + + @Override + public String getDisplayName() { + return "Standardize nullability annotations"; + } + + @Override + public String getDescription() { + return "Define nullable and non-null annotations that are to be used troughout your project. All divergent annotations will be replaced where possible while preserving semantic."; + } + + @Override + public Set getTags() { + return Stream.of("nullability").collect(Collectors.toSet()); + } + + @Override + public Validated validate() { + return super.validate().and(Validated.test("nullableAnnotationsFqn", "must be resolvable as known nullability annotations", this.nullabilityAnnotationsFqn, fqns -> fqns.size() == getNullabilityAnnotations().size())); + } + + @Override + protected TreeVisitor getVisitor() { + return new StandardizeNullabilityAnnotationsVisitor(getKnownNullabilityAnnotations(), getNullabilityAnnotations()); + } + + private List getNullabilityAnnotations() { + return getNullabilityAnnotationsFqn() + .stream() + .flatMap(fqn -> getKnownNullabilityAnnotations().stream().filter(annotation -> Objects.equals(fqn, annotation.getFqn()))) + .collect(Collectors.toList()); + } + + private Set getKnownNullabilityAnnotations() { + return Stream.concat(Arrays.stream(KnownNullabilityAnnotations.values()), additionalNullabilityAnnotations.stream()).collect(Collectors.toSet()); + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsVisitor.java b/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsVisitor.java new file mode 100644 index 00000000000..0f60b828c8e --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/nullability/StandardizeNullabilityAnnotationsVisitor.java @@ -0,0 +1,316 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.nullability; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.TypeUtils; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +class StandardizeNullabilityAnnotationsVisitor extends JavaIsoVisitor { + + private static final String NULLABILITY_ANNOTATION_MARKER = "nullabilityAnnotations"; + + private final Map knownNullabilityDictionary; + + private final List nullabilityAnnotations; + + public StandardizeNullabilityAnnotationsVisitor(Set knownNullabilityAnnotations, List nullabilityAnnotations) { + this.knownNullabilityDictionary = knownNullabilityAnnotations.stream().collect(Collectors.toMap(NullabilityAnnotation::getFqn, Function.identity())); + this.nullabilityAnnotations = nullabilityAnnotations; + } + + + @Override + public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext executionContext) { + J.Annotation jAnnotation = super.visitAnnotation(annotation, executionContext); + JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(jAnnotation.getType()); + if (fullyQualified != null && knownNullabilityDictionary.containsKey(fullyQualified.getFullyQualifiedName())) { + NullabilityAnnotation nullabilityAnnotation = knownNullabilityDictionary.get(fullyQualified.getFullyQualifiedName()); + getCursor().getParentOrThrow() + .computeMessageIfAbsent(NULLABILITY_ANNOTATION_MARKER, k -> new HashSet<>()) + .add(new MatchedNullabilityAnnotation(jAnnotation, nullabilityAnnotation)); + } + return jAnnotation; + } + + @Override + public J.AnnotatedType visitAnnotatedType(J.AnnotatedType t, ExecutionContext executionContext) { + J.AnnotatedType annotatedType = super.visitAnnotatedType(t, executionContext); + Set matchedNullabilityAnnotations = getCursor().pollMessage(NULLABILITY_ANNOTATION_MARKER); + if (matchedNullabilityAnnotations == null || matchedNullabilityAnnotations.isEmpty()) { + return annotatedType; + } + Set targetTypes = new HashSet<>(); + targetTypes.add(NullabilityAnnotation.Target.TYPE_USE); + Cursor parentScopeCursor = getCursor().dropParentUntil(o -> o instanceof J.ClassDeclaration || o instanceof J.MethodDeclaration || Cursor.ROOT_VALUE.equals(o)); + if (Cursor.ROOT_VALUE.equals(parentScopeCursor.getValue())) { + targetTypes.add(NullabilityAnnotation.Target.LOCAL_FIELD); + } else if (parentScopeCursor.getValue() instanceof J.MethodDeclaration) { + targetTypes.add(NullabilityAnnotation.Target.PARAMETER); + } else if (parentScopeCursor.getValue() instanceof J.ClassDeclaration) { + targetTypes.add(NullabilityAnnotation.Target.FIELD); + } + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(matchedNullabilityAnnotations, targetTypes); + if (annotationsForReplacement.isEmpty()) { + return annotatedType; + } + + maybeRemoveMatchedAnnotationImports(matchedNullabilityAnnotations); + + List currentNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getJAnnotation).collect(Collectors.toList()); + List cleanedAnnotations = new LinkedList<>(annotatedType.getAnnotations()); + cleanedAnnotations.removeAll(currentNullabilityAnnotations); + + J.AnnotatedType cleanAnnotatedType = annotatedType.withAnnotations(cleanedAnnotations); + J.AnnotatedType typeWithNewAnnotations = annotationsForReplacement.stream().reduce( + cleanAnnotatedType, + (J.AnnotatedType currentType, NullabilityAnnotation annotation) -> currentType.withTemplate( + annotationTemplate(annotation), + currentType.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)) + ), (oldType, newType) -> newType + ); + Space originalPrefix = annotatedType.getAnnotations().isEmpty() + ? annotatedType.getTypeExpression().getPrefix() + : annotatedType.getAnnotations().get(0).getPrefix(); + return typeWithNewAnnotations.withAnnotations(ListUtils.mapFirst(typeWithNewAnnotations.getAnnotations(), a -> a.withPrefix(originalPrefix))); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + J.ClassDeclaration classDeclaration = super.visitClassDeclaration(classDecl, executionContext); + Set matchedNullabilityAnnotations = getCursor().pollMessage(NULLABILITY_ANNOTATION_MARKER); + if (matchedNullabilityAnnotations == null || matchedNullabilityAnnotations.isEmpty()) { + return classDeclaration; + } + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(matchedNullabilityAnnotations, EnumSet.of(NullabilityAnnotation.Target.TYPE, NullabilityAnnotation.Target.TYPE_USE)); + if (annotationsForReplacement.isEmpty()) { + return classDeclaration; + } + + maybeRemoveMatchedAnnotationImports(matchedNullabilityAnnotations); + + List currentNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getJAnnotation).collect(Collectors.toList()); + List cleanedAnnotations = new LinkedList<>(classDeclaration.getLeadingAnnotations()); + cleanedAnnotations.removeAll(currentNullabilityAnnotations); + + J.ClassDeclaration cleanClassDeclaration = classDeclaration.withLeadingAnnotations(cleanedAnnotations); + return annotationsForReplacement.stream().reduce( + cleanClassDeclaration, + (J.ClassDeclaration currentDeclaration, NullabilityAnnotation annotation) -> currentDeclaration.withTemplate( + annotationTemplate(annotation), + currentDeclaration.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)) + ), (oldDeclaration, newDeclaration) -> newDeclaration + ); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) { + J.MethodDeclaration methodDeclaration = super.visitMethodDeclaration(method, executionContext); + Set matchedNullabilityAnnotations = getCursor().pollMessage(NULLABILITY_ANNOTATION_MARKER); + if (matchedNullabilityAnnotations == null || matchedNullabilityAnnotations.isEmpty()) { + return methodDeclaration; + } + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(matchedNullabilityAnnotations, EnumSet.of(NullabilityAnnotation.Target.METHOD, NullabilityAnnotation.Target.TYPE_USE)); + if (annotationsForReplacement.isEmpty()) { + return methodDeclaration; + } + + maybeRemoveMatchedAnnotationImports(matchedNullabilityAnnotations); + + List currentNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getJAnnotation).collect(Collectors.toList()); + List cleanedAnnotations = new LinkedList<>(methodDeclaration.getLeadingAnnotations()); + cleanedAnnotations.removeAll(currentNullabilityAnnotations); + + J.MethodDeclaration cleanedMethodDeclaration = methodDeclaration.withLeadingAnnotations(cleanedAnnotations); + return annotationsForReplacement.stream().reduce( + cleanedMethodDeclaration, + (J.MethodDeclaration currentDeclaration, NullabilityAnnotation annotation) -> currentDeclaration.withTemplate( + annotationTemplate(annotation), + currentDeclaration.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)) + ), (oldDeclaration, newDeclaration) -> newDeclaration + ); + } + + @Override + public J.Package visitPackage(J.Package pkg, ExecutionContext executionContext) { + J.Package jPackage = super.visitPackage(pkg, executionContext); + Set matchedNullabilityAnnotations = getCursor().pollMessage(NULLABILITY_ANNOTATION_MARKER); + if (matchedNullabilityAnnotations == null || matchedNullabilityAnnotations.isEmpty()) { + return jPackage; + } + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(matchedNullabilityAnnotations, EnumSet.of(NullabilityAnnotation.Target.PACKAGE)); + if (annotationsForReplacement.isEmpty()) { + return jPackage; + } + + maybeRemoveMatchedAnnotationImports(matchedNullabilityAnnotations); + + List currentNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getJAnnotation).collect(Collectors.toList()); + List cleanedAnnotations = new LinkedList<>(jPackage.getAnnotations()); + cleanedAnnotations.removeAll(currentNullabilityAnnotations); + + J.Package cleanedPackage = jPackage.withAnnotations(cleanedAnnotations); + return annotationsForReplacement.stream().reduce( + cleanedPackage, + (J.Package currentDeclaration, NullabilityAnnotation annotation) -> currentDeclaration.withTemplate( + annotationTemplate(annotation), + currentDeclaration.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)) + ), (oldDeclaration, newDeclaration) -> newDeclaration + ); + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) { + J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, executionContext); + Set matchedNullabilityAnnotations = getCursor().pollMessage(NULLABILITY_ANNOTATION_MARKER); + if (matchedNullabilityAnnotations == null || matchedNullabilityAnnotations.isEmpty()) { + return variableDeclarations; + } + Set targetTypes = new HashSet<>(); + targetTypes.add(NullabilityAnnotation.Target.TYPE_USE); + Cursor parentScopeCursor = getCursor().dropParentUntil(o -> o instanceof J.ClassDeclaration || o instanceof J.MethodDeclaration || Cursor.ROOT_VALUE.equals(o)); + if (Cursor.ROOT_VALUE.equals(parentScopeCursor.getValue())) { + targetTypes.add(NullabilityAnnotation.Target.LOCAL_FIELD); + } else if (parentScopeCursor.getValue() instanceof J.MethodDeclaration) { + targetTypes.add(NullabilityAnnotation.Target.PARAMETER); + } else if (parentScopeCursor.getValue() instanceof J.ClassDeclaration) { + targetTypes.add(NullabilityAnnotation.Target.FIELD); + } + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(matchedNullabilityAnnotations, targetTypes); + if (annotationsForReplacement.isEmpty()) { + return variableDeclarations; + } + + maybeRemoveMatchedAnnotationImports(matchedNullabilityAnnotations); + + List currentNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getJAnnotation).collect(Collectors.toList()); + List cleanedAnnotations = new LinkedList<>(variableDeclarations.getLeadingAnnotations()); + cleanedAnnotations.removeAll(currentNullabilityAnnotations); + + J.VariableDeclarations cleanedVariableDeclaration = variableDeclarations.withLeadingAnnotations(cleanedAnnotations); + return annotationsForReplacement.stream().reduce( + cleanedVariableDeclaration, + (J.VariableDeclarations currentDeclaration, NullabilityAnnotation annotation) -> currentDeclaration.withTemplate( + annotationTemplate(annotation), + currentDeclaration.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)) + ), (oldDeclaration, newDeclaration) -> newDeclaration + ); + } + + private LinkedHashSet getAnnotationsForReplacement(Set matchedNullabilityAnnotations, Set possibleTargetTypes) { + Set usedNullabilityAnnotations = matchedNullabilityAnnotations.stream().map(MatchedNullabilityAnnotation::getNullabilityAnnotation).collect(Collectors.toSet()); + Set matchedNullabilities = usedNullabilityAnnotations.stream().map(NullabilityAnnotation::getNullability).collect(Collectors.toSet()); + if (matchedNullabilities.isEmpty()) { + // no nullabilities, nothing to do + return new LinkedHashSet<>(); + } + if (matchedNullabilities.size() > 1) { + // different nullabilities on a single element, we better do nothing + return new LinkedHashSet<>(); + } + + NullabilityAnnotation.Nullability nullability = matchedNullabilities.stream().findFirst().get(); + Set scopesToCover = usedNullabilityAnnotations + .stream() + .map(NullabilityAnnotation::getScopes) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + if (scopesToCover.isEmpty()) { + return new LinkedHashSet<>(); + } + + LinkedHashSet annotationsForReplacement = getAnnotationsForReplacement(nullability, possibleTargetTypes, scopesToCover); + if (annotationsForReplacement.isEmpty()) { + // We were not able to find a good replacement + return new LinkedHashSet<>(); + } + if (Objects.equals(usedNullabilityAnnotations, annotationsForReplacement)) { + // Our replacement would not change used annotations. + // Only the order may have changed as we collect matched annotation in a set. + // Anyway, no semantic change -> we return an empty list. + return new LinkedHashSet<>(); + } + return annotationsForReplacement; + } + + private LinkedHashSet getAnnotationsForReplacement(NullabilityAnnotation.Nullability nullability, Set targetTypes, Set scopesToCover) { + List usableNullabilityAnnotations = getNullabilityAnnotationsForTargets(nullability, targetTypes); + Optional singleAnnotation = usableNullabilityAnnotations + .stream() + .filter(a -> Objects.equals(scopesToCover, a.getScopes())) + .findFirst(); + + if (singleAnnotation.isPresent()) { + LinkedHashSet result = new LinkedHashSet<>(); + result.add(singleAnnotation.get()); + return result; + } + + Set coveredScopes = new HashSet<>(); + LinkedHashSet usedAnnotations = new LinkedHashSet<>(); + for (NullabilityAnnotation possibleAnnotation : usableNullabilityAnnotations) { + if (Objects.equals(scopesToCover, coveredScopes)) { + break; + } + coveredScopes.addAll(possibleAnnotation.getScopes()); + usedAnnotations.add(possibleAnnotation); + } + return Objects.equals(scopesToCover, coveredScopes) + ? usedAnnotations + : new LinkedHashSet<>(); + } + + private List getNullabilityAnnotationsForTargets(NullabilityAnnotation.Nullability nullability, Set targets) { + return nullabilityAnnotations + .stream() + .filter(a -> a.getNullability() == nullability) + .filter(a -> a.getTargets().stream().anyMatch(targets::contains)) + .collect(Collectors.toList()); + } + + private void maybeRemoveMatchedAnnotationImports(Collection matchedAnnotations) { + matchedAnnotations.forEach(a -> maybeRemoveImport(a.getNullabilityAnnotation().getFqn())); + } + + private JavaTemplate annotationTemplate(NullabilityAnnotation annotation) { + return JavaTemplate.builder(this::getCursor, "@" + annotation.getFqn()) + .imports(annotation.getFqn()) + .javaParser(JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath())) + .build(); + } + + @Value + @AllArgsConstructor + private static class MatchedNullabilityAnnotation { + + J.Annotation jAnnotation; + + NullabilityAnnotation nullabilityAnnotation; + } +} diff --git a/rewrite-java/src/main/java/org/openrewrite/java/nullability/package-info.java b/rewrite-java/src/main/java/org/openrewrite/java/nullability/package-info.java new file mode 100644 index 00000000000..cb7885ea253 --- /dev/null +++ b/rewrite-java/src/main/java/org/openrewrite/java/nullability/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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. + */ +@NonNullApi +@NonNullFields +package org.openrewrite.java.nullability; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; \ No newline at end of file diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/CoordinateBuilder.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/CoordinateBuilder.java index b8edc7ae5fd..0d88648c9f2 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/CoordinateBuilder.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/CoordinateBuilder.java @@ -55,6 +55,7 @@ public JavaCoordinates before() { return before(Space.Location.STATEMENT_PREFIX); } + @Override public JavaCoordinates replace() { return replace(Space.Location.STATEMENT_PREFIX); } @@ -77,11 +78,23 @@ public JavaCoordinates replace() { } } + public static class AnnotatedType extends Expression { + + AnnotatedType(J.AnnotatedType tree) { + super(tree); + } + + public JavaCoordinates addAnnotation(Comparator idealOrdering) { + return new JavaCoordinates(tree, Space.Location.ANNOTATIONS, JavaCoordinates.Mode.BEFORE, idealOrdering); + } + } + public static class Annotation extends Expression { Annotation(J.Annotation tree) { super(tree); } + @Override public JavaCoordinates replace() { return replace(Space.Location.ANNOTATION_PREFIX); } @@ -174,6 +187,7 @@ public Identifier(J.Identifier tree) { super(tree); } + @Override public JavaCoordinates replace() { return replace(Space.Location.IDENTIFIER_PREFIX); } @@ -276,6 +290,11 @@ public static class Package extends Statement { super(tree); } + public JavaCoordinates addAnnotation(Comparator idealOrdering) { + return new JavaCoordinates(tree, Space.Location.ANNOTATIONS, JavaCoordinates.Mode.BEFORE, idealOrdering); + } + + @Override public JavaCoordinates replace() { return replace(Space.Location.PACKAGE_PREFIX); } @@ -300,6 +319,7 @@ public static class Yield extends Statement { super(tree); } + @Override public JavaCoordinates replace() { return replace(Space.Location.YIELD_PREFIX); } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java index c3d4351737b..87e85ac7bf1 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/J.java @@ -156,8 +156,8 @@ public String toString() { @Override @Transient - public CoordinateBuilder.Expression getCoordinates() { - return new CoordinateBuilder.Expression(this); + public CoordinateBuilder.AnnotatedType getCoordinates() { + return new CoordinateBuilder.AnnotatedType(this); } } diff --git a/rewrite-java/src/main/resources/META-INF/rewrite/nullability.yml b/rewrite-java/src/main/resources/META-INF/rewrite/nullability.yml new file mode 100644 index 00000000000..f7d4453a06e --- /dev/null +++ b/rewrite-java/src/main/resources/META-INF/rewrite/nullability.yml @@ -0,0 +1,68 @@ +# +# Copyright 2023 the original author or authors. +#

+# Licensed 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 +#

+# https://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. +# +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.nullability.UseJavaxNullabilityAnnotations +displayName: Use javax nullability annotations +description: Replaces other known nullability annotations with Javax annotations where possible. +tags: + - nullability +recipeList: + - org.openrewrite.java.nullability.StandardizeNullabilityAnnotations: + nullabilityAnnotationsFqn: + - javax.annotation.Nullable + - javax.annotation.Nonnull +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.nullability.UseJakartaNullabilityAnnotations +displayName: Use Jakarta nullability annotations +description: Replaces other known nullability annotations with Jakarta annotations where possible. +tags: + - nullability +recipeList: + - org.openrewrite.java.nullability.StandardizeNullabilityAnnotations: + nullabilityAnnotationsFqn: + - jakarta.annotation.Nullable + - jakarta.annotation.Nonnull +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.nullability.UseSpringNullabilityAnnotations +displayName: Use spring nullability annotations +description: Replaces other known nullability annotations with Spring annotations where possible. +tags: + - nullability +recipeList: + - org.openrewrite.java.nullability.StandardizeNullabilityAnnotations: + nullabilityAnnotationsFqn: + - org.springframework.lang.Nullable + - org.springframework.lang.NonNull + - org.springframework.lang.NonNullFields + - org.springframework.lang.NonNullApi +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.openrewrite.java.nullability.UseOpenRewriteNullabilityAnnotations +displayName: Use OpenRewrite nullability annotations +description: Replaces other known nullability annotations with OpenRewrite annotations where possible. +tags: + - nullability +recipeList: + - org.openrewrite.java.nullability.StandardizeNullabilityAnnotations: + nullabilityAnnotationsFqn: + - org.openrewrite.internal.lang.Nullable + - org.openrewrite.internal.lang.NullFields + - org.openrewrite.internal.lang.NonNull + - org.openrewrite.internal.lang.NonNullFields + - org.openrewrite.internal.lang.NonNullApi