Repository navigation
NullableOnMethodReturnType: only move annotations applicable to TYPE_USE - #968
Merged
timtebeek merged 5 commits intoAug 11, 2026
Conversation
The recipe matched any annotation whose name ends in `Nullable` and moved it into the return type without checking that the annotation is legal in a type context. For an array return the annotation lands on the array dimension, so a declaration-only annotation, one declared `@Target(ElementType.METHOD)`, turned `@Nullable public String[] value()` into `public String @nullable[] value()`, which no longer compiles. Read `@Target` from the annotation type and move the annotation only when `TYPE_USE` is among its element types, accepting both the single constant and the array form. An annotation with no `@Target`, with only declaration targets, or whose declaration cannot be resolved is left on the method, so the recipe stays conservative when type attribution is missing. JSpecify style `@Nullable` is `TYPE_USE` and still moves. A constructor has no return type expression, and the recipe removed the leading annotation before discovering that, silently deleting it. It now bails out before the removal. `AnnotateNullableMethods` delegates to this recipe for the method it has just annotated, and the `JavaTemplate` stub it parses is the only source of type attribution for that annotation. The stub now declares `@Target(ElementType.TYPE_USE)` for the annotations that recipe already classifies as type-use, so its output is unchanged. No existing test expectation changed.
3 of 4 tasks
mergify Bot
added a commit
to robfrank/linklift
that referenced
this pull request
Aug 20, 2026
… 2.40.0 to 2.41.0 [skip ci] Bumps [org.openrewrite.recipe:rewrite-static-analysis](https://github.com/openrewrite/rewrite-static-analysis) from 2.40.0 to 2.41.0. Release notes *Sourced from [org.openrewrite.recipe:rewrite-static-analysis's releases](https://github.com/openrewrite/rewrite-static-analysis/releases).* > 2.41.0 > ------ > > What's Changed > -------------- > > * Pre-install the JavaScript RPC npm package before running tests by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#956](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/956) > * UnnecessaryExplicitTypeArguments: retain witness for a return-only ty… by [`@neil-mushell`](https://github.com/neil-mushell) in [openrewrite/rewrite-static-analysis#958](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/958) > * Do not delete expressions that may have side effects by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#959](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/959) > * OpenRewrite recipe best practices by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#960](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/960) > * UseLambdaForFunctionalInterface: only convert when the anonymous class implements the SAM by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#962](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/962) > * ReplaceStringBuilderWithString: wrap `char[]` appends in `String.valueOf` by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/977) > * RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#971](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/971) > * Make sure ModifierOrder doesn't alter Python's def quasi-modifier by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#981](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/981) > * NullableOnMethodReturnType: only move annotations applicable to TYPE\_USE by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#968](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/968) > * `FallThrough` to work only for Java files by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#982](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/982) > * Retry a failed npx warm, and survive a machine without Node by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#984](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/984) > * Make `DefaultComesLast` apply only to Java by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#983](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/983) > > New Contributors > ---------------- > > * [`@martinfrancois`](https://github.com/martinfrancois) made their first contribution in [openrewrite/rewrite-static-analysis#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/977) > > **Full Changelog**: <openrewrite/rewrite-static-analysis@v2.40.0...v2.41.0> Commits * [`43b51de`](openrewrite/rewrite-static-analysis@43b51de) Make DefaultComesLast apply only to Java ([#983](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/983)) * [`5d08edd`](openrewrite/rewrite-static-analysis@5d08edd) Retry a failed npx warm, and survive a machine without Node ([#984](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/984)) * [`49ddf78`](openrewrite/rewrite-static-analysis@49ddf78) FallThrough to work only for Java files ([#982](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/982)) * [`9699a7e`](openrewrite/rewrite-static-analysis@9699a7e) NullableOnMethodReturnType: only move annotations applicable to TYPE\_USE ([#968](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/968)) * [`0c6f970`](openrewrite/rewrite-static-analysis@0c6f970) Make sure ModifierOrder doesn't alter Python's def quasi-modifier ([#981](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/981)) * [`6ef24c5`](openrewrite/rewrite-static-analysis@6ef24c5) RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides ([#971](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/971)) * [`0d8f7d7`](openrewrite/rewrite-static-analysis@0d8f7d7) ReplaceStringBuilderWithString: wrap `char[]` appends in `String.valueOf` ([#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/977)) * [`9a15a92`](openrewrite/rewrite-static-analysis@9a15a92) OpenRewrite recipe best practices * [`a394f54`](openrewrite/rewrite-static-analysis@a394f54) UnnecessaryExplicitTypeArguments: regression test for witness on varargs meth... * [`0ecb6d8`](openrewrite/rewrite-static-analysis@0ecb6d8) Update Gradle wrapper to 9.7.0 * Additional commits viewable in [compare view](openrewrite/rewrite-static-analysis@v2.40.0...v2.41.0) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's changed?
NullableOnMethodReturnTypenow moves a
@Nullableannotation onto the return type only when that annotation is allowed in atype context. An annotation declared
@Target(ElementType.METHOD), or with no@Target, stays onthe method. A constructor is also left alone, because it has no return type.
With this annotation declaration:
and this method:
current main produces
public String @Nullable[] value(). With this change the method is left alone.A new helper,
isApplicableToTypeUse, reads the@Targetmeta-annotation from the type of the@Nullableannotation itself, not from the method's return type. If the annotation's own type doesnot resolve, the annotation is not moved.
What's your motivation?
javac rejects the output of current main with
error: annotation @Nullable not applicable in this type context: the recipe turns a file that compiles into one that does not. It hits everydeclaration-only
@Nullable, which includes JSR-305javax.annotation.Nullable, Spring'sorg.springframework.lang.Nullable, AndroidX'sandroidx.annotation.Nullableand any@Nullableaproject declares for itself without
@Target(ElementType.TYPE_USE).Current main has a second defect, on constructors. It produces no compile error, but it silently
loses the annotation: the annotation is removed from the method before the recipe checks whether
there is a return type to move it onto, and a constructor has no return type. This change checks for
the return type first. Both defects reproduce on 2.40.0 and on 2.41.0-SNAPSHOT built from main.
Anything in particular you'd like reviewers to focus on?
One other recipe needed a small change.
AnnotateNullableMethodsgives the annotation it adds atype by parsing a small stub of that annotation's declaration, then calls
NullableOnMethodReturnTypeto place it. On this branch that stub declares
@Target(ElementType.TYPE_USE)whenever theannotation being added is one of the four names in that recipe's own
TYPE_USE_NULLABLE_ANNOTATIONSset. Without that addition the stub would carry no
@Target, the new guard would decline to move theannotation, and
AnnotateNullableMethodswould stop placing it on the return type.That
@Targettherefore comes fromTYPE_USE_NULLABLE_ANNOTATIONS, not from the real declaration onthe classpath. If one of the four names ever stops being
@Target(TYPE_USE), the stub will still sayit is.
Three limits:
descriptionand therecipes.csvrow say so.public @Nullable Map.Entry<K, V> e(), which javac rejects. This change does not alter that;MoveFieldAnnotationToTypecovers it."Support more nullability return annotations", which is not mine, does. It answers open issue NullableOnMethodReturnType seems incomplete #318,
whose complaint is the opposite of the one fixed here: NullableOnMethodReturnType seems incomplete #318 says the recipe moves too few
annotations. Support more nullability return annotations #945 widens matching by simple name with no
@Targetcheck, and two of the names itadds,
CheckForNullandNonnull, resolve in its own test fixture to JSR-305 annotations that arenot applicable to type contexts, so moved onto a return type they produce exactly the non-compiling
output this pull request is about. The guard added here is a prerequisite for that work. I am happy
to rebase onto Support more nullability return annotations #945, or for Support more nullability return annotations #945 to rebase onto this branch, whichever you prefer.
Have you considered any alternatives or workarounds?
Yes. The new check could apply only when the return type is an array, so that a declaration-only
annotation would still be moved in front of a non-array return type.
That is possible because the two positions differ. An annotation written directly before a non-array
return type,
public @Nullable String test(), is still parsed as a declaration annotation on themethod when the annotation type is not applicable to type contexts, so that shape compiles even for a
declaration-only annotation. In
public String @Nullable[] test()the annotation can only be a typeannotation, and there a declaration-only annotation does not compile. The array position is the one
current main uses for an array return type, and the one javac rejects.
The narrower version would keep the recipe doing something for JSR-305 and Spring instead of nothing.
Against it: its output would read as though the annotation applied to the return type, while in Java
it would still apply to the method, so the code would say something other than what it means.
Switching is small: move
!isApplicableToTypeUse(...)into theinstanceof J.ArrayTypebranch andupdate one expectation, in
noChangeForDeclarationOnlyAnnotation, a test this branch adds: itsnon-array
scalar()method would then be rewritten, itsString[][]one still not.Any additional context
This change adds 8 tests to
NullableOnMethodReturnTypeTest. Without the code change in this pullrequest, 4 of them fail:
noChangeForAnnotationWithoutTarget,noChangeForConstructor,noChangeForDeclarationOnlyAnnotationandnoChangeForDeclarationOnlyAnnotationOnArrayType. Noexisting test expectation changed; the test file is additions only.
The other 4 pass either way.
moveTypeUseAnnotationToArrayTypeandmoveAnnotationTargetingBothMethodAndTypeUseassert that@Target(TYPE_USE)and@Target({METHOD, TYPE_USE})annotations are still moved onto the return type;noChangeForDeclarationOnlyAnnotationInModifierPositionandnoChangeForUnknownAnnotationTypeassert that an annotation written among the modifiers, and one whose type does not resolve, are still
left alone. The class holds 16 tests.
This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and
this description.
Checklist