Skip to content

JSpecify migrations remove nullability annotations from void methods - #1285

Open
sullis wants to merge 1 commit into
openrewrite:mainfrom
sullis:ss-jspecify-edge-case
Open

sullis wants to merge 1 commit into
openrewrite:mainfrom
sullis:ss-jspecify-edge-case

Conversation

@sullis

@sullis sullis commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What's changed

  • New recipe org.openrewrite.java.migrate.jspecify.RemoveAnnotationFromVoidMethod. It removes annotations matching a type pattern (for example org.jetbrains.annotations.*ull*) from methods that return void, wherever they appear in the declaration:

    • before the modifiers: @Nullable public void m()
    • after a modifier: public @Nullable static void m()
    • before type parameters: public static @Nullable <T> void m()
    • directly before the return type: public @Nullable void m()

    The import is removed too when nothing else uses it.

  • All seven JSpecify migrations in jspecify.yml (javax, jakarta, JetBrains, Micrometer, Spring, Micronaut, FindBugs) now run this recipe before ChangeType, and before MoveAnnotationToArrayType in the migrations that use it.

    • jakarta, JetBrains, Micrometer, Spring, and Micronaut use the *ull* pattern.
    • javax and FindBugs list the annotations one by one instead (javax: Nullable, CheckForNull, Nonnull; FindBugs: Nullable, CheckForNull, PossiblyNull, NonNull). In those packages *ull* also matches the *ByDefault annotations, such as @ParametersAreNonnullByDefault, which still mean something on a void method and must not be removed.

Why

A nullability annotation on a void method has no meaning. JSpecify's @Nullable is a TYPE_USE annotation, so Java doesn't allow it on a void return. Before this change, ChangeType carried code like @Nullable void foo() over to JSpecify unchanged, and the migrated code failed to compile.

Scope

  • Only methods returning primitive void are changed. Methods returning boxed Void keep their annotations, since Void can legitimately be null.
  • Annotations on parameters of void methods are left alone.
  • Annotations that don't match the pattern, such as @Deprecated, stay in place.
  • *ByDefault annotations on void methods are kept, so the existing migrations still handle them (for example, javax @ParametersAreNonnullByDefault still becomes @NullMarked).
  • Formatting is preserved: after removal, the declaration keeps its indentation whether it starts with modifiers, type parameters, or the return type.

Tests

  • RemoveAnnotationFromVoidMethodTest covers:
    • removing one matching annotation, and several at once
    • keeping annotations that don't match the pattern
    • methods without modifiers, and methods with type parameters
    • annotations after modifiers, before type parameters, and mixed with non-matching annotations
    • leaving non-void methods, parameter annotations, and boxed Void methods unchanged
  • JSpecifyBestPracticesTest adds end-to-end cases for:
    • void methods annotated with JetBrains and jakarta @Nullable
    • a void method with javax @Nullable and @ParametersAreNonnullByDefault: @Nullable is removed and @ParametersAreNonnullByDefault becomes @NullMarked
    • a void method with FindBugs @Nullable and @ReturnValuesAreNonnullByDefault: @Nullable is removed and @ReturnValuesAreNonnullByDefault is kept

🤖 Generated with Claude Code

@sullis
sullis force-pushed the ss-jspecify-edge-case branch from a9b327e to 91ca5b5 Compare October 7, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready to Review

Development

Successfully merging this pull request may close these issues.

2 participants