JSpecify mode: a diamond passed to `<U> U id(U u)` is inferred as `Box<String>` whatever the target type

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

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
56/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
java
Domain
testing-qa

Research direction

Start with GenericDiamondTests.genericMethodTypeVarParamWithDiamondArg in nullaway/src/test/java/com/uber/nullaway/ and read the JSpecify inference handling for diamond expressions passed to generic methods. Compare the bare type-variable parameter in id with the nested type-variable parameter in idBox, using the provided CompilationTestHelper sample and flags. Done means the nullable-target cases produce no diagnostic and the non-null target reports the null argument, matching the stated expectations.

Written by the indexing model from the issue text.

Description

false negative false positive jspecify

With JSpecify mode on, a diamond new Box<>(…) passed to a generic method whose parameter is the bare type variable, <U> U id(U u), is inferred as Box<String> no matter what the result is assigned to. Box<@Nullable String> b = id(new Box<>(null)) is reported as an incompatible assignment, and Box<String> b = id(new Box<>(null)), which passes null to the constructor of a Box<String>, is not reported. The same calls through <U> Box<U> idBox(Box<U> b), where the parameter mentions the type variable inside a type, are handled correctly since #1708 and #1544 (0.14.0).

Versions: NullAway master at e91ea4bcf and the 0.14.2 release give the same result. Error Prone 2.50.0, JDK 21.0.9. Flags: -XepOpt:NullAway:AnnotatedPackages=com.uber -XepOpt:NullAway:JSpecifyMode=true -XDaddTypeAnnotationsToSymbol=true, and the same with -XepOpt:NullAway:JSpecifyExperimental=true added.

Code sample. I ran it as a CompilationTestHelper test in nullaway/src/test/java/com/uber/nullaway/ with makeTestHelperWithArgs, without // BUG: markers, so the helper lists every diagnostic.

package com.uber;
import org.jspecify.annotations.*;
@NullMarked
public class Test {
  static class Box<T extends @Nullable Object> {
    Box(T value) {}
  }
  static <U extends @Nullable Object> U id(U u) { return u; }
  static <U extends @Nullable Object> Box<U> idBox(Box<U> b) { return b; }
  static @Nullable String nullableString() { return null; }
  static void takeNullableBox(Box<@Nullable String> b) {}
  void assign() {
    Box<@Nullable String> b = id(new Box<>(null));
  }
  void arg() {
    takeNullableBox(id(new Box<>(null)));
  }
  Box<@Nullable String> ret() {
    return id(new Box<>(nullableString()));
  }
  void positive() {
    Box<String> b = id(new Box<>(null));
  }
  void controlAssign() {
    Box<@Nullable String> b = idBox(new Box<>(null));
  }
  void controlPositive() {
    Box<String> b = idBox(new Box<>(null));
  }
}

Expected: no diagnostic on lines 13, 16, and 19, as on line 25, where idBox gets the same arguments. On line 22, passing @Nullable parameter 'null' where @NonNull is required, as on line 28. The idBox lines are NullAway's own behavior for the same diamond, and the parameter shape is the only difference.

Actual (both versions):

/Test.java:13: warning: [NullAway] incompatible types: Box<String> cannot be converted to Box<@Nullable String>
/Test.java:16: warning: [NullAway] incompatible types: Box<String> cannot be converted to Box<@Nullable String>
/Test.java:19: warning: [NullAway] incompatible types: Box<String> cannot be converted to Box<@Nullable String>
/Test.java:28: warning: [NullAway] passing @Nullable parameter 'null' where @NonNull is required

With JSpecifyExperimental=true the same four, plus inference failure: type variable T constrained to be both @NonNull and @Nullable on lines 19 and 28. Line 22 gets no diagnostic in either configuration.

GenericDiamondTests.genericMethodTypeVarParamWithDiamondArg uses this shape but only checks that there is no stack overflow. #1825 (constructor-declared type variables in a diamond call) is a different shape. Found while triaging #1470.

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.