Improved inference for generic method calls#1244
Conversation
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (3)
743-771: Self-contained “firstOrDefault” scenario is solid; consider one more negative checkNice, minimal harness to validate interplay between nullable elements and non-null default. For slightly broader coverage, you could add a negative where the default value is null to ensure parameter-flow constraints fire:
static void use() { // should infer T -> @Nullable String String result = firstOrDefault(Collections.singletonList(null), "hello"); // BUG: Diagnostic contains: dereferenced expression result is @Nullable result.hashCode(); // should infer T -> @NonNull String String result2 = firstOrDefault(Collections.singletonList("bye"), "hello"); result2.hashCode(); + // negative: default value null should be rejected + // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required + String bad = firstOrDefault(Collections.singletonList("x"), null); }
773-800: Future-proof the ignored test by marking the local as @nullableWhen this test is eventually un-ignored, the assignment
String x = null;will get flagged under @NullMarked. Declaringxas @nullable makes the intent explicit and avoids an unrelated failure.static void use() { - String x = null; + @Nullable String x = null; // should infer T -> @Nullable String String result = firstOrDefault(Collections.singletonList(x), "hello"); // BUG: Diagnostic contains: dereferenced expression result is @Nullable result.hashCode(); }
876-897: Consider asserting nullability on the inferred return in the “ok” pathThe “bad” case validates respecting the non-null bound from the Function parameter. For symmetry, you can assert that the “ok” case yields a @nullable result by adding a dereference diagnostic:
static void ok(Function<@Nullable String, @Nullable String> f) { String s = applyId(f, null); + // BUG: Diagnostic contains: dereferenced expression s is @Nullable + s.hashCode(); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java(3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-14T18:50:06.094Z
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.094Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.
Applied to files:
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
- GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: Build Caffeine with snapshot
🔇 Additional comments (6)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (6)
710-714: Good addition: exercises both explicit and inferred return type-variable pathsAdding the explicit type-arg call and the pure inference call strengthens coverage for return type-variable inference in presence of nullable bounds.
802-827: Nice coverage for varargs inference across nullable/non-null casesThis block does a good job exercising:
- all-non-null varargs,
- mixed varargs with null,
- interaction with a helper returning @nullable String.
The expectations on foo3 and foo6 are particularly helpful to validate parameter-flow constraints vs. LHS-driven inference.
829-855: Lambda inference scenario captured and scoped with @ignore appropriatelyClear, focused test of lambda handling with a note on current limitations. This will be useful to un-ignore once lambda constraints are supported.
856-874: Field-assignment cases look on pointGood mix of legal and illegal assignments to ensure inference across assignment context is working for both nullable and non-null RHS.
899-920: Good placeholder for respecting class type-var upper boundsHaving this test in place (ignored) will help ensure we don’t regress once upper-bound handling for class type variables is implemented.
244-246: No stale test expectations; all tests use the updated diagnostic phrasing
A scan of allDiagnostic contains:assertions confirms every occurrence now uses “Cannot assign from type … to type …”. No tests remain with the old wording.
lazaroclapp
left a comment
There was a problem hiding this comment.
Mostly questions as I swap all this into memory, and the occasional nit. Overall LGTM! 🙂
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
Show resolved
Hide resolved
| // this is a generic constructor call with no explicit type arguments | ||
| return true; | ||
| } | ||
| }*/ |
There was a problem hiding this comment.
Why is the support for constructors commented out? If this is future work, maybe a TODO comment?
There was a problem hiding this comment.
Good catch! This is some bogus code that I think Copilot must have generated? I deleted it and added a comment that supporting generic constructors using the diamond operator is a TODO in 7ffb9da
| * @param methodSymbol the symbol for the method being called | ||
| * @param methodInvocationTree the method invocation tree representing the call | ||
| * @param allInvocations a set of all method invocations that require inference, including nested | ||
| * ones |
There was a problem hiding this comment.
If this is an output parameter, constructed while generating the constraints, it probably should be noted in the javadoc, since the invocations only pass a set of size 1 here :)
There was a problem hiding this comment.
I updated the Javadoc in 44b0e75. The set does get mutated inside generateConstraintsForParam.
| } else { | ||
| Type argumentType = getTreeType(argument, config); | ||
| if (argumentType == null) { | ||
| // bail out of any checking involving raw types for now |
There was a problem hiding this comment.
This admits raw type arguments on any argument when solving constraints, right? We err on the side of a false negative (this is fine, just want to make sure my understanding is correct)
There was a problem hiding this comment.
Yes, this is correct. We currently have lots of false negatives with raw types (and that will likely continue for the foreseeable future...)
| state, config, ((Type.ForAll) type).qtype, typeVarNullability) | ||
| .getReturnType(); | ||
| Type returnTypeAtCallSite = castToNonNull(ASTHelpers.getType(invocationTree)); | ||
| return TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( |
There was a problem hiding this comment.
Both getTypeWithInferredNullability and this call apply restoreExplicitNullabilityAnnotations with different typeVarNullability. I am not sure I fully understand what each step here is doing. Might need a quick comment above each call?
There was a problem hiding this comment.
I tried improving the comments in 676b89c; hope it's clearer now
There was a problem hiding this comment.
Took me a moment still, but I think I got it. And yeah, not sure there is a super easy way to explain it... 😅
| } | ||
|
|
||
| private static boolean isTypeVariable(Type t) { | ||
| // for now ignore captures |
There was a problem hiding this comment.
Captures means <?> here. Right?
There was a problem hiding this comment.
Yes, I clarified the comment in 8ad8e7b
| subtypeComponentType.accept(this, superComponentType); | ||
| superComponentType.accept(this, subtypeComponentType); |
There was a problem hiding this comment.
Why in both directions? Aren't arrays covariant?
Edit: Ah, I see the issue. It's easy to allow A a to be passed to @Nullable A[] b as b[0] = a, but passing A[] c to an argument expecting @Nullable A[] b means now the callee can write null values into c... what is the right JSpecify section for this? Can't parse this requirement out of e.g. https://jspecify.dev/docs/spec/#nullness-delegating-subtyping
There was a problem hiding this comment.
No, you are right, array subtyping is covariant! I just messed this up. Fixed with a test in 04f7bff
There was a problem hiding this comment.
This looks right per covariance, but also, I still see an issue here more broadly, no?
Consider this example:
class Main {
public static <T extends Object> void f(T[] vals, T other) {
vals[0] = other;
}
public static void main(String[] args) {
String[] cell = new String[1];
f(cell, "Hello");
System.out.println(cell[0]);
f(cell, new Integer(7));
System.out.println(cell[0]);
}
}
It actually compiles for Java, but results in a runtime java.lang.ArrayStoreException.
Unfortunately, we don't have a similar runtime check mechanism, so:
class Main {
public static <T extends @Nullable Object> void f(T[] vals, T other) {
vals[0] = other;
}
public static void main(String[] args) {
String[] cell = new String[1];
f(cell, "Hello");
System.out.println(cell[0].hashCode());
f(cell, null);
System.out.println(cell[0].hashCode());
}
}
will just plainly result in a false negative for NullAway and an NPE at runtime, right?
At the same time, forcing invariance for arrays might not be conformant and also potentially overly restricting compared to basic Java subtyping.
I am sure there is a 50+ messages thread on this in JSpecify somewhere 😅 ...
There was a problem hiding this comment.
You're exactly right 😂 jspecify/jspecify#65 And the conclusion was that despite the extra unsoundness, covariant subtyping for arrays wins, to match Java.
There was a problem hiding this comment.
Very well then, LGTM!
| NullnessState n = | ||
| (st.nullness == NullnessState.UNKNOWN) ? NullnessState.NONNULL : st.nullness; |
There was a problem hiding this comment.
Isn't this redundant with the check below that will return InferredNullability.NONNULL for anything but NullnessState.NULLABLE?
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (6)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (6)
241-246: Stabilize diagnostic substring to reduce brittlenessThis assertion currently relies on “Cannot assign from type Foo to type Foo<@nullable Object>”. Other tests in this file commonly match on “due to mismatched nullability of type parameters” for similar scenarios. Consider aligning to that phrasing to make the test more resilient to message tweaks.
Proposed tweak:
- // BUG: Diagnostic contains: Cannot assign from type Foo<Object> to type Foo<@Nullable Object> + // BUG: Diagnostic contains: due to mismatched nullability of type parameters
743-771: Great self-contained inference scenario; consider adding null-default variantThis test nicely exercises return-context + parameter constraints. Optional: add a case where the default is null to also validate solver behavior when only the default carries nullness.
If you’re interested, add:
+ // should infer T -> @Nullable String from default alone + String result3 = firstOrDefault(Collections.singletonList("bye"), null); + // BUG: Diagnostic contains: dereferenced expression result3 is @Nullable + result3.hashCode();
773-800: Ignored test: track with an issue reference for follow-upThe scenario is useful but currently skipped. Please link to a tracking issue in the Ignore reason to avoid bitrotting.
Proposed tweak:
- @Ignore("need better support for generics inference combined with local vars") + @Ignore("need better support for generics inference combined with local vars; see issue #1075")
802-827: Varargs inference cases look solid; add zero-arg call to exercise return-context-only inferenceGreat coverage of mixed-nullness and helper-returned nullable elements. One more high-value case: inference with zero varargs arguments (no parameter-flow info), relying solely on the assignment context.
Example:
+ // Inference from assignment context only (no args) + Foo<String> foo0 = make(); + // And the nullable variant + Foo<@Nullable String> foo0n = make();
829-854: Reasonable to ignore lambda-based inference for now; please cross-reference a tracking issueSince lambdas are a known gap, the Ignore is fine. Add an issue reference in the message for traceability.
- @Ignore("need better handling of lambdas") + @Ignore("need better handling of lambdas; see issue #1075")
899-920: Ignored test for respecting non-null bounds on class type vars: OK, link to trackingGood placeholder to prevent regressions once implemented. Please reference the tracking issue in the Ignore to make intent explicit.
- @Ignore("we need to respect upper bounds of class type variables during inference") + @Ignore("respect upper bounds of class type variables during inference; see issue #1075")📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java(1 hunks)nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java(3 hunks)🚧 Files skipped from review as they are similar to previous changes (2)
- nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java
- nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: msridhar PR: uber/NullAway#1245 File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102 Timestamp: 2025-08-14T18:50:06.159Z Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.📚 Learning: 2025-08-14T18:50:06.159Z
Learnt from: msridhar PR: uber/NullAway#1245 File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102 Timestamp: 2025-08-14T18:50:06.159Z Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.Applied to files:
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: Build spring-framework with snapshot
- GitHub Check: Build caffeine with snapshot
🔇 Additional comments (4)
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (4)
710-714: LGTM: explicit vs inference exercise is valuableCovering both explicit type args and inference for a type-variable return improves confidence in the solver changes. No issues.
856-874: Field-initializer inference tests look goodThese cover return-context constraints well, including both nullable and non-null assignments. No changes requested.
888-892: Potential missing diagnostic on assignment to non-null localHere T should be inferred as @nullable String (from f) or as @nonnull String (from the assignment context). If it’s inferred @nullable, the assignment to a non-null local should be flagged. If it’s inferred @nonnull, passing null for x should be flagged. As written, no diagnostic is asserted here; please confirm intended behavior and either:
- assert an assignment error, or
- assert a parameter-passing error.
This will ensure we don’t accidentally mask a soundness issue.
Proposed options:
- If assignment is the error:
- String s = applyId(f, null); + // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull + String s = applyId(f, null);
- If parameter passing is the error (due to return-context forcing T non-null):
- String s = applyId(f, null); + // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required + String s = applyId(f, null);
922-948: Array covariance test is on pointThis accurately tests array covariance interacting with nullability inference and diagnostics. Looks good.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)
318-319: Samethisleak issue as above.This constructor has the same pattern of leaking
thistoGenericsChecks.
🧹 Nitpick comments (2)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)
301-303: Potential issue with leakingthisin constructor.While the comment acknowledges the leak, passing
thisfrom a constructor to another object is still a code smell that could lead to issues ifGenericsCheckstries to access uninitialized fields ofNullAway. Consider using a factory method or builder pattern to ensure full initialization before passing the reference.Would you like me to suggest a refactoring that avoids the
thisleak?nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
503-507: Consider logging or reporting unsatisfiable constraints.When constraints are unsatisfiable, the code silently falls back to javac's inferred nullability. Consider at least logging this scenario for debugging purposes, as the TODO suggests reporting an error might be beneficial.
} catch (UnsatisfiableConstraintsException e) { - // for now, do nothing. we'll just end up using whatever nullability gets attached by - // javac. - // TODO report an error here, optionally? + // Log the unsatisfiable constraints for debugging + // TODO: Consider reporting an error here based on configuration + if (config.isDebugMode()) { + // Log the exception details for debugging + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
nullaway/src/main/java/com/uber/nullaway/NullAway.java(13 hunks)nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java(3 hunks)nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java(51 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
🧰 Additional context used
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (2)
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java (1)
UnsatisfiableConstraintsException(21-25)nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
TypeSubstitutionUtils(22-256)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
- GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: Build spring-framework with snapshot
- GitHub Check: Build caffeine with snapshot
🔇 Additional comments (7)
nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java (2)
17-21: LGTM! Clean refactoring to instance-based GenericsChecks.The transition from
ConfigtoGenericsChecksis well-executed, making the visitor's dependencies clearer by directly accepting the generics checking logic rather than configuration.
65-66: Good refactoring to instance methods.The change from static method calls with config to instance method calls on genericsChecks improves encapsulation and aligns with the instance-based architecture.
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (5)
459-460: Good encapsulation of solver creation.Creating the solver through a factory method provides good abstraction and makes testing easier.
1435-1442: Well-documented synthetic type creation.The synthetic
@Nullableannotation type creation is well-documented with clear explanation of whyErrorTypeis used. This is a clever workaround for javac's package-private limitations.
621-633: Good separation of concerns for inference detection.The static helper method
isGenericCallNeedingInferencecleanly encapsulates the logic for determining when type inference is needed, making the code more maintainable.
486-502: Efficient constraint solving with shared results.The approach of collecting all invocations that need inference and then applying the same solution to all of them is efficient and avoids redundant computation. The use of
LinkedHashSetmaintains insertion order while preventing duplicates.
606-611: Recursive constraint generation handles nested calls well.The recursive generation of constraints for nested generic method calls is elegantly handled. The
allInvocationsset effectively tracks all calls requiring inference.
| genericsChecks.getGenericReturnNullnessAtInvocation( | ||
| ASTHelpers.getSymbol(tree), tree, state, config); | ||
| ASTHelpers.getSymbol(tree), tree, state); |
There was a problem hiding this comment.
Parameter removal is incomplete - config is still referenced.
The call to genericsChecks.getGenericReturnNullnessAtInvocation correctly removed the config parameter, but config is still being accessed in the condition on Line 1101. Since GenericsChecks is now instance-based and already has access to the config through its own field, you should check if the config.isJSpecifyMode() check is still needed here or if it should be handled internally within GenericsChecks.
Three fixes: 1. There may be multiple `TypeVariable` objects representing the same underlying type variable due to differences in annotations (e.g., `T` vs `@Nullable T`). Switch our maps key'd on `TypeVariable` to use the underlying `Element` instead, which should uniquely identify the type variable. 2. Previously when handling constraints, we would treat types like `@Nullable T` as if they were the same as just `T`; this was wrong. Now, only treat `TypeVariable` objects with no nullness annotations as actual type variables whose nullability must be inferred. 3. Before we didn't properly determine whether an `AssignmentTree` was assigning to a local variable. For testing, we also add a new config flag `WarnOnGenericInferenceFailure` to allow for a warning to be emitted when inference fails for a generic method call. This way, we can test the expectation that generic inference fails for certain calls. This is off by default; I'm still unsure how best to expose these inference-related errors to the user.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java (1)
21-25: Unchecked exception rationale acknowledgedThe explanatory comment for using
RuntimeExceptionis sufficient and aligns with prior discussion. LGTM.
🧹 Nitpick comments (11)
nullaway/src/main/java/com/uber/nullaway/Config.java (1)
314-320: Make the new Config method default to preserve binary compatibilityAdding a new abstract method to a public interface breaks external implementors. Recommend providing a default implementation returning
false.Apply this diff:
- boolean warnOnGenericInferenceFailure(); + default boolean warnOnGenericInferenceFailure() { + return false; + }nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java (1)
312-312: Constructor wiring correctly reads the new flagDefaulting to
falseis sensible. Consider documenting this flag in the user-facing configuration docs/wiki alongside other flags.I can draft the docs snippet for the wiki/README if helpful.
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java (2)
28-36: Avoid boolean parameter “boolean trap” with a small enum
localVariableTypeis ambiguous at call sites. Consider an enum to make intent explicit (and extensible if more sources arise).For example:
- void addSubtypeConstraint(Type subtype, Type supertype, boolean localVariableType) + enum ConstraintOrigin { GENERAL, ASSIGN_TO_LOCAL } + void addSubtypeConstraint(Type subtype, Type supertype, ConstraintOrigin origin)
52-53: Tighten key type for stronger contracts (optional)Returning
Map<Element, InferredNullability>is loose; callers likely expectTypeVariableSymbol. Consider:
- Narrowing to
Map<Symbol.TypeVariableSymbol, InferredNullability>, or- Documenting that keys are always
TypeVariableSymbol.Example signature change:
- Map<Element, InferredNullability> solve() throws UnsatisfiableConstraintsException; + Map<com.sun.tools.javac.code.Symbol.TypeVariableSymbol, InferredNullability> solve() + throws UnsatisfiableConstraintsException;nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (7)
743-771: Nice self-contained inference scenario; consider adding a branch-constraint stress case.This test validates combining argument- and assignment-context constraints. To also cover the case where the default value is
nullbut the list element is non-null (conflicting lower/upper constraints), you could add:static void use() { // should infer T -> @Nullable String String result = firstOrDefault(Collections.singletonList(null), "hello"); // BUG: Diagnostic contains: dereferenced expression result is @Nullable result.hashCode(); // should infer T -> @NonNull String String result2 = firstOrDefault(Collections.singletonList("bye"), "hello"); result2.hashCode(); + // default is null, element is non-null; inference should conservatively pick @Nullable + String result3 = firstOrDefault(Collections.singletonList("bye"), null); + // BUG: Diagnostic contains: dereferenced expression result3 is @Nullable + result3.hashCode(); }
773-801: Avoid long-lived @ignore; tie to a tracking issue or assert current behavior.Right now this is ignored pending better support for inference with local vars. Consider either referencing the tracking issue directly in the annotation or converting to an explicit “current behavior” test (with a
BUG:expectation) until support lands.- @Ignore("need better support for generics inference combined with local vars") + @Ignore("need better support for generics inference combined with local vars; tracked in #1075")
833-858: Lambda inference: prefer a targeted failing assertion over @ignore where feasible.If possible, encode today’s limitation with an explicit “BUG: Diagnostic contains: Cannot pass parameter …” rather than skipping the test, and keep the
@Ignoreonly for the positive path you want once inference improves. Also, adding an issue reference in the message helps track re-enablement.
860-878: Field-assignment coverage is good; add a deref to lock in flow-sensitive nullness.To assert that the inferred nullability on
field3flows to subsequent dereferences, add:@Nullable String field3 = id(null); @Nullable String field4 = id("hello"); + // BUG: Diagnostic contains: dereferenced expression field3 is @Nullable + { field3.hashCode(); }
903-924: Upper bound on class type variables: add an issue reference in @ignore.Helps future triage and re-enablement when support is added.
- @Ignore("we need to respect upper bounds of class type variables during inference") + @Ignore("respect upper bounds of class type variables during inference; tracked in #1075 (or follow-up)")
1029-1047: Reassignment test: assert downstream deref uses the inferred nullness.Currently it only compiles; add a deref to make the expected nullness of
resultobservable.void test() { Object result = null; result = make(); + // BUG: Diagnostic contains: dereferenced expression result is @Nullable + result.hashCode(); }
1092-1098: Helper with inference-failure warnings: naming and scope nit.The helper is clear and localized. Optional: rename to
makeHelperWarnOnInferenceFailure()to be consistent with other helpers and use it only in tests that assert those warnings to minimize accidental coupling.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (8)
nullaway/src/main/java/com/uber/nullaway/Config.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java(4 hunks)nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java(14 hunks)nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
- nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-14T18:50:06.159Z
Learnt from: msridhar
PR: uber/NullAway#1245
File: guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java:101-102
Timestamp: 2025-08-14T18:50:06.159Z
Learning: In NullAway JSpecify tests, when JDK version requirements exist due to bytecode annotation reading capabilities, prefer failing tests over skipping them on unsupported versions to ensure CI catches regressions and enforces proper JDK version usage for developers.
Applied to files:
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on windows-latest with Error Prone 2.41.0
- GitHub Check: JDK 17 on macos-latest with Error Prone 2.41.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.31.0
- GitHub Check: JDK 17 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: JDK 11 on ubuntu-latest with Error Prone 2.14.0
- GitHub Check: Build spring-framework with snapshot
- GitHub Check: Build caffeine with snapshot
🔇 Additional comments (14)
nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java (1)
62-62: Enum constant insertion may change ordinals; verify no ordinal-based logic/serializationAdding
GENERIC_INFERENCE_FAILUREmid-enum shifts ordinals of subsequent constants. If any code (internal or external) persists or relies on ordinals, this is a binary/source compatibility hazard. If ordinals are not used anywhere, we’re fine; otherwise, consider appending at the end or decoupling via explicit codes.nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java (1)
227-230: Consistent dummy behaviorThrowing
IllegalStateException(ERROR_MESSAGE)matches the rest of DummyOptionsConfig. LGTM.nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java (3)
105-107: Flag name addition looks goodConstant name and namespace are consistent with existing flags. No issues.
227-227: Field naming and placement are consistent
warnOnInferenceFailurefits existing style for boolean flags. LGTM.
605-609: New Config getter correctly exposes the flagMethod name matches the interface and returns the stored value. LGTM.
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java (2)
41-44: Clarify handling of “unknown” nullabilityThe result space is {NONNULL, NULLABLE}. Please document how unconstrained/unknown variables are reported (e.g., omitted from the map vs. defaulting to NONNULL). This affects callers’ defaults.
46-53: Document solver lifecycle and call semanticsPlease clarify:
- Is the solver single-use (no more constraints after
solve()), or can it be reused?- Is
solve()idempotent if called multiple times?- Thread-safety expectations.
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (7)
244-245: Updated diagnostic message is correct for the new solver behavior.Switching the expectation to “Cannot assign from type Foo to type Foo<@nullable Object>” accurately reflects the assignment-site incompatibility when
Uis fixed by the second parameter. Looks good.
710-714: Good coverage: explicit vs inferred type arguments on a type-variable-returning method.Both paths exercise the solver in the right way and mirror realistic usage patterns.
802-831: Varargs inference scenarios look solid.Good mix of: mixed-null argument lists, nullable producers in the argument list, and assignment-context-only (no-arg) inference. This should catch common regressions in constraint generation and solving for varargs.
880-901: Bound-respecting inference through Function<T,T> looks correct.This exercises both the success path and the failure when
Twould need to be non-null butnullis passed. Good signal for solver constraints on both parameter and return positions.
926-952: Array covariance vs component nullability — verify intended soundness tradeoff.This test encodes that
String[]is acceptable where@Nullable String[]is expected (viaT[]withT = @Nullable String). That aligns with Java’s array covariance, but it is potentially unsound w.r.t. storing nulls through theT[]view. If that relaxation is intentional for JSpecify-mode nullness, great; otherwise, we may want a negative test that tries to writenullthrough the parameter to ensure we don’t mask a latent store-safety issue.
954-987: RowMapper/Spring-inspired scenario reads well and validates no inference failures.Using the inference-failure warning flag here is fine since no failures are expected; this helps guard accidental regressions that would start warning. No changes needed.
989-1023: Optional.of vs ofNullable coverage is excellent.Great that you assert both the new “Failed to infer type argument nullability” path and the classic “passing @nullable parameter” path; this will keep messaging stable as the solver evolves.
This goes part way towards addressing #1075 and hopefully sets us up with a decent architecture for further inference improvements in the future. We introduce a basic constraint generation / constraint solver infrastructure for solving inference. The solver interface is
ConstraintSolverwith an initial implementation inConstraintSolverImpl. Subtype constraints are generated based on return and parameter flow at call sites, and nested calls requiring inference are also handled. We have several new tests, including a couple that don't work yet and should be handled in future PRs.This PR is unfortunately rather large. If desired, I could split it into one PR that introduces the constraint solver code and one that uses the constraint solver, but I'm not sure that would help too much with reviewability.
Summary by CodeRabbit