Unify method-reference return-nullness handling for contracts and library models
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
Research direction
Read ContractHandler.onMatchMethodReference, onOverrideMethodReferenceReturnNullability in LibraryModelsHandler, and ContractsTests.contractMethodReferenceReturnNullnessCurrentBehaviorJSpecify first. Trace how each path derives argument nullness for bound and unbound references and feeds compatibility checks and generic inference. Unify those paths while keeping stream-filter refinements separate, then update the regression test to verify the contract cases are accepted and nullable inputs remain rejected.
Written by the indexing model from the issue text.
Description
Follow-up to #1968 and this review discussion.
Method-reference return nullness currently travels through two paths. ContractHandler.onMatchMethodReference uses setComputedNullness for override checks, while the new onMethodReferenceReturnNullnessHint hook is used by LibraryModelsHandler for both return compatibility and generic inference. Equivalent contracts and library models therefore behave differently. The divergence includes false positives for non-null inputs and a false negative for a type-variable input whose nullness is unresolved.
With #1968 in JSpecify mode, Class.cast references support bound and unbound references and generic stream inference. For a method annotated @Contract("!null -> !null") with a nullable declared return, the current behavior is:
| Case | Current result |
|---|---|
Static or bound reference assigned to Function<String, String> |
Accepted |
Unbound reference assigned to BiFunction<Test, String, String> |
Rejected as returning @Nullable |
Stream<String>.map(Test::id) returned as Stream<String> |
Inference failure: R constrained to both @NonNull and @Nullable |
| Equivalent lambdas | Accepted |
<T extends @Nullable Object> Function<T, String> using Test::str |
Accepted despite unresolved input nullness (false negative) |
Same type-variable target using String.class::cast |
Rejected as returning @Nullable |
For example:
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
@Contract("!null -> !null")
static @Nullable String id(@Nullable String s) { return s; }
@Contract("!null -> !null")
@Nullable String self(@Nullable String s) { return s; }
@Contract("!null -> !null")
static @Nullable String str(@Nullable Object o) {
return o == null ? null : o.toString();
}
Function<String, String> staticReference() {
return Test::id; // Accepted.
}
BiFunction<Test, String, String> unboundReference() {
return Test::self; // False positive: referenced method returns @Nullable.
}
Stream<String> streamMethodReference(Stream<String> input) {
return input.map(Test::id); // False positive: conflicting nullness constraints on R.
}
Stream<String> streamLambda(Stream<String> input) {
return input.map(s -> id(s)); // Accepted.
}
<T extends @Nullable Object> Function<T, String> contractTypeVariable() {
return Test::str; // False negative: accepted even though T may admit null.
}
<T extends @Nullable Object> Function<T, String> libraryModelTypeVariable() {
return String.class::cast; // Rejected: referenced method returns @Nullable.
}
}
The regression test ContractsTests.contractMethodReferenceReturnNullnessCurrentBehaviorJSpecify pins the unbound-reference and stream-inference behavior pending the follow-up.
For the type-variable case, ContractHandler.funcInterfaceParamIsNonNull treats the parameter of type T as non-null and sets the computed return nullness to non-null. The library-model path instead uses GenericsChecks.getTypeNullnessHint, which returns UNKNOWN for an unannotated type variable with a nullable upper bound. The contract only guarantees a non-null result for non-null inputs; it cannot guarantee a non-null result for every permitted instantiation of T. The contract reference should therefore be rejected just as the Class.cast reference is. This comparison was identified in the re-review of #1968.
A possible direction is to use one handler path for contract- and library-model-based method-reference return nullness in both compatibility checking and generic inference. Share argument-nullness derivation, including declaration annotations, substituted type arguments, unresolved type variables, and unbound receiver offsets. Keep flow-dependent stream-filter refinements separate.
The fix should accept the non-null contract cases above, reject the nullable-bound type-variable contract reference, retain rejection when inputs may be null, and remain conservative for unresolved nullness and unsupported reference forms. Update the pinned regression test when the two paths are unified.
- Dominant language
- Java
- Stars
- 4.1k
- Forks
- 379
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 106
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from uber/NullAway
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
uber/NullAway#2145 · 1 reaction ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
uber/NullAway#2058 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
uber/NullAway#1899 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 15/100
uber/NullAway#2160 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 62/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
johanhaleby/occurrent#1505 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
FasterXML/jackson-databind#6443 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
finos/fluxnova-bpm-platform#387 ·
Maintainers usually reply within 4 days
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100