-
Notifications
You must be signed in to change notification settings - Fork 116
Move type-use annotations to array brackets during JSpecify migration #1038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5420515
277ff50
504b4d0
ed23fc5
6f7b27c
a64835e
f40ea20
b2b687a
4da435e
fd94c84
4d3de37
b816016
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /* | ||
| * Copyright 2026 the original author or authors. | ||
| * <p> | ||
| * 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 | ||
| * <p> | ||
| * https://docs.moderne.io/licensing/moderne-source-available-license | ||
| * <p> | ||
| * 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 org.jspecify.annotations.Nullable; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| 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. " + | ||
| "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*") | ||
| String annotationType; | ||
|
|
||
| String displayName = "Move annotation to array type"; | ||
|
|
||
| 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<?, ExecutionContext> getVisitor() { | ||
| return Preconditions.check(new UsesType<>(annotationType, null), new JavaIsoVisitor<ExecutionContext>() { | ||
| final TypeMatcher typeMatcher = new TypeMatcher(annotationType); | ||
|
|
||
| @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; | ||
| } | ||
|
|
||
| J.@Nullable Annotation[] match = {null}; | ||
| List<J.Annotation> 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.ArrayType arrayType = (J.ArrayType) md.getReturnTypeExpression(); | ||
| //noinspection DataFlowIssue | ||
| arrayType = arrayType.withAnnotations( | ||
| singletonList(match[0].withPrefix(Space.SINGLE_SPACE))); | ||
| md = md.withReturnTypeExpression(arrayType); | ||
| if (md.getLeadingAnnotations().isEmpty()) { | ||
| md = md.withReturnTypeExpression(arrayType.withPrefix( | ||
| arrayType.getPrefix().withWhitespace(""))); | ||
| } | ||
| return autoFormat(md, arrayType, ctx, getCursor().getParentOrThrow()); | ||
| } | ||
|
|
||
| @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; | ||
| } | ||
|
|
||
| J.@Nullable Annotation[] match = {null}; | ||
| List<J.Annotation> 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); | ||
|
Comment on lines
+98
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose I'm trying to figure out if we have recipes that are going to be fragile in light of leading annotations being dropped and whether any of our recipes need to be adapted to handle the change in position for the annotations (have not had a chance to dig into this further yet)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know what you'd need to be comfortable seeing this merged! |
||
|
|
||
| J.ArrayType arrayType = (J.ArrayType) mv.getTypeExpression(); | ||
| //noinspection DataFlowIssue | ||
| arrayType = arrayType.withAnnotations( | ||
| singletonList(match[0].withPrefix(Space.SINGLE_SPACE))); | ||
| if (mv.getLeadingAnnotations().isEmpty()) { | ||
| arrayType = arrayType.withPrefix(arrayType.getPrefix().withWhitespace("")); | ||
| } | ||
| mv = mv.withTypeExpression(arrayType); | ||
| return autoFormat(mv, arrayType, ctx, getCursor().getParentOrThrow()); | ||
| } | ||
|
|
||
| private boolean matchesType(J.Annotation ann) { | ||
| JavaType.FullyQualified fq = TypeUtils.asFullyQualified(ann.getType()); | ||
| return fq != null && typeMatcher.matches(fq); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there the scenario to handle when the type of the elements in the array is a nested class? Pulled from the implementation below
J.@Nullable Annotation[]is the ideal form, but do we need to consider if someone wrote@Nullable J.Annotation[], or would that just not compile to begin with?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were working through parallel PRs also related to type use annotations
There we settled on recognizing a select few, which if folks were adding these to nested types already, I imagine they knew what they were doing and wouldn't want those moved to the array itself.