JSpecify: a `var` initialized with a conditional expression gets no @Nullable type argument from diamond or generic method inference

Open
#2,038 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
75/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
java
Domain
backend, testing

Research direction

Start with the TODO in ConditionalExprTests.varConditionalDoesNotProvideTargetTypeForGenericMethodInference and compare the existing tests for direct var inference and conditional expressions. Add regression cases for the Box diamond expression and generic box call, expecting nullable dereference diagnostics for both. Run the relevant ConditionalExprTests tests; done means both conditional cases report the expected diagnostics.

Written by the indexing model from the issue text.

Description

false negative jspecify

In JSpecify mode, var cond = flag ? new Box<>(null) : new Box<>("fallback"); gets a non-null type argument, so cond.get().hashCode() is not reported. Without the conditional, var d = new Box<>(null); is inferred as Box<@Nullable Object> (since #1708, v0.14.0) and d.get().hashCode() is reported. A generic method call behaves the same way: var d2 = box(null); is reported on dereference, var cond2 = flag ? box(null) : box("fallback"); is not.

This is the case left over from #1633: the TODO in ConditionalExprTests.varConditionalDoesNotProvideTargetTypeForGenericMethodInference still links #1633, which is otherwise fixed.

Version: NullAway master at 3035507e1 (after 0.14.2), JDK 21.0.9.

Flags: -XepOpt:NullAway:AnnotatedPackages=com.uber -XepOpt:NullAway:JSpecifyMode=true -XDaddTypeAnnotationsToSymbol=true -XepOpt:NullAway:JSpecifyExperimental=true

import org.jspecify.annotations.*;
@NullMarked
public class Test {
  static class Box<T extends @Nullable Object> {
    private final T value;
    Box(T value) { this.value = value; }
    T get() { return value; }
  }
  interface Box2<T extends @Nullable Object> { T get(); }
  static <T extends @Nullable Object> Box2<T> box(T t) { throw new RuntimeException(); }
  void test(boolean flag) {
    var cond = flag ? new Box<>(null) : new Box<>("fallback");
    cond.get().hashCode(); // expected: dereferenced expression is @Nullable
    var cond2 = flag ? box(null) : box("fallback");
    cond2.get().hashCode(); // expected: dereferenced expression is @Nullable
  }
}

Observed: no diagnostic on either dereference (a CompilationTestHelper test with this source and no // BUG: lines passes).

Expected: dereferenced expression cond.get() is @Nullable and dereferenced expression cond2.get() is @Nullable, the same diagnostic NullAway reports for the direct forms var d = new Box<>(null); d.get().hashCode(); and var d2 = box(null); d2.get().hashCode(); (checked with the same flags; both are reported).

Grounding: JLS 14.4.1 types a var local as the upward projection of its initializer's type, and JLS 15.25.3 types a reference conditional as the lub of its operand types, so the type argument of cond comes from Box<@Nullable Object> and Box<String>; one operand admits null, so the result should too. The existing TODO in ConditionalExprTests states the same expectation.

Related: #1633 (the direct form, fixed by #1708), #1477 (conditional expressions with a target type, closed).

Dominant language
Java
Stars
4.1k
Forks
379
Avg merge
1d 2h
Merged PRs (30d)
106

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from uber/NullAway

All issues in uber/NullAway

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.