diff --git a/build.gradle.kts b/build.gradle.kts index fb0e89dd79..76db8c0a79 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,6 +97,7 @@ dependencies { testRuntimeOnly("org.apache.groovy:groovy:4.+") testRuntimeOnly("org.jboss.logging:jboss-logging:3.6.0.Final") testRuntimeOnly("jakarta.annotation:jakarta.annotation-api:2.1.1") + testRuntimeOnly("io.micrometer:micrometer-core:1.15.1") testRuntimeOnly("org.springframework:spring-core:6.1.13") testRuntimeOnly("io.micronaut:micronaut-core:4.10.8") testRuntimeOnly("com.google.code.findbugs:jsr305:3.0.2") diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index 8a8d807ec3..76cd78d7ba 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -25,6 +25,16 @@ preconditions: - org.openrewrite.Singleton recipeList: - org.openrewrite.java.jspecify.MigrateToJSpecify +# The static analysis recipes below introduce `org.jspecify.annotations.*`, and the Spring +# migration leaf is deliberately disabled in MigrateToJSpecify, so Spring projects still need +# the JSpecify dependency. +- org.openrewrite.java.dependencies.AddDependency: + groupId: org.jspecify + artifactId: jspecify + version: 1.0.0 + # *ull* matches Nullable, NonNull, NonNullApi and NonNullFields + onlyIfUsing: org.springframework.lang.*ull* + acceptTransitive: true - org.openrewrite.staticanalysis.AnnotateNullableMethods - org.openrewrite.staticanalysis.AnnotateNullableParameters - org.openrewrite.staticanalysis.AnnotateRequiredParameters @@ -149,7 +159,8 @@ recipeList: groupId: org.jspecify artifactId: jspecify version: 1.0.0 - onlyIfUsing: org.springframework.lang.*ull* + # *ull* matches Nullable, NonNull, NonNullApi and NonNullFields + onlyIfUsing: io.micrometer.core.lang.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: annotationType: io.micrometer.core.lang.*ull* diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java index 207f2bdd7d..3032623c9a 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java @@ -23,6 +23,8 @@ import org.openrewrite.test.RewriteTest; import static org.openrewrite.java.Assertions.*; +import static org.openrewrite.gradle.Assertions.buildGradle; +import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; import static org.openrewrite.maven.Assertions.pomXml; @SuppressWarnings("NotNullFieldNotInitialized") @@ -38,6 +40,7 @@ public void defaults(RecipeSpec spec) { "jakarta.annotation-api", "annotations", "spring-core", + "micrometer-core", "micronaut-core")); } @@ -501,6 +504,372 @@ class Bar { ); } + @Test + void migrateFromMicrometerAnnotationsToJspecify() { + rewriteRun( + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + import io.micrometer.core.lang.NonNull; + import io.micrometer.core.lang.Nullable; + + public class Test { + @NonNull + public String field1; + @Nullable + public String field2; + @Nullable + public Foo.Bar foobar; + } + + interface Foo { + class Bar { + @NonNull + public String barField; + } + } + """, + """ + import org.jspecify.annotations.NonNull; + import org.jspecify.annotations.Nullable; + + public class Test { + @NonNull + public String field1; + @Nullable + public String field2; + public Foo.@Nullable Bar foobar; + } + + interface Foo { + class Bar { + @NonNull + public String barField; + } + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + io.micrometer + micrometer-core + 1.15.1 + + + + """, + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + io.micrometer + micrometer-core + 1.15.1 + + + org.jspecify + jspecify + 1.0.0 + + + + """ + ) + ) + ); + } + + @Test + void migrateFromMicrometerAnnotationsWithExistingJspecifyDependency() { + rewriteRun( + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + import io.micrometer.core.lang.Nullable; + + public class Test { + @Nullable + public String field1; + } + """, + """ + import org.jspecify.annotations.Nullable; + + public class Test { + @Nullable + public String field1; + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + io.micrometer + micrometer-core + 1.15.1 + + + org.jspecify + jspecify + 1.0.0 + + + + """ + ) + ) + ); + } + + @Test + void micrometerProjectBuiltWithGradleAlsoGetsTheJspecifyDependency() { + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()), + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + import io.micrometer.core.lang.Nullable; + + public class Test { + @Nullable + public String field; + } + """, + """ + import org.jspecify.annotations.Nullable; + + public class Test { + @Nullable + public String field; + } + """ + ) + ), + //language=groovy + buildGradle( + """ + plugins { + id "java-library" + } + + repositories { + mavenCentral() + } + + dependencies { + implementation "io.micrometer:micrometer-core:1.15.1" + } + """, + """ + plugins { + id "java-library" + } + + repositories { + mavenCentral() + } + + dependencies { + implementation "io.micrometer:micrometer-core:1.15.1" + implementation "org.jspecify:jspecify:1.0.0" + } + """ + ) + ) + ); + } + + @Test + void micrometerRecipeDoesNotActivateOnSpringAnnotations() { + rewriteRun( + spec -> spec.recipeFromResource( + "/META-INF/rewrite/jspecify.yml", + "org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations"), + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + import org.springframework.lang.Nullable; + + public class Test { + @Nullable + public String field1; + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + org.springframework + spring-core + 6.1.13 + + + + """ + ) + ) + ); + } + + @Test + void micrometerRecipeDoesNotActivateWithoutNullnessAnnotations() { + rewriteRun( + spec -> spec.recipeFromResource( + "/META-INF/rewrite/jspecify.yml", + "org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations"), + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + public class Test { + public String field1; + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + io.micrometer + micrometer-core + 1.15.1 + + + + """ + ) + ) + ); + } + + @Test + void addJspecifyDependencyOnSpringOnlyProject() { + // MigrateToJSpecify disables the Spring leaf, but JSpecifyBestPractices still introduces + // `org.jspecify.annotations.*`, so a Spring-only project still needs the dependency + rewriteRun( + mavenProject("foo", + //language=java + srcMainJava( + java( + """ + import org.springframework.lang.Nullable; + + public class Test { + @Nullable + public String field1; + + public String maybe() { + if (field1 != null) { + return field1; + } + return null; + } + } + """, + """ + import org.springframework.lang.Nullable; + + public class Test { + @Nullable + public String field1; + + public @org.jspecify.annotations.Nullable String maybe() { + if (field1 != null) { + return field1; + } + return null; + } + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + org.springframework + spring-core + 6.1.13 + + + + """, + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + org.jspecify + jspecify + 1.0.0 + + + org.springframework + spring-core + 6.1.13 + + + + """ + ) + ) + ); + } + @Test void migrateFromMicronautAnnotationToJspecify() { rewriteRun(