From 5420515894f140ba7c2f3656587ffa97c899d156 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:28:24 +0200 Subject: [PATCH 01/12] Move type-use annotations to array brackets during JSpecify migration (#934) --- .../jspecify/MoveAnnotationToArrayType.java | 122 ++++++++++++ .../resources/META-INF/rewrite/jspecify.yml | 13 ++ .../jspecify/JSpecifyBestPracticesTest.java | 77 ++++++++ .../MoveAnnotationToArrayTypeTest.java | 178 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java create mode 100644 src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java new file mode 100644 index 0000000000..a8e2e682c8 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024 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.jspecify; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.TypeMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.*; + +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Collections.singletonList; + +@EqualsAndHashCode(callSuper = false) +@Value +public class MoveAnnotationToArrayType extends Recipe { + + @Option(displayName = "Annotation type", + description = "The type of annotation to move to the array type.", + example = "org.jspecify.annotations.*", + required = false) + @org.jspecify.annotations.Nullable + String annotationType; + + String displayName = "Move annotation to array type"; + + String description = "When a type-use annotation like `@Nullable` is applied to an array type, " + + "it should be placed on the array brackets rather than before the element type. " + + "For example, `@Nullable byte[]` becomes `byte @Nullable[]`."; + + @Override + public TreeVisitor getVisitor() { + String pattern = annotationType == null ? "org.jspecify.annotations.*" : annotationType; + + return Preconditions.check(new UsesType<>(pattern, null), new JavaIsoVisitor() { + final TypeMatcher typeMatcher = new TypeMatcher(pattern); + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); + + if (!(md.getReturnTypeExpression() instanceof J.ArrayType)) { + return md; + } + + AtomicReference matched = new AtomicReference<>(); + md = md.withLeadingAnnotations(ListUtils.map(md.getLeadingAnnotations(), a -> { + if (matched.get() == null && matchesType(a)) { + matched.set(a); + return null; + } + return a; + })); + + if (matched.get() != null) { + J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); + arrayType = arrayType.withAnnotations( + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + md = md.withReturnTypeExpression(arrayType); + if (md.getLeadingAnnotations().isEmpty()) { + md = md.withReturnTypeExpression(arrayType.withPrefix( + arrayType.getPrefix().withWhitespace(""))); + } + md = autoFormat(md, arrayType, ctx, getCursor().getParentOrThrow()); + } + return md; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations mv = super.visitVariableDeclarations(multiVariable, ctx); + + if (!(mv.getTypeExpression() instanceof J.ArrayType)) { + return mv; + } + + AtomicReference matched = new AtomicReference<>(); + mv = mv.withLeadingAnnotations(ListUtils.map(mv.getLeadingAnnotations(), a -> { + if (matched.get() == null && matchesType(a)) { + matched.set(a); + return null; + } + return a; + })); + + if (matched.get() != null) { + J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); + arrayType = arrayType.withAnnotations( + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + if (mv.getLeadingAnnotations().isEmpty()) { + arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); + } + mv = mv.withTypeExpression(arrayType); + mv = autoFormat(mv, arrayType, ctx, getCursor().getParentOrThrow()); + } + return mv; + } + + private boolean matchesType(J.Annotation ann) { + JavaType.FullyQualified fq = TypeUtils.asFullyQualified(ann.getType()); + return fq != null && typeMatcher.matches(fq); + } + }); + } +} diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index c3b8a50e05..0270a2556c 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -79,6 +79,8 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromJakartaAnnotationApi @@ -103,6 +105,8 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromJetbrainsAnnotations @@ -127,6 +131,8 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations @@ -151,6 +157,8 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromSpringFrameworkAnnotations @@ -175,9 +183,12 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromMicronautAnnotations + displayName: Migrate from Micronaut Framework annotations to JSpecify description: Migrate from Micronaut Framework annotations to JSpecify. preconditions: @@ -199,3 +210,5 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jspecify.annotations.* 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 4a80f44ec0..0d3a1cedf5 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java @@ -35,6 +35,83 @@ public void defaults(RecipeSpec spec) { .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jakarta.annotation-api", "annotations", "spring-core", "micronaut-core")); } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") + @Test + void jspecifyArrayNullable() { + rewriteRun( + mavenProject("foo", + srcMainJava( + //language=java + java( + """ + import javax.annotation.Nullable; + + class Foo { + @Nullable + public byte[] bar() { + return null; + } + + public void baz(@Nullable byte[] a) { + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public byte @Nullable[] bar() { + return null; + } + + public void baz(byte @Nullable[] a) { + } + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + """, + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + org.jspecify + jspecify + 1.0.0 + + + + """ + ) + ) + ); + } + @DocumentExample @Test void migrateFromJavaxAnnotationApiToJspecify() { diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java new file mode 100644 index 0000000000..882c1ced5c --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024 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.jspecify; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Issue; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") +class MoveAnnotationToArrayTypeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .recipe(new MoveAnnotationToArrayType("org.jspecify.annotations.*")) + .parser(JavaParser.fromJavaVersion().classpath("jspecify")); + } + + @DocumentExample + @Test + void moveNullableToArrayReturnType() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + @Nullable + public byte[] bar() { + return null; + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public byte @Nullable[] bar() { + return null; + } + } + """ + ) + ); + } + + @Test + void moveNullableToArrayParameter() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public void baz(@Nullable byte[] a) { + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public void baz(byte @Nullable[] a) { + } + } + """ + ) + ); + } + + @Test + void moveNullableToArrayField() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + @Nullable + public byte[] data; + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public byte @Nullable[] data; + } + """ + ) + ); + } + + @Test + void noChangeForNonArrayReturnType() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + @Nullable + public String bar() { + return null; + } + } + """ + ) + ); + } + + @Test + void noChangeForNonArrayParameter() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public void baz(@Nullable String a) { + } + } + """ + ) + ); + } + + @Test + void multiDimensionalArray() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + @Nullable + public String[][] bar() { + return null; + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Foo { + public String @Nullable[][] bar() { + return null; + } + } + """ + ) + ); + } +} From 277ff50fa6ad4069247954fb1355e4e96f282f7e Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:46:13 +0200 Subject: [PATCH 02/12] Run MoveAnnotationToArrayType before ChangeType to preserve semantics Move the recipe before ChangeType in each migration pipeline so it matches on the old annotation type (e.g. javax.annotation.*). This avoids incorrectly moving pre-existing JSpecify annotations where @Nullable String[] intentionally means "array of nullable Strings." --- .../jspecify/MoveAnnotationToArrayType.java | 21 +++--- .../resources/META-INF/rewrite/jspecify.yml | 25 ++++--- .../MoveAnnotationToArrayTypeTest.java | 71 +++++++++++++++---- 3 files changed, 81 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index a8e2e682c8..2290df4df3 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -33,24 +33,23 @@ public class MoveAnnotationToArrayType extends Recipe { @Option(displayName = "Annotation type", - description = "The type of annotation to move to the array type.", - example = "org.jspecify.annotations.*", - required = false) - @org.jspecify.annotations.Nullable + description = "The type of annotation to move to the array type. " + + "Should target the pre-migration annotation type to avoid changing the semantics " + + "of pre-existing type-use annotations on object arrays.", + example = "javax.annotation.*") String annotationType; String displayName = "Move annotation to array type"; - String description = "When a type-use annotation like `@Nullable` is applied to an array type, " + - "it should be placed on the array brackets rather than before the element type. " + - "For example, `@Nullable byte[]` becomes `byte @Nullable[]`."; + String description = "When an annotation like `@Nullable` is applied to an array type in declaration position, " + + "this recipe moves it to the array brackets. " + + "For example, `@Nullable byte[]` becomes `byte @Nullable[]`. " + + "Best used before `ChangeType` in a migration pipeline, targeting the pre-migration annotation type."; @Override public TreeVisitor getVisitor() { - String pattern = annotationType == null ? "org.jspecify.annotations.*" : annotationType; - - return Preconditions.check(new UsesType<>(pattern, null), new JavaIsoVisitor() { - final TypeMatcher typeMatcher = new TypeMatcher(pattern); + return Preconditions.check(new UsesType<>(annotationType, null), new JavaIsoVisitor() { + final TypeMatcher typeMatcher = new TypeMatcher(annotationType); @Override public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index 0270a2556c..e499835b14 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -65,6 +65,8 @@ recipeList: version: latest.release onlyIfUsing: javax.annotation.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: javax.annotation.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: javax.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -79,8 +81,6 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromJakartaAnnotationApi @@ -95,6 +95,8 @@ recipeList: version: 1.0.0 onlyIfUsing: jakarta.annotation.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: jakarta.annotation.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: jakarta.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -105,8 +107,6 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromJetbrainsAnnotations @@ -121,6 +121,8 @@ recipeList: version: 1.0.0 onlyIfUsing: org.jetbrains.annotations.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.jetbrains.annotations.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.jetbrains.annotations.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -131,8 +133,6 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations @@ -147,6 +147,8 @@ recipeList: version: 1.0.0 onlyIfUsing: org.springframework.lang.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: io.micrometer.core.lang.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.micrometer.core.lang.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -157,8 +159,6 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromSpringFrameworkAnnotations @@ -173,6 +173,8 @@ recipeList: version: 1.0.0 onlyIfUsing: org.springframework.lang.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: org.springframework.lang.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.springframework.lang.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -183,12 +185,9 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.java.jspecify.MigrateFromMicronautAnnotations - displayName: Migrate from Micronaut Framework annotations to JSpecify description: Migrate from Micronaut Framework annotations to JSpecify. preconditions: @@ -200,6 +199,8 @@ recipeList: version: 1.0.0 onlyIfUsing: io.micronaut.core.annotation.*ull* acceptTransitive: true + - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: + annotationType: io.micronaut.core.annotation.* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.micronaut.core.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -210,5 +211,3 @@ recipeList: ignoreDefinition: true - org.openrewrite.staticanalysis.java.MoveFieldAnnotationToType: annotationType: org.jspecify.annotations.* - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jspecify.annotations.* diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java index 882c1ced5c..36bfbee53f 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java @@ -30,8 +30,8 @@ class MoveAnnotationToArrayTypeTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { spec - .recipe(new MoveAnnotationToArrayType("org.jspecify.annotations.*")) - .parser(JavaParser.fromJavaVersion().classpath("jspecify")); + .recipe(new MoveAnnotationToArrayType("javax.annotation.*")) + .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jspecify")); } @DocumentExample @@ -41,7 +41,7 @@ void moveNullableToArrayReturnType() { //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { @Nullable @@ -51,7 +51,7 @@ public byte[] bar() { } """, """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public byte @Nullable[] bar() { @@ -69,7 +69,7 @@ void moveNullableToArrayParameter() { //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public void baz(@Nullable byte[] a) { @@ -77,7 +77,7 @@ public void baz(@Nullable byte[] a) { } """, """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public void baz(byte @Nullable[] a) { @@ -94,7 +94,7 @@ void moveNullableToArrayField() { //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { @Nullable @@ -102,7 +102,7 @@ class Foo { } """, """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public byte @Nullable[] data; @@ -112,13 +112,41 @@ class Foo { ); } + @Test + void moveNullableToObjectArrayReturnType() { + rewriteRun( + //language=java + java( + """ + import javax.annotation.Nullable; + + class Foo { + @Nullable + public String[] bar() { + return null; + } + } + """, + """ + import javax.annotation.Nullable; + + class Foo { + public String @Nullable[] bar() { + return null; + } + } + """ + ) + ); + } + @Test void noChangeForNonArrayReturnType() { rewriteRun( //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { @Nullable @@ -137,7 +165,7 @@ void noChangeForNonArrayParameter() { //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public void baz(@Nullable String a) { @@ -154,7 +182,7 @@ void multiDimensionalArray() { //language=java java( """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { @Nullable @@ -164,7 +192,7 @@ public String[][] bar() { } """, """ - import org.jspecify.annotations.Nullable; + import javax.annotation.Nullable; class Foo { public String @Nullable[][] bar() { @@ -175,4 +203,23 @@ class Foo { ) ); } + + @Test + void noChangeForPreExistingJSpecifyAnnotation() { + rewriteRun( + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Foo { + @Nullable + public String[] bar() { + return null; + } + } + """ + ) + ); + } } From 504b4d0e8edfd7a43d79579aad11de4094fea859 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:49:07 +0200 Subject: [PATCH 03/12] Add test: pre-existing JSpecify array-of-nullable-elements is not affected Verifies that a project with both javax annotations (to migrate) and pre-existing JSpecify @Nullable String[] (meaning array of nullable elements) only migrates the javax annotations without touching the already-correct JSpecify annotations. --- .../jspecify/JSpecifyBestPracticesTest.java | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) 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 0d3a1cedf5..29bba08be6 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java @@ -32,7 +32,7 @@ class JSpecifyBestPracticesTest implements RewriteTest { public void defaults(RecipeSpec spec) { spec .recipeFromResource("/META-INF/rewrite/jspecify.yml", "org.openrewrite.java.jspecify.JSpecifyBestPractices") - .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jakarta.annotation-api", "annotations", "spring-core", "micronaut-core")); + .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jspecify", "jakarta.annotation-api", "annotations", "spring-core", "micronaut-core")); } @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") @@ -112,6 +112,79 @@ public void baz(byte @Nullable[] a) { ); } + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") + @Test + void existingJSpecifyArrayOfNullableElementsNotAffected() { + rewriteRun( + mavenProject("foo", + srcMainJava( + //language=java + java( + """ + import javax.annotation.Nullable; + + class Legacy { + @Nullable + public String[] bar() { + return null; + } + } + """, + """ + import org.jspecify.annotations.Nullable; + + class Legacy { + public String @Nullable[] bar() { + return null; + } + } + """ + ), + //language=java + java( + """ + import org.jspecify.annotations.Nullable; + + class Modern { + @Nullable String[] arrayOfNullableStrings; + + public @Nullable String[] bar() { + return arrayOfNullableStrings; + } + + public void baz(@Nullable String[] a) { + } + } + """ + ) + ), + //language=xml + pomXml( + """ + + 4.0.0 + com.example.foobar + foobar-core + 1.0.0 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + org.jspecify + jspecify + 1.0.0 + + + + """ + ) + ) + ); + } + @DocumentExample @Test void migrateFromJavaxAnnotationApiToJspecify() { From ed23fc5e3af6c0848b4dac3ced91ebfc3b8cc26d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:52:19 +0200 Subject: [PATCH 04/12] Fix copyright year, use import, and early returns in MoveAnnotationToArrayType --- .../jspecify/MoveAnnotationToArrayType.java | 52 ++++++++++--------- .../MoveAnnotationToArrayTypeTest.java | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index 2290df4df3..2d97fd04f1 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2026 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import org.openrewrite.java.search.UsesType; import org.openrewrite.java.tree.*; +import org.jspecify.annotations.Nullable; + import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.singletonList; @@ -59,7 +61,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - AtomicReference matched = new AtomicReference<>(); + AtomicReference matched = new AtomicReference<>(); md = md.withLeadingAnnotations(ListUtils.map(md.getLeadingAnnotations(), a -> { if (matched.get() == null && matchesType(a)) { matched.set(a); @@ -68,18 +70,19 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return a; })); - if (matched.get() != null) { - J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); - arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); - md = md.withReturnTypeExpression(arrayType); - if (md.getLeadingAnnotations().isEmpty()) { - md = md.withReturnTypeExpression(arrayType.withPrefix( - arrayType.getPrefix().withWhitespace(""))); - } - md = autoFormat(md, arrayType, ctx, getCursor().getParentOrThrow()); + if (matched.get() == null) { + return md; + } + + J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); + arrayType = arrayType.withAnnotations( + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + md = md.withReturnTypeExpression(arrayType); + if (md.getLeadingAnnotations().isEmpty()) { + md = md.withReturnTypeExpression(arrayType.withPrefix( + arrayType.getPrefix().withWhitespace(""))); } - return md; + return autoFormat(md, arrayType, ctx, getCursor().getParentOrThrow()); } @Override @@ -90,7 +93,7 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return mv; } - AtomicReference matched = new AtomicReference<>(); + AtomicReference matched = new AtomicReference<>(); mv = mv.withLeadingAnnotations(ListUtils.map(mv.getLeadingAnnotations(), a -> { if (matched.get() == null && matchesType(a)) { matched.set(a); @@ -99,17 +102,18 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return a; })); - if (matched.get() != null) { - J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); - arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); - if (mv.getLeadingAnnotations().isEmpty()) { - arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); - } - mv = mv.withTypeExpression(arrayType); - mv = autoFormat(mv, arrayType, ctx, getCursor().getParentOrThrow()); + if (matched.get() == null) { + return mv; + } + + J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); + arrayType = arrayType.withAnnotations( + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + if (mv.getLeadingAnnotations().isEmpty()) { + arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); } - return mv; + mv = mv.withTypeExpression(arrayType); + return autoFormat(mv, arrayType, ctx, getCursor().getParentOrThrow()); } private boolean matchesType(J.Annotation ann) { diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java index 36bfbee53f..a795e4f3bb 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2026 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. From 6f7b27cc318be5fbb9003becd32a225bcdb51715 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:55:04 +0200 Subject: [PATCH 05/12] Narrow annotation type patterns to *ull* to match only nullability annotations --- .../migrate/jspecify/MoveAnnotationToArrayType.java | 2 +- src/main/resources/META-INF/rewrite/jspecify.yml | 12 ++++++------ .../jspecify/MoveAnnotationToArrayTypeTest.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index 2d97fd04f1..e43664aeff 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -38,7 +38,7 @@ public class MoveAnnotationToArrayType extends Recipe { description = "The type of annotation to move to the array type. " + "Should target the pre-migration annotation type to avoid changing the semantics " + "of pre-existing type-use annotations on object arrays.", - example = "javax.annotation.*") + example = "javax.annotation.*ull*") String annotationType; String displayName = "Move annotation to array type"; diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index e499835b14..7470ab9518 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -66,7 +66,7 @@ recipeList: onlyIfUsing: javax.annotation.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: javax.annotation.* + annotationType: javax.annotation.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: javax.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -96,7 +96,7 @@ recipeList: onlyIfUsing: jakarta.annotation.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: jakarta.annotation.* + annotationType: jakarta.annotation.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: jakarta.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -122,7 +122,7 @@ recipeList: onlyIfUsing: org.jetbrains.annotations.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jetbrains.annotations.* + annotationType: org.jetbrains.annotations.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.jetbrains.annotations.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -148,7 +148,7 @@ recipeList: onlyIfUsing: org.springframework.lang.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: io.micrometer.core.lang.* + annotationType: io.micrometer.core.lang.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.micrometer.core.lang.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -174,7 +174,7 @@ recipeList: onlyIfUsing: org.springframework.lang.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.springframework.lang.* + annotationType: org.springframework.lang.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.springframework.lang.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -200,7 +200,7 @@ recipeList: onlyIfUsing: io.micronaut.core.annotation.*ull* acceptTransitive: true - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: io.micronaut.core.annotation.* + annotationType: io.micronaut.core.annotation.*ull* - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.micronaut.core.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java index a795e4f3bb..ea555d561a 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java @@ -30,7 +30,7 @@ class MoveAnnotationToArrayTypeTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { spec - .recipe(new MoveAnnotationToArrayType("javax.annotation.*")) + .recipe(new MoveAnnotationToArrayType("javax.annotation.*ull*")) .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jspecify")); } From a64835ed0afe8c6eaf03b85555196b0a8d47a5b7 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 22:56:31 +0200 Subject: [PATCH 06/12] Replace AtomicReference with find-then-remove using equality check --- .../jspecify/MoveAnnotationToArrayType.java | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index e43664aeff..6505b4f6ec 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -26,8 +26,6 @@ import org.jspecify.annotations.Nullable; -import java.util.concurrent.atomic.AtomicReference; - import static java.util.Collections.singletonList; @EqualsAndHashCode(callSuper = false) @@ -61,22 +59,20 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - AtomicReference matched = new AtomicReference<>(); - md = md.withLeadingAnnotations(ListUtils.map(md.getLeadingAnnotations(), a -> { - if (matched.get() == null && matchesType(a)) { - matched.set(a); - return null; - } - return a; - })); - - if (matched.get() == null) { + J.@Nullable Annotation match = md.getLeadingAnnotations().stream() + .filter(this::matchesType) + .findFirst() + .orElse(null); + if (match == null) { return md; } + J.Annotation toRemove = match; + md = md.withLeadingAnnotations(ListUtils.map(md.getLeadingAnnotations(), a -> a == toRemove ? null : a)); + J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + singletonList(match.withPrefix(Space.SINGLE_SPACE))); md = md.withReturnTypeExpression(arrayType); if (md.getLeadingAnnotations().isEmpty()) { md = md.withReturnTypeExpression(arrayType.withPrefix( @@ -93,22 +89,20 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return mv; } - AtomicReference matched = new AtomicReference<>(); - mv = mv.withLeadingAnnotations(ListUtils.map(mv.getLeadingAnnotations(), a -> { - if (matched.get() == null && matchesType(a)) { - matched.set(a); - return null; - } - return a; - })); - - if (matched.get() == null) { + J.@Nullable Annotation match = mv.getLeadingAnnotations().stream() + .filter(this::matchesType) + .findFirst() + .orElse(null); + if (match == null) { return mv; } + J.Annotation toRemove = match; + mv = mv.withLeadingAnnotations(ListUtils.map(mv.getLeadingAnnotations(), a -> a == toRemove ? null : a)); + J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + singletonList(match.withPrefix(Space.SINGLE_SPACE))); if (mv.getLeadingAnnotations().isEmpty()) { arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); } From b2b687abc79e27b37f5bbbfd9b5a5a5cd932878c Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 23:02:48 +0200 Subject: [PATCH 07/12] Use ListUtils.map result equality check instead of stream --- .../jspecify/MoveAnnotationToArrayType.java | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index 6505b4f6ec..5c9951ee51 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -26,6 +26,9 @@ import org.jspecify.annotations.Nullable; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + import static java.util.Collections.singletonList; @EqualsAndHashCode(callSuper = false) @@ -59,20 +62,23 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - J.@Nullable Annotation match = md.getLeadingAnnotations().stream() - .filter(this::matchesType) - .findFirst() - .orElse(null); - if (match == null) { + AtomicReference matched = new AtomicReference<>(); + List leading = ListUtils.map(md.getLeadingAnnotations(), a -> { + if (matched.get() == null && matchesType(a)) { + matched.set(a); + return null; + } + return a; + }); + if (leading == md.getLeadingAnnotations()) { return md; } - - J.Annotation toRemove = match; - md = md.withLeadingAnnotations(ListUtils.map(md.getLeadingAnnotations(), a -> a == toRemove ? null : a)); + md = md.withLeadingAnnotations(leading); J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); + //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(match.withPrefix(Space.SINGLE_SPACE))); + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); md = md.withReturnTypeExpression(arrayType); if (md.getLeadingAnnotations().isEmpty()) { md = md.withReturnTypeExpression(arrayType.withPrefix( @@ -89,20 +95,23 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return mv; } - J.@Nullable Annotation match = mv.getLeadingAnnotations().stream() - .filter(this::matchesType) - .findFirst() - .orElse(null); - if (match == null) { + AtomicReference matched = new AtomicReference<>(); + List leading = ListUtils.map(mv.getLeadingAnnotations(), a -> { + if (matched.get() == null && matchesType(a)) { + matched.set(a); + return null; + } + return a; + }); + if (leading == mv.getLeadingAnnotations()) { return mv; } - - J.Annotation toRemove = match; - mv = mv.withLeadingAnnotations(ListUtils.map(mv.getLeadingAnnotations(), a -> a == toRemove ? null : a)); + mv = mv.withLeadingAnnotations(leading); J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); + //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(match.withPrefix(Space.SINGLE_SPACE))); + singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); if (mv.getLeadingAnnotations().isEmpty()) { arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); } From 4da435edff1c2c3bff45d28830e7dfe5fb2e1859 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 23:05:28 +0200 Subject: [PATCH 08/12] Remove AtomicReference and redundant test --- .../jspecify/MoveAnnotationToArrayType.java | 45 ++++++----- .../jspecify/JSpecifyBestPracticesTest.java | 77 ------------------- 2 files changed, 24 insertions(+), 98 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index 5c9951ee51..c8732475dd 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -27,7 +27,6 @@ import org.jspecify.annotations.Nullable; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.singletonList; @@ -62,23 +61,25 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - AtomicReference matched = new AtomicReference<>(); - List leading = ListUtils.map(md.getLeadingAnnotations(), a -> { - if (matched.get() == null && matchesType(a)) { - matched.set(a); - return null; - } - return a; - }); - if (leading == md.getLeadingAnnotations()) { + List annotations = md.getLeadingAnnotations(); + List leading = ListUtils.map(annotations, a -> matchesType(a) ? null : a); + if (leading == annotations) { return md; } md = md.withLeadingAnnotations(leading); + J.@Nullable Annotation match = null; + for (J.Annotation a : annotations) { + if (matchesType(a)) { + match = a; + break; + } + } + J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + singletonList(match.withPrefix(Space.SINGLE_SPACE))); md = md.withReturnTypeExpression(arrayType); if (md.getLeadingAnnotations().isEmpty()) { md = md.withReturnTypeExpression(arrayType.withPrefix( @@ -95,23 +96,25 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return mv; } - AtomicReference matched = new AtomicReference<>(); - List leading = ListUtils.map(mv.getLeadingAnnotations(), a -> { - if (matched.get() == null && matchesType(a)) { - matched.set(a); - return null; - } - return a; - }); - if (leading == mv.getLeadingAnnotations()) { + List annotations = mv.getLeadingAnnotations(); + List leading = ListUtils.map(annotations, a -> matchesType(a) ? null : a); + if (leading == annotations) { return mv; } mv = mv.withLeadingAnnotations(leading); + J.@Nullable Annotation match = null; + for (J.Annotation a : annotations) { + if (matchesType(a)) { + match = a; + break; + } + } + J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(matched.get().withPrefix(Space.SINGLE_SPACE))); + singletonList(match.withPrefix(Space.SINGLE_SPACE))); if (mv.getLeadingAnnotations().isEmpty()) { arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); } 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 29bba08be6..e232d504aa 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/JSpecifyBestPracticesTest.java @@ -35,83 +35,6 @@ public void defaults(RecipeSpec spec) { .parser(JavaParser.fromJavaVersion().classpath("jsr305", "jspecify", "jakarta.annotation-api", "annotations", "spring-core", "micronaut-core")); } - @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") - @Test - void jspecifyArrayNullable() { - rewriteRun( - mavenProject("foo", - srcMainJava( - //language=java - java( - """ - import javax.annotation.Nullable; - - class Foo { - @Nullable - public byte[] bar() { - return null; - } - - public void baz(@Nullable byte[] a) { - } - } - """, - """ - import org.jspecify.annotations.Nullable; - - class Foo { - public byte @Nullable[] bar() { - return null; - } - - public void baz(byte @Nullable[] a) { - } - } - """ - ) - ), - //language=xml - pomXml( - """ - - 4.0.0 - com.example.foobar - foobar-core - 1.0.0 - - - javax.annotation - javax.annotation-api - 1.3.2 - - - - """, - """ - - 4.0.0 - com.example.foobar - foobar-core - 1.0.0 - - - javax.annotation - javax.annotation-api - 1.3.2 - - - org.jspecify - jspecify - 1.0.0 - - - - """ - ) - ) - ); - } - @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/934") @Test void existingJSpecifyArrayOfNullableElementsNotAffected() { From fd94c8468f31262372ef25d4607f5e3b185f4961 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 23:09:39 +0200 Subject: [PATCH 09/12] Capture match from ListUtils.map lambda, drop separate for loop --- .../jspecify/MoveAnnotationToArrayType.java | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java index c8732475dd..a28f7db7ac 100644 --- a/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java +++ b/src/main/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayType.java @@ -61,25 +61,23 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - List annotations = md.getLeadingAnnotations(); - List leading = ListUtils.map(annotations, a -> matchesType(a) ? null : a); - if (leading == annotations) { + J.@Nullable Annotation[] match = {null}; + List leading = ListUtils.map(md.getLeadingAnnotations(), a -> { + if (match[0] == null && matchesType(a)) { + match[0] = a; + return null; + } + return a; + }); + if (leading == md.getLeadingAnnotations()) { return md; } md = md.withLeadingAnnotations(leading); - J.@Nullable Annotation match = null; - for (J.Annotation a : annotations) { - if (matchesType(a)) { - match = a; - break; - } - } - J.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(match.withPrefix(Space.SINGLE_SPACE))); + singletonList(match[0].withPrefix(Space.SINGLE_SPACE))); md = md.withReturnTypeExpression(arrayType); if (md.getLeadingAnnotations().isEmpty()) { md = md.withReturnTypeExpression(arrayType.withPrefix( @@ -96,25 +94,23 @@ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations m return mv; } - List annotations = mv.getLeadingAnnotations(); - List leading = ListUtils.map(annotations, a -> matchesType(a) ? null : a); - if (leading == annotations) { + J.@Nullable Annotation[] match = {null}; + List leading = ListUtils.map(mv.getLeadingAnnotations(), a -> { + if (match[0] == null && matchesType(a)) { + match[0] = a; + return null; + } + return a; + }); + if (leading == mv.getLeadingAnnotations()) { return mv; } mv = mv.withLeadingAnnotations(leading); - J.@Nullable Annotation match = null; - for (J.Annotation a : annotations) { - if (matchesType(a)) { - match = a; - break; - } - } - J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); //noinspection DataFlowIssue arrayType = arrayType.withAnnotations( - singletonList(match.withPrefix(Space.SINGLE_SPACE))); + singletonList(match[0].withPrefix(Space.SINGLE_SPACE))); if (mv.getLeadingAnnotations().isEmpty()) { arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); } From 4d3de37bdc223dcaad47fc9fadda47f08a20f188 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 31 Mar 2026 23:19:00 +0200 Subject: [PATCH 10/12] Update generated recipes.csv --- .../resources/META-INF/rewrite/recipes.csv | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 26a2aef002..753df7c33f 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -40,7 +40,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.R maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.RemovedJaxBModuleProvided,Do not package `java.xml.bind` and `java.activation` modules in WebSphere Liberty applications,"The `java.xml.bind` and `java.activation` modules were removed in Java11. Websphere Liberty provides its own implementation of the modules, which can be used by specifying the `jaxb-2.2` feature in the server.xml file. This recipe updates the `javax.xml.bind` and `javax.activation` dependencies to use the `provided` scope to avoid class loading issues.",5,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.WasDevMvnChangeParentArtifactId,Change `net.wasdev.maven.parent:java8-parent` to `:parent`,This recipe changes the artifactId of the `` tag in the `pom.xml` from `java8-parent` to `parent`.,2,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.ComIntelliJAnnotationsToOrgJetbrainsAnnotations,Migrate com.intellij:annotations to org.jetbrains:annotations,This recipe will upgrade old dependency of com.intellij:annotations to the newer org.jetbrains:annotations.,2,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.JavaBestPractices,Java best practices,"Applies opinionated best practices for Java projects targeting Java 25. This recipe includes the full Java 25 upgrade chain plus additional improvements to code style, API usage, and third-party dependency reduction that go beyond what the version migration recipes apply.",20934,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.JavaBestPractices,Java best practices,"Applies opinionated best practices for Java projects targeting Java 25. This recipe includes the full Java 25 upgrade chain plus additional improvements to code style, API usage, and third-party dependency reduction that go beyond what the version migration recipes apply.",20939,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javaee6,Migrate to JavaEE6,"These recipes help with the Migration to Java EE 6, flagging and updating deprecated methods.",2,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javaee7,Migrate to JavaEE7,"These recipes help with the Migration to Java EE 7, flagging and updating deprecated methods.",8,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javaee8,Migrate to JavaEE8,"These recipes help with the Migration to Java EE 8, flagging and updating deprecated methods.",18,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, @@ -264,9 +264,9 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.j maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.jakarta.UpdateJakartaFacesApi41,Update Jakarta EE Java Faces Dependencies to 4.1.x,Update Jakarta EE Java Faces Dependencies to 4.1.x.,2,,Jakarta,Modernize,Java,,Recipes for migrating to [Jakarta EE](https://jakarta.ee/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.jakarta.OmniFacesNamespaceMigration,OmniFaces Namespace Migration,Find and replace legacy OmniFaces namespaces.,3,,Jakarta,Modernize,Java,,Recipes for migrating to [Jakarta EE](https://jakarta.ee/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.jakarta.UpgradeFaces41OpenSourceLibraries,Upgrade Faces open source libraries,Upgrade OmniFaces and MyFaces/Mojarra libraries to Jakarta EE11 versions.,5,,Jakarta,Modernize,Java,,Recipes for migrating to [Jakarta EE](https://jakarta.ee/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javax.AddColumnAnnotation,`@ElementCollection` annotations must be accompanied by a defined `@Column` annotation,"When an attribute is annotated with `@ElementCollection`, a separate table is created for the attribute that includes the attribute -ID and value. In OpenJPA, the column for the annotated attribute is named element, whereas EclipseLink names the column based on -the name of the attribute. To remain compatible with tables that were created with OpenJPA, add a `@Column` annotation with the name +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javax.AddColumnAnnotation,`@ElementCollection` annotations must be accompanied by a defined `@Column` annotation,"When an attribute is annotated with `@ElementCollection`, a separate table is created for the attribute that includes the attribute +ID and value. In OpenJPA, the column for the annotated attribute is named element, whereas EclipseLink names the column based on +the name of the attribute. To remain compatible with tables that were created with OpenJPA, add a `@Column` annotation with the name attribute set to element.",1,,`javax` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javax.AddDefaultConstructorToEntityClass,`@Entity` objects with constructors must also have a default constructor,"When a Java Persistence API (JPA) entity class has a constructor with arguments, the class must also have a default, no-argument constructor. The OpenJPA implementation automatically generates the no-argument constructor, but the EclipseLink implementation does not.",1,,`javax` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.javax.AddJaxwsRuntime,Use the latest JAX-WS API and runtime for Jakarta EE 8,"Update build files to use the latest JAX-WS runtime from Jakarta EE 8 to maintain compatibility with Java version 11 or greater. The recipe will add a JAX-WS run-time, in Gradle `compileOnly`+`testImplementation` and Maven `provided` scope, to any project that has a transitive dependency on the JAX-WS API. **The resulting dependencies still use the `javax` namespace, despite the move to the Jakarta artifact**.",3,,`javax` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, @@ -462,13 +462,14 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.d maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.datanucleus.UpgradeDataNucleus_5_2,Migrate to DataNucleus 5.2,"Migrate DataNucleus applications to 5.2. This recipe first applies the 5.1 migration, then handles the column mapping package move and query-related property renames introduced in 5.2.",87,,DataNucleus,Modernize,Java,,Recipes for migrating [DataNucleus](https://www.datanucleus.org/) JDO/JPA persistence applications.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.datanucleus.DataNucleusPackageMoves_5_2,DataNucleus 5.2 package moves,Relocate packages that were moved in DataNucleus 5.2.,2,,DataNucleus,Modernize,Java,,Recipes for migrating [DataNucleus](https://www.datanucleus.org/) JDO/JPA persistence applications.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.datanucleus.DataNucleusProperties_5_2,DataNucleus 5.2 property migrations,Rename property keys that changed in DataNucleus 5.2.,7,,DataNucleus,Modernize,Java,,Recipes for migrating [DataNucleus](https://www.datanucleus.org/) JDO/JPA persistence applications.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.JSpecifyBestPractices,JSpecify best practices,"Apply JSpecify best practices, such as migrating off of alternatives, and adding missing `@Nullable` annotations.",32,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateToJSpecify,Migrate to JSpecify,This recipe will migrate to JSpecify annotations from various other nullability annotation standards.,27,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJavaxAnnotationApi,Migrate from javax annotation API to JSpecify,Migrate from javax annotation API to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJakartaAnnotationApi,Migrate from Jakarta annotation API to JSpecify,Migrate from Jakarta annotation API to JSpecify.,5,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJetbrainsAnnotations,Migrate from JetBrains annotations to JSpecify,Migrate from JetBrains annotations to JSpecify.,5,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations,Migrate from Micrometer annotations to JSpecify,Migrate from Micrometer annotations to JSpecify.,5,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromSpringFrameworkAnnotations,Migrate from Spring Framework annotations to JSpecify,Migrate from Spring Framework annotations to JSpecify.,5,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromMicronautAnnotations,Migrate from Micronaut Framework annotations to JSpecify,Migrate from Micronaut Framework annotations to JSpecify.,5,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType,Move annotation to array type,"When an annotation like `@Nullable` is applied to an array type in declaration position, this recipe moves it to the array brackets. For example, `@Nullable byte[]` becomes `byte @Nullable[]`. Best used before `ChangeType` in a migration pipeline, targeting the pre-migration annotation type.",1,,Jspecify,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""annotationType"",""type"":""String"",""displayName"":""Annotation type"",""description"":""The type of annotation to move to the array type. Should target the pre-migration annotation type to avoid changing the semantics of pre-existing type-use annotations on object arrays."",""example"":""javax.annotation.*ull*"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.JSpecifyBestPractices,JSpecify best practices,"Apply JSpecify best practices, such as migrating off of alternatives, and adding missing `@Nullable` annotations.",37,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateToJSpecify,Migrate to JSpecify,This recipe will migrate to JSpecify annotations from various other nullability annotation standards.,32,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJavaxAnnotationApi,Migrate from javax annotation API to JSpecify,Migrate from javax annotation API to JSpecify.,7,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJakartaAnnotationApi,Migrate from Jakarta annotation API to JSpecify,Migrate from Jakarta annotation API to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromJetbrainsAnnotations,Migrate from JetBrains annotations to JSpecify,Migrate from JetBrains annotations to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromMicrometerAnnotations,Migrate from Micrometer annotations to JSpecify,Migrate from Micrometer annotations to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromSpringFrameworkAnnotations,Migrate from Spring Framework annotations to JSpecify,Migrate from Spring Framework annotations to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.jspecify.MigrateFromMicronautAnnotations,Migrate from Micronaut Framework annotations to JSpecify,Migrate from Micronaut Framework annotations to JSpecify.,6,,,JSpecify,Java,,,Recipes for adopting [JSpecify](https://jspecify.dev/) nullability annotations.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,com.google.guava.InlineGuavaMethods,Inline `guava` methods annotated with `@InlineMe`,Automatically generated recipes to inline method calls based on `@InlineMe` annotations discovered in the type table.,66,,,Guava,Google,,,,,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.scala.migrate.UpgradeScala_2_12,Migrate to Scala 2.12.+,Upgrade the Scala version for compatibility with newer Java versions.,2,,,Migrate,Scala,,,,,, From b8160162f63f9af3103ec4ddbfbc4b15cce23885 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 1 Apr 2026 11:48:28 +0200 Subject: [PATCH 11/12] Add test for nested class array type annotation movement --- .../MoveAnnotationToArrayTypeTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java index ea555d561a..ce6ca2002b 100644 --- a/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java +++ b/src/test/java/org/openrewrite/java/migrate/jspecify/MoveAnnotationToArrayTypeTest.java @@ -204,6 +204,32 @@ class Foo { ); } + @Test + void moveNullableToNestedClassArrayField() { + rewriteRun( + //language=java + java( + """ + import javax.annotation.Nullable; + import java.util.Map; + + class Foo { + @Nullable + public Map.Entry[] entries; + } + """, + """ + import javax.annotation.Nullable; + import java.util.Map; + + class Foo { + public Map.Entry @Nullable[] entries; + } + """ + ) + ); + } + @Test void noChangeForPreExistingJSpecifyAnnotation() { rewriteRun( From b4c7d302b9a294d73e5e6837efdbe7af48b92bd3 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 2 Apr 2026 21:03:14 +0200 Subject: [PATCH 12/12] Skip MoveAnnotationToArrayType for TYPE_USE annotations Jakarta, JetBrains, and Micronaut annotations target TYPE_USE, so `@Nullable X[]` (element nullable) already has distinct semantics from `X @Nullable[]` (array nullable). Moving them would change meaning. --- src/main/resources/META-INF/rewrite/jspecify.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/META-INF/rewrite/jspecify.yml b/src/main/resources/META-INF/rewrite/jspecify.yml index 7470ab9518..bbf2217104 100644 --- a/src/main/resources/META-INF/rewrite/jspecify.yml +++ b/src/main/resources/META-INF/rewrite/jspecify.yml @@ -95,8 +95,8 @@ recipeList: version: 1.0.0 onlyIfUsing: jakarta.annotation.*ull* acceptTransitive: true - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: jakarta.annotation.*ull* + # Not moving jakarta annotations to array brackets; they target TYPE_USE, + # so `@Nullable X[]` (element nullable) already differs from `X @Nullable[]` (array nullable). - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: jakarta.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -121,8 +121,8 @@ recipeList: version: 1.0.0 onlyIfUsing: org.jetbrains.annotations.*ull* acceptTransitive: true - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: org.jetbrains.annotations.*ull* + # Not moving JetBrains annotations to array brackets; they target TYPE_USE, + # so `@Nullable X[]` (element nullable) already differs from `X @Nullable[]` (array nullable). - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: org.jetbrains.annotations.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable @@ -199,8 +199,8 @@ recipeList: version: 1.0.0 onlyIfUsing: io.micronaut.core.annotation.*ull* acceptTransitive: true - - org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType: - annotationType: io.micronaut.core.annotation.*ull* + # Not moving Micronaut annotations to array brackets; they target TYPE_USE, + # so `@Nullable X[]` (element nullable) already differs from `X @Nullable[]` (array nullable). - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.micronaut.core.annotation.Nullable newFullyQualifiedTypeName: org.jspecify.annotations.Nullable