From 44cc0c85e45339f7e4944a828e2a08b67f749550 Mon Sep 17 00:00:00 2001 From: martinfrancois Date: Mon, 10 Aug 2026 13:25:10 +0200 Subject: [PATCH 1/3] Add JSpecify for Micrometer projects and keep it for Spring projects MigrateFromMicrometerAnnotations rewrites io.micrometer.core.lang nullness annotations to org.jspecify.annotations, but guarded its AddDependency step on org.springframework.lang.*ull*. A project that used only Micrometer had its source rewritten and never received org.jspecify:jspecify, so the output did not compile. Point the guard at io.micrometer.core.lang.*ull*, the types the recipe migrates. That guard was also the only path adding JSpecify for Spring projects. MigrateFromSpringFrameworkAnnotations is deliberately disabled in MigrateToJSpecify, yet the static analysis recipes in JSpecifyBestPractices still insert org.jspecify.annotations.* into Spring sources. JSpecifyBestPractices therefore gets its own AddDependency step guarded on org.springframework.lang.*ull*. That guard has to match the pre-migration Spring annotations, because AddDependency does not see annotations other recipes insert during the same run. JSpecifyBestPracticesTest covers the Micrometer dependency, an existing JSpecify dependency, two negative controls, and the Spring-only composite case. The shared parser classpath in that class now includes micrometer-core. --- build.gradle.kts | 1 + .../resources/META-INF/rewrite/jspecify.yml | 11 +- .../jspecify/JSpecifyBestPracticesTest.java | 370 ++++++++++++++++++ 3 files changed, 381 insertions(+), 1 deletion(-) 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..117a5fea1f 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -25,6 +25,15 @@ 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 + onlyIfUsing: org.springframework.lang.*ull* + acceptTransitive: true - org.openrewrite.staticanalysis.AnnotateNullableMethods - org.openrewrite.staticanalysis.AnnotateNullableParameters - org.openrewrite.staticanalysis.AnnotateRequiredParameters @@ -149,7 +158,7 @@ recipeList: groupId: org.jspecify artifactId: jspecify version: 1.0.0 - onlyIfUsing: org.springframework.lang.*ull* + 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..614e1b1f87 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,373 @@ 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() { + // The Spring migration leaf is deliberately disabled in MigrateToJSpecify, but the static + // analysis recipes in JSpecifyBestPractices still introduce `org.jspecify.annotations.*`, + // so a Spring-only project must still get the JSpecify 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( From 62ce2aefea3c2a4e85bd25d6a22f25ab0e960eff Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 12 Aug 2026 00:31:23 +0200 Subject: [PATCH 2/3] Trim commentary --- .../java/migrate/jspecify/JSpecifyBestPracticesTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 614e1b1f87..3032623c9a 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java @@ -789,9 +789,8 @@ public class Test { @Test void addJspecifyDependencyOnSpringOnlyProject() { - // The Spring migration leaf is deliberately disabled in MigrateToJSpecify, but the static - // analysis recipes in JSpecifyBestPractices still introduce `org.jspecify.annotations.*`, - // so a Spring-only project must still get the JSpecify dependency. + // 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 From 4fb819edb601fb661e958578b57fd148c60b6082 Mon Sep 17 00:00:00 2001 From: martinfrancois Date: Sun, 16 Aug 2026 23:43:51 +0200 Subject: [PATCH 3/3] Document what the *ull* glob matches --- src/main/resources/META-INF/rewrite/jspecify.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index 117a5fea1f..76cd78d7ba 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -32,6 +32,7 @@ recipeList: 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 @@ -158,6 +159,7 @@ recipeList: groupId: org.jspecify artifactId: jspecify version: 1.0.0 + # *ull* matches Nullable, NonNull, NonNullApi and NonNullFields onlyIfUsing: io.micrometer.core.lang.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: