Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.Space;
import org.openrewrite.java.tree.TypeUtils;

import java.util.List;

@EqualsAndHashCode(callSuper = false)
@Value
public class RemoveAnnotationFromVoidMethod extends Recipe {

@Option(displayName = "Annotation type",
description = "The type of annotation to remove from methods returning `void`.",
example = "org.jetbrains.annotations.*ull*")
String annotationType;

String displayName = "Remove annotation from `void` method";

String description = "Nullability annotations are meaningless on methods returning `void`, " +
"and type-use annotations such as JSpecify's `@Nullable` are not permitted there. " +
"This recipe removes such annotations so they are not carried over by a later `ChangeType`.";

@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() == null ||
md.getReturnTypeExpression().getType() != JavaType.Primitive.Void) {
return md;
}

// Annotations following a modifier, as in `public @Nullable static void m()`
md = md.withModifiers(ListUtils.map(md.getModifiers(),
m -> m.withAnnotations(removeMatching(m.getAnnotations()))));

// Annotations preceding type parameters, as in `public @Nullable <T> void m()`
J.TypeParameters typeParameters = md.getAnnotations().getTypeParameters();
if (typeParameters != null) {
md = md.getAnnotations().withTypeParameters(
typeParameters.withAnnotations(removeMatching(typeParameters.getAnnotations())));
}

// Annotations directly preceding the return type, as in `public @Nullable void m()`
if (md.getReturnTypeExpression() instanceof J.AnnotatedType) {
J.AnnotatedType annotatedType = (J.AnnotatedType) md.getReturnTypeExpression();
List<J.Annotation> annotations = annotatedType.getAnnotations();
List<J.Annotation> remaining = removeMatching(annotations);
if (remaining.isEmpty()) {
md = md.withReturnTypeExpression(annotatedType.getTypeExpression().withPrefix(
annotatedType.getPrefix().withWhitespace(annotations.get(0).getPrefix().getWhitespace())));
} else if (remaining != annotations) {
Space firstAnnotationPrefix = annotations.get(0).getPrefix();
md = md.withReturnTypeExpression(annotatedType.withAnnotations(
ListUtils.mapFirst(remaining, a -> a.withPrefix(firstAnnotationPrefix))));
}
}

List<J.Annotation> original = md.getLeadingAnnotations();
List<J.Annotation> leading = removeMatching(original);
if (leading == original) {
return md;
}

Space firstPrefix = original.get(0).getPrefix();
if (!leading.isEmpty()) {
leading = ListUtils.mapFirst(leading, a -> a.withPrefix(firstPrefix));
return md.withLeadingAnnotations(leading);
}

md = md.withLeadingAnnotations(leading);
if (!md.getModifiers().isEmpty()) {
md = md.withModifiers(Space.formatFirstPrefix(md.getModifiers(),
Space.firstPrefix(md.getModifiers()).withWhitespace(firstPrefix.getWhitespace())));
} else if (md.getPadding().getTypeParameters() != null) {
md = md.getPadding().withTypeParameters(md.getPadding().getTypeParameters().withPrefix(
md.getPadding().getTypeParameters().getPrefix().withWhitespace(firstPrefix.getWhitespace())));
} else {
md = md.withReturnTypeExpression(md.getReturnTypeExpression().withPrefix(
md.getReturnTypeExpression().getPrefix().withWhitespace(firstPrefix.getWhitespace())));
}
return md;
}

private List<J.Annotation> removeMatching(List<J.Annotation> annotations) {
return ListUtils.map(annotations, a -> {
if (matchesType(a)) {
maybeRemoveImport(TypeUtils.asFullyQualified(a.getType()));
return null;
}
return a;
});
}

private boolean matchesType(J.Annotation ann) {
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(ann.getType());
return fq != null && typeMatcher.matches(fq);
}
});
}
}
26 changes: 26 additions & 0 deletions src/main/resources/META-INF/rewrite/jspecify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ recipeList:
version: 1.0.0
onlyIfUsing: javax.annotation.*ull*
acceptTransitive: true
# Not `*ull*`, which would also remove the `*ByDefault` annotations; those are meaningful on methods
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: javax.annotation.Nullable
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: javax.annotation.CheckForNull
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: javax.annotation.Nonnull
- org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType:
annotationType: javax.annotation.*ull*
- org.openrewrite.java.ChangeType:
Expand Down Expand Up @@ -102,6 +109,8 @@ recipeList:
version: 1.0.0
onlyIfUsing: jakarta.annotation.*ull*
acceptTransitive: true
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
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:
Expand Down Expand Up @@ -130,6 +139,8 @@ recipeList:
version: 1.0.0
onlyIfUsing: org.jetbrains.annotations.*ull*
acceptTransitive: true
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
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:
Expand Down Expand Up @@ -158,6 +169,8 @@ recipeList:
version: 1.0.0
onlyIfUsing: org.springframework.lang.*ull*
acceptTransitive: true
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: io.micrometer.core.lang.*ull*
- org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType:
annotationType: io.micrometer.core.lang.*ull*
- org.openrewrite.java.ChangeType:
Expand Down Expand Up @@ -186,6 +199,8 @@ recipeList:
version: 1.0.0
onlyIfUsing: org.springframework.lang.*ull*
acceptTransitive: true
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: org.springframework.lang.*ull*
- org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType:
annotationType: org.springframework.lang.*ull*
- org.openrewrite.java.ChangeType:
Expand Down Expand Up @@ -214,6 +229,8 @@ recipeList:
version: 1.0.0
onlyIfUsing: io.micronaut.core.annotation.*ull*
acceptTransitive: true
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
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:
Expand Down Expand Up @@ -242,6 +259,15 @@ recipeList:
version: 1.0.0
onlyIfUsing: edu.umd.cs.findbugs.annotations.*ull*
acceptTransitive: true
# Not `*ull*`, which would also remove the `*ByDefault` annotations; those are meaningful on methods
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: edu.umd.cs.findbugs.annotations.Nullable
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: edu.umd.cs.findbugs.annotations.CheckForNull
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: edu.umd.cs.findbugs.annotations.PossiblyNull
- org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod:
annotationType: edu.umd.cs.findbugs.annotations.NonNull
- org.openrewrite.java.migrate.jspecify.MoveAnnotationToArrayType:
annotationType: edu.umd.cs.findbugs.annotations.*ull*
- org.openrewrite.java.ChangeType:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 the original author or authors.
* 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.
Expand Down Expand Up @@ -261,6 +261,108 @@ public void bar(@Nullable String baz) {
);
}

@Test
void jetbrainsAnnotationMethodWithVoidReturnType() {
rewriteRun(
//language=java
java(
"""
class Foo {
@org.jetbrains.annotations.Nullable
public void bar(int i) {
}
}
""",
"""
class Foo {
public void bar(int i) {
}
}
"""
)
);
}

@Test
void jakartaAnnotationMethodWithVoidReturnType() {
rewriteRun(
//language=java
java(
"""
class Foo {
@jakarta.annotation.Nullable
public void bar(int i) {
}
}
""",
"""
class Foo {
public void bar(int i) {
}
}
"""
)
);
}

@Test
void javaxParametersAreNonnullByDefaultOnVoidMethodMigratedToNullMarked() {
rewriteRun(
//language=java
java(
"""
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

class Foo {
@Nullable
@ParametersAreNonnullByDefault
public void bar(String s) {
}
}
""",
"""
import org.jspecify.annotations.NullMarked;

class Foo {
@NullMarked
public void bar(String s) {
}
}
"""
)
);
}

@Test
void spotBugsReturnValuesAreNonnullByDefaultOnVoidMethodRetained() {
rewriteRun(
//language=java
java(
"""
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;

class Foo {
@Nullable
@ReturnValuesAreNonnullByDefault
public void bar(String s) {
}
}
""",
"""
import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;

class Foo {
@ReturnValuesAreNonnullByDefault
public void bar(String s) {
}
}
"""
)
);
}

@Test
void migrateFromJakartaAnnotationApiToJspecify() {
rewriteRun(
Expand Down
Loading
Loading