Skip to content

Improved inference for generic method calls#1244

Merged
msridhar merged 80 commits intomasterfrom
generics-type-constraint-solver
Aug 24, 2025
Merged

Improved inference for generic method calls#1244
msridhar merged 80 commits intomasterfrom
generics-type-constraint-solver

Conversation

@msridhar
Copy link
Copy Markdown
Collaborator

@msridhar msridhar commented Aug 13, 2025

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 ConstraintSolver with an initial implementation in ConstraintSolverImpl. 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

  • New Features
    • Adds solver-based nullability inference for generic method calls (including nested calls and arrays), improving accuracy without explicit type arguments.
    • Introduces a configurable warning when generic inference fails, along with a new diagnostic message type.
  • Bug Fixes
    • More precise diagnostics for mismatched generic nullability in assignments.
  • Tests
    • Extensive new test coverage for generic inference scenarios (varargs, arrays, optionals, mappers, reassignment), with some cases marked ignored pending support.
  • Chores
    • Configuration and CLI flag support for the new inference warning.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 check

Nice, 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 @nullable

When this test is eventually un-ignored, the assignment String x = null; will get flagged under @NullMarked. Declaring x as @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” path

The “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.

📥 Commits

Reviewing files that changed from the base of the PR and between 45aad64 and 0f6a460.

📒 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 paths

Adding 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 cases

This 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 appropriately

Clear, 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 point

Good 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 bounds

Having 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 all Diagnostic contains: assertions confirms every occurrence now uses “Cannot assign from type … to type …”. No tests remain with the old wording.

Copy link
Copy Markdown
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly questions as I swap all this into memory, and the occasional nit. Overall LGTM! 🙂

// this is a generic constructor call with no explicit type arguments
return true;
}
}*/
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the support for constructors commented out? If this is future work, maybe a TODO comment?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried improving the comments in 676b89c; hope it's clearer now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Captures means <?> here. Right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I clarified the comment in 8ad8e7b

Comment on lines +138 to +139
subtypeComponentType.accept(this, superComponentType);
superComponentType.accept(this, subtypeComponentType);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you are right, array subtyping is covariant! I just messed this up. Fixed with a test in 04f7bff

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😅 ...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're exactly right 😂 jspecify/jspecify#65 And the conclusion was that despite the extra unsoundness, covariant subtyping for arrays wins, to match Java.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very well then, LGTM!

Comment on lines +200 to +201
NullnessState n =
(st.nullness == NullnessState.UNKNOWN) ? NullnessState.NONNULL : st.nullness;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this redundant with the check below that will return InferredNullability.NONNULL for anything but NullnessState.NULLABLE?

Copy link
Copy Markdown
Collaborator Author

@msridhar msridhar Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right! Fixed in c9d7e08

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 brittleness

This 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 variant

This 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-up

The 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 inference

Great 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 issue

Since 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 tracking

Good 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 825285b and 893b22e.

📒 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 valuable

Covering 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 good

These 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 local

Here 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:

  1. If assignment is the error:
-    String s = applyId(f, null);
+    // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull
+    String s = applyId(f, null);
  1. 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 point

This accurately tests array covariance interacting with nullability inference and diagnostics. Looks good.

@msridhar
Copy link
Copy Markdown
Collaborator Author

Currently includes the changes in #1256. Land after #1256.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)

318-319: Same this leak issue as above.

This constructor has the same pattern of leaking this to GenericsChecks.

🧹 Nitpick comments (2)
nullaway/src/main/java/com/uber/nullaway/NullAway.java (1)

301-303: Potential issue with leaking this in constructor.

While the comment acknowledges the leak, passing this from a constructor to another object is still a code smell that could lead to issues if GenericsChecks tries to access uninitialized fields of NullAway. 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 this leak?

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 893b22e and 128a74f.

📒 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 Config to GenericsChecks is 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 @Nullable annotation type creation is well-documented with clear explanation of why ErrorType is 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 isGenericCallNeedingInference cleanly 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 LinkedHashSet maintains 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 allInvocations set effectively tracks all calls requiring inference.

Comment on lines 1105 to +1106
genericsChecks.getGenericReturnNullnessAtInvocation(
ASTHelpers.getSymbol(tree), tree, state, config);
ASTHelpers.getSymbol(tree), tree, state);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolver.java (1)

21-25: Unchecked exception rationale acknowledged

The explanatory comment for using RuntimeException is 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 compatibility

Adding 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 flag

Defaulting to false is 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

localVariableType is 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 expect TypeVariableSymbol. 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 null but 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 @Ignore only 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 field3 flows 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 result observable.

     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.

📥 Commits

Reviewing files that changed from the base of the PR and between 128a74f and ee2fdd6.

📒 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/serialization

Adding GENERIC_INFERENCE_FAILURE mid-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 behavior

Throwing 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 good

Constant name and namespace are consistent with existing flags. No issues.


227-227: Field naming and placement are consistent

warnOnInferenceFailure fits existing style for boolean flags. LGTM.


605-609: New Config getter correctly exposes the flag

Method 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” nullability

The 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 semantics

Please 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 U is 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 T would need to be non-null but null is 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 (via T[] with T = @Nullable String). That aligns with Java’s array covariance, but it is potentially unsound w.r.t. storing nulls through the T[] view. If that relaxation is intentional for JSpecify-mode nullness, great; otherwise, we may want a negative test that tries to write null through 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.

@msridhar msridhar merged commit cbc3b51 into master Aug 24, 2025
13 of 14 checks passed
@msridhar msridhar deleted the generics-type-constraint-solver branch August 24, 2025 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants