Skip to content

Place @Nullable correctly on qualified nested return types - #996

Draft
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/nullable-qualified-nested-return
Draft

Place @Nullable correctly on qualified nested return types#996
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/nullable-qualified-nested-return

Conversation

@martinfrancois

@martinfrancois martinfrancois commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Suggested review order: 30 of 52 (Score: 3.5)
Review first: #970

What's changed?

Adds 1 known-failing test to NullableOnMethodReturnTypeTest that reproduces a wrong annotation position: when the return type is a qualified nested type, the recipe places a TYPE_USE @Nullable before the whole qualified name instead of before the innermost simple name. No recipe code changes. The test is marked @ExpectedToFail so the suite stays green; removing the mark shows the failure. With the mark in place, the class runs 17 tests, 1 skipped, and all 16 pre-existing tests still pass.

What's your motivation?

Recipe: the nullable method-return annotation recipe.

NullableOnMethodReturnType moves @Nullable from its own line onto the return type. For a qualified nested type it moves the annotation to the wrong spot.

Before

import org.jspecify.annotations.Nullable;

import java.util.Map;

class Test<K, V> {
    @Nullable
    public Map.Entry<K, V> entry() {
        return null;
    }
}

Actual after the recipe

Using current main.

public @Nullable Map.Entry<K, V> entry() {

This output does not compile. org.jspecify.annotations.Nullable is TYPE_USE only, and javac rejects the produced form with:

error: type annotation @Nullable is not expected here ... (to annotate a qualified type, write Map.@Nullable Entry<K,V>)

Expected after the recipe

The annotation MUST be attached to the innermost simple name. This output compiles and follows JLS 9.7.4 and javac's diagnostic:

public Map.@Nullable Entry<K, V> entry() {

This assumes the annotation is TYPE_USE targeted; since #968 merged, that is the only case the recipe acts on. Before the recipe runs, the code compiles. After the recipe runs, the code should still compile, with the annotation attached to the innermost simple name. The mechanism: for non-array return types the recipe wraps the whole type in a J.AnnotatedType, so the annotation lands before Map. The correct placement logic already exists in MoveFieldAnnotationToType (see #379 for the fully qualified handling), which AnnotateNullableMethods chains after this recipe; the defect only shows when NullableOnMethodReturnType runs on its own.

Found while preparing #968, which fixed related defects in this recipe and disclosed this leftover in its description.

Anything in particular you'd like reviewers to focus on?

I think this is a genuine bug, because the recipe turns compiling code into non-compiling code. Note the existing nestedType test (from #579) is not in conflict: it uses org.openrewrite.internal.lang.Nullable, a declaration annotation, where placement before the qualified name is legal. A fix for this case should still check whether that test's expectation public @Nullable B.C bar() needs to change with it. If you agree this should change, I would gladly prepare the fix. If this behavior is intended, feel free to close this and I know it is settled.

Have you considered any alternatives or workarounds?

Running the full AnnotateNullableMethods chain instead of this recipe alone avoids the broken output, because MoveFieldAnnotationToType repairs the position afterwards. That only helps users of the composite recipe, not of this recipe by itself.

Any additional context

Pre-existing tests changed: None.

The limit was disclosed in the description of my merged #968. This reproduction was prepared with AI assistance (Claude Code). I reviewed the tests and this description.

The added reproduction tests and the existing suite together cover changed and unchanged behavior. The known-failing tests remain disabled until implementation. The formatter run was calibrated per file; untouched lines were not reformatted.

Checklist

…urn type

moveNullableToNestedTypeSimpleName pins the correct placement of a TYPE_USE
annotation on a qualified nested return type: Map.@nullable Entry<K, V>.
The recipe currently emits public @nullable Map.Entry<K, V>, which javac
rejects for a TYPE_USE-only annotation such as org.jspecify.annotations.
Nullable. The limit was disclosed in openrewrite#968. Marked @ExpectedToFail until
fixed.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 11, 2026
@martinfrancois martinfrancois changed the title NullableOnMethodReturnType: add failing test for qualified nested return type Place @Nullable correctly on qualified nested return types Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant