You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JSpecify: false positive when a method returns ? extends T and T has a @Nullable upper bound (support NO_CHANGE nullness operator better) #1727
NullAway 0.14.0, and current master (21f02dd). Error Prone 2.50.0, JDK 21.
Flags: -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:HandleWildcardGenerics=true -XDaddTypeAnnotationsToSymbol=true. HandleWildcardGenerics defaults to JSpecifyExperimental, so JSpecifyExperimental=true triggers it too. With wildcard handling off, nothing is reported.
Summary
A method whose return type is a type variable R instantiated to ? extends T produces a @Nullable result whenever T is declared T extends @Nullable Object, even though the wildcard's bound is T itself. Returning that result as T is then reported, and so is passing it to anything that expects T.
The wildcard's bound is what gets lost: Box<? extends String> and Box<T> are both handled correctly, and ? extends T misbehaves only when T has a @Nullable upper bound. That suggests NullAway falls back to the type parameter's declared bound (@Nullable Object on Box's R) instead of reading the wildcard's own bound.
Reproducer
importorg.jspecify.annotations.NullMarked;
importorg.jspecify.annotations.Nullable;
@NullMarkedpublicclassTest {
interfaceBox<Rextends@NullableObject> {
Rget();
}
static <Textends@NullableObject> Tunwrap(Box<? extendsT> b) {
// BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return typereturnb.get();
}
}
Test.java:11: warning: [NullAway] returning @Nullable expression from method with @NonNull return type
return b.get();
^
Note that the message calls T a @NonNull return type although T is bounded by @Nullable Object.
What does and does not report
Same Box, same flags:
Signature
Reported
<T extends @Nullable Object> T unwrap(Box<T> b)
no
<T> T unwrap(Box<? extends T> b)
no
String unwrap(Box<? extends String> b)
no
<T extends @Nullable Object> Box<T> unwrap(Box<? extends Box<T>> b)
no
<T extends @Nullable Object> T unwrap(Box<? extends T> b)
yes
The result also poisons a local. Storing it in a T local reports nothing on its own, but every later use of that local that requires non-null is reported:
static <Textends@NullableObject> voidpass(Box<? extendsT> b) {
Tt = b.get();
// BUG: Diagnostic contains: passing @Nullable parameter 't' where @NonNull is requiredTest.<T>sink(t);
}
static <Textends@NullableObject> voidsink(Tt) {}
Why the wildcard is the right declaration
The wildcard is not decoration. In a reduce whose accumulator starts at null:
callers pass reducers that accept a null accumulator but never produce one, so their type is Fn2<@Nullable Integer, Integer, Integer>. Without the wildcard, generic invariance rejects them against Fn2<A, S, A> where A is @Nullable Integer.
Workaround
@SuppressWarnings("NullAway") on the enclosing method.
Where we hit it
EnumerableDefaults.aggregate, while replacing the Checker Framework with NullAway in Apache Calcite (apache/calcite#5213).
The constructor assignment also drags a second, cascading report along with it:
Test.java:17: warning: [NullAway] initializer method does not guarantee @NonNull field 'current' (line 14) is initialized along all control-flow paths (remember to check for exceptions or early returns).
The field variant splits the same way as the return variant: Box<T> is clean, <T> ... Box<? extends T> with a non-null bound is clean, and only <T extends @Nullable Object> ... Box<? extends T> reports.
A JDK type behaves identically, so library models are not involved:
@vlsi I just wanted to thank you quickly for all these great reports and for testing JSpecifyExperimental! It'll take us some time to go through the reports, particularly as I'm juggling a few other things. But, we'll get to them as soon as we can, and try to fix as much as possible for the next release.
changed the title [-]JSpecify: false positive when a method returns `? extends T` and `T` has a `@Nullable` upper bound[/-][+]JSpecify: false positive when a method returns `? extends T` and `T` has a `@Nullable` upper bound (support parametric nullness)[/+]on Aug 26, 2026
This one is going to require some work, since it requires better modeling the NO_CHANGE nullness operator from the JSpecify spec, rather than just treating expressions as being either @Nullable or @NonNull. When we have code like:
static <Textends@NullableObject> Tunwrap(Box<? extendsT> b) {
// BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return typereturnb.get();
}
We want to reason that the nullability of b.get() will always be compatible with the nullness of T, so the code is safe. But, if we have code like:
Status check on master at 9555b20: every case in this issue still behaves as reported, and #1767 and #1806 track them all as @Ignored tests in NullnessOperatorTests.
The false positives (the body's return and local-argument examples, the aggregate signature, and both field examples from the first comment) still reproduce with JSpecifyExperimental=true and share one cause, the missing NO_CHANGE operator. With JSpecifyMode=true alone, the return example is clean.
For aggregate, NullAway reports twice: passing @Nullable parameter 'acc' where @NonNull is required on acc = func.apply(acc, s);, as well as at return acc;.
The dereference through Foo<? extends U> from the 2026-08-26 comment is already reported, so that guard holds.
The t.hashCode() false negative from the 2026-08-28 comment involves no wildcard and is missed in plain JSpecify mode too. Since the plan is to get the non-wildcard cases working first, I've moved it to JSpecify: dereferencing a T value is not reported when T extends @Nullable Object #2027 so it can be fixed and closed on its own; issue1727NullableBoundDereferenceFalseNegative can point there.
Activity
vlsi commentedon Aug 24, 2026
Another sink for the same wrong nullness: assigning the result to a field of type
T. Verified onmaster(21f02dd) with the same flags.The constructor assignment also drags a second, cascading report along with it:
The field variant splits the same way as the return variant:
Box<T>is clean,<T> ... Box<? extends T>with a non-null bound is clean, and only<T extends @Nullable Object> ... Box<? extends T>reports.A JDK type behaves identically, so library models are not involved:
Found in the same Calcite migration (apache/calcite#5213).
msridhar commentedon Aug 24, 2026
@vlsi I just wanted to thank you quickly for all these great reports and for testing JSpecifyExperimental! It'll take us some time to go through the reports, particularly as I'm juggling a few other things. But, we'll get to them as soon as we can, and try to fix as much as possible for the next release.
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Point the NullAway workarounds at the issues they came…
[CALCITE-7736] Let the shared utilities take nullable arguments
[-]JSpecify: false positive when a method returns `? extends T` and `T` has a `@Nullable` upper bound[/-][+]JSpecify: false positive when a method returns `? extends T` and `T` has a `@Nullable` upper bound (support parametric nullness)[/+]msridhar commentedon Aug 26, 2026
This one is going to require some work, since it requires better modeling the
NO_CHANGEnullness operator from the JSpecify spec, rather than just treating expressions as being either@Nullableor@NonNull. When we have code like:We want to reason that the nullability of
b.get()will always be compatible with the nullness ofT, so the code is safe. But, if we have code like:We want to reason that
f.get()could be@Nullable, so we should warn on a dereference (relevant spec section).@vlsi how critical is this one for you all? For now I plan to de-prioritize to see if there are easier issues we can quickly fix first.
11 remaining items
Report a type variable whose bound admits null where a non-null wildc…
Report a type variable whose bound admits null where a non-null wildc…
Report a type variable whose bound admits null where a non-null wildc…
Resolve an inference result by the type javac inferred for it
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
[CALCITE-7736] Let the shared utilities take nullable arguments
vlsi commentedon Oct 22, 2026
Status check on
masterat 9555b20: every case in this issue still behaves as reported, and #1767 and #1806 track them all as@Ignored tests inNullnessOperatorTests.aggregatesignature, and both field examples from the first comment) still reproduce withJSpecifyExperimental=trueand share one cause, the missingNO_CHANGEoperator. WithJSpecifyMode=truealone, the return example is clean.aggregate, NullAway reports twice:passing @Nullable parameter 'acc' where @NonNull is requiredonacc = func.apply(acc, s);, as well as atreturn acc;.Foo<? extends U>from the 2026-08-26 comment is already reported, so that guard holds.t.hashCode()false negative from the 2026-08-28 comment involves no wildcard and is missed in plain JSpecify mode too. Since the plan is to get the non-wildcard cases working first, I've moved it to JSpecify: dereferencing aTvalue is not reported whenT extends @Nullable Object#2027 so it can be fixed and closed on its own;issue1727NullableBoundDereferenceFalseNegativecan point there.