Version
NullAway 0.14.0, and current master (21f02dd). Error Prone 2.50.0, JDK 21.
Flags: -XepOpt:NullAway:JSpecifyMode=true -XDaddTypeAnnotationsToSymbol=true, with WarnOnGenericInferenceFailure on. It defaults to JSpecifyExperimental, so JSpecifyExperimental=true turns it on.
Summary
When a generic method's return type is @Nullable T, the inference check appears to read that @Nullable as a constraint on T itself. Combined with a non-null target type, it produces
inference failure: type variable T constrained to be both @NonNull and @Nullable
The arguments are irrelevant — a call with only non-null arguments reports just the same. A nullable target type does not report, and an explicit type witness does not report.
Standing alone this is a misleading second diagnostic next to a real one (compare #1721). It becomes a plain false positive when a @Contract makes the result non-null: the genuine finding correctly disappears, and the spurious inference failure is all that is left.
Reproducer: false positive
package foo;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
@Contract("_, !null -> !null")
static <T extends @Nullable Object> @Nullable T first(@Nullable T v0, @Nullable T v1) {
return v0 != null ? v0 : v1;
}
@Contract("_, !null -> !null")
static @Nullable String firstString(@Nullable String v0, @Nullable String v1) {
return v0 != null ? v0 : v1;
}
static String useGeneric(@Nullable String a) {
// BUG: Diagnostic contains: inference failure: type variable T constrained to be both @NonNull and @Nullable
return first(a, "");
}
static String useConcrete(@Nullable String a) {
return firstString(a, ""); // no finding
}
static String useWitness(@Nullable String a) {
return Test.<String>first(a, ""); // no finding
}
}
The inference failure is the only diagnostic on useGeneric. The contract is honoured everywhere else: no returning @Nullable expression is reported, and -XepOpt:NullAway:WarnOnGenericInferenceFailure=false leaves the method completely clean. So the code is right and the two checks disagree.
Reproducer: the underlying misdiagnosis, no contract involved
package foo;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static <T extends @Nullable Object> @Nullable T id(T v) {
return v;
}
static String nonNullTarget() {
// BUG: Diagnostic contains: inference failure: type variable T constrained to be both @NonNull and @Nullable
// BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type
return id("");
}
static @Nullable String nullableTarget() {
return id(""); // no finding
}
static String witness() {
// BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type
return Test.<String>id("");
}
}
T = String satisfies every constraint here. The result is nullable because the return type is @Nullable T, not because T is nullable, and the second finding on nonNullTarget says so correctly. The inference failure adds a conflict that does not exist, and the witness form shows it: with T pinned to non-null String, the same call is accepted and only the genuine finding remains.
What does and does not report
| Call |
Inference failure |
String s = id("") — non-null target |
yes |
@Nullable String s = id("") — nullable target |
no |
String s = Test.<String>id("") — explicit witness |
no |
String s = first(a, b) — both arguments non-null |
yes |
@Nullable String s = first(a, b) — both arguments nullable |
no |
T m(@Nullable T v0, T v1) — return type not @Nullable |
no |
Expected
@Nullable on a use of T in the return type constrains the result, not T. Inferring T = String for id("") in a String context should succeed, leaving the nullability of the result to the checks that already handle it correctly.
Workaround
Write the type argument out, or declare the method so the signature states the same thing without a contract:
static <T extends @Nullable Object> T firstNonNull(@Nullable T v0, T v1) {
return v0 != null ? v0 : v1;
}
T is inferred non-null from the second argument, @Nullable T still accepts a nullable first argument, and the result is non-null. Nothing reports.
Where we hit it
Util.first is the Elvis operator of the codebase, and 19 of its call sites report. Found while replacing the Checker Framework with NullAway in Apache Calcite (apache/calcite#5213).
Version
NullAway 0.14.0, and current
master(21f02dd). Error Prone 2.50.0, JDK 21.Flags:
-XepOpt:NullAway:JSpecifyMode=true -XDaddTypeAnnotationsToSymbol=true, withWarnOnGenericInferenceFailureon. It defaults toJSpecifyExperimental, soJSpecifyExperimental=trueturns it on.Summary
When a generic method's return type is
@Nullable T, the inference check appears to read that@Nullableas a constraint onTitself. Combined with a non-null target type, it producesThe arguments are irrelevant — a call with only non-null arguments reports just the same. A nullable target type does not report, and an explicit type witness does not report.
Standing alone this is a misleading second diagnostic next to a real one (compare #1721). It becomes a plain false positive when a
@Contractmakes the result non-null: the genuine finding correctly disappears, and the spurious inference failure is all that is left.Reproducer: false positive
The inference failure is the only diagnostic on
useGeneric. The contract is honoured everywhere else: noreturning @Nullable expressionis reported, and-XepOpt:NullAway:WarnOnGenericInferenceFailure=falseleaves the method completely clean. So the code is right and the two checks disagree.Reproducer: the underlying misdiagnosis, no contract involved
T = Stringsatisfies every constraint here. The result is nullable because the return type is@Nullable T, not becauseTis nullable, and the second finding onnonNullTargetsays so correctly. The inference failure adds a conflict that does not exist, and the witness form shows it: withTpinned to non-nullString, the same call is accepted and only the genuine finding remains.What does and does not report
String s = id("")— non-null target@Nullable String s = id("")— nullable targetString s = Test.<String>id("")— explicit witnessString s = first(a, b)— both arguments non-null@Nullable String s = first(a, b)— both arguments nullableT m(@Nullable T v0, T v1)— return type not@NullableExpected
@Nullableon a use ofTin the return type constrains the result, notT. InferringT = Stringforid("")in aStringcontext should succeed, leaving the nullability of the result to the checks that already handle it correctly.Workaround
Write the type argument out, or declare the method so the signature states the same thing without a contract:
Tis inferred non-null from the second argument,@Nullable Tstill accepts a nullable first argument, and the result is non-null. Nothing reports.Where we hit it
Util.firstis the Elvis operator of the codebase, and 19 of its call sites report. Found while replacing the Checker Framework with NullAway in Apache Calcite (apache/calcite#5213).