Skip to content

Initial inference for wildcards#1549

Merged
msridhar merged 27 commits into
masterfrom
wildcard-inference-experiment
May 3, 2026
Merged

Initial inference for wildcards#1549
msridhar merged 27 commits into
masterfrom
wildcard-inference-experiment

Conversation

@msridhar
Copy link
Copy Markdown
Collaborator

@msridhar msridhar commented Apr 26, 2026

This is an initial (incomplete) implementation of extending our generic method inference to handle wildcard types. It has known bugs and incompleteness (#1522, #1500, #1551, #1552), but we'll handle those in follow-up PRs to keep this one at a reasonable size. This support is also behind a flag and off by default.

The key changes here are in ConstraintSolverImpl. When constraining one class type C to be a subtype of another class type S, we now constrain their type arguments using "containment" (JLS 4.5.1 + the JSpecify spec) for wildcard type arguments. Containment has various cases depending on whether we have extends or super bounds. Also, when one type is constraint to be a subtype of a wildcard type, we use the wildcard's bound to generate the constraint.

Beyond the above:

  • We refactor some common logic between wildcard subtype checking and wildcard constraint generation into GenericsUtils
  • We extend a substitution case in TypeSubstitutionUtils to handle both extends and super bounds

See the new test cases in WildcardTests, including some that are ignored with follow-up issues. Beyond addressing those follow ups, I plan to also go through any new reports from this support in our integration test subjects (junit, spring-framework, caffeine) and check their validity in order to make the inference more robust.

Summary by CodeRabbit

  • New Features

    • Improved wildcard generics handling: effective upper-bound computation, wildcard-aware subtype/containment constraints, and broader restoration of nullability for both extends and super bounds—resulting in more precise nullability analysis and inference.
  • Tests

    • Re-enabled and added wildcard inference, method-reference, lambda, and stream mapping tests; introduced active and regression cases to validate wildcard/nullability behaviors.

@msridhar msridhar changed the base branch from master to wildcard-opposite-bounds April 26, 2026 15:14
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 80.45977% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.45%. Comparing base (162b2a8) to head (272c447).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...m/uber/nullaway/generics/ConstraintSolverImpl.java 77.96% 3 Missing and 10 partials ⚠️
...java/com/uber/nullaway/generics/GenericsUtils.java 83.33% 1 Missing and 2 partials ⚠️
.../uber/nullaway/generics/TypeSubstitutionUtils.java 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1549      +/-   ##
============================================
- Coverage     88.52%   88.45%   -0.07%     
- Complexity     2862     2869       +7     
============================================
  Files           103      103              
  Lines          9522     9586      +64     
  Branches       1911     1930      +19     
============================================
+ Hits           8429     8479      +50     
- Misses          528      532       +4     
- Partials        565      575      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@msridhar msridhar force-pushed the wildcard-opposite-bounds branch from 627c9d2 to ef71fef Compare April 26, 2026 19:35
@msridhar msridhar force-pushed the wildcard-inference-experiment branch from 8a72414 to a1d6652 Compare April 26, 2026 19:37
Base automatically changed from wildcard-opposite-bounds to master April 26, 2026 21:01
@msridhar msridhar force-pushed the wildcard-inference-experiment branch from aa9dcd2 to 493301a Compare April 26, 2026 21:02
@msridhar msridhar force-pushed the wildcard-inference-experiment branch from 4771d4c to 46e2b25 Compare April 27, 2026 05:22
@msridhar msridhar changed the title [draft] some wildcard inference Initial inference for wildcards Apr 27, 2026
@msridhar msridhar marked this pull request as ready for review April 27, 2026 18:11
@msridhar msridhar requested a review from lazaroclapp April 27, 2026 18:11
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Walkthrough

Refactors wildcard upper-bound extraction into new GenericsUtils helpers and removes the visitor-local helper. Updates CheckIdenticalNullabilityVisitor to use those helpers and to normalize RHS wildcards when comparing extends bounds. Extends ConstraintSolverImpl to generate containment-specific nullability constraints for type arguments (including wildcard↔wildcard and wildcard↔concrete relations) and adds a visitWildcardType path. Broadens TypeSubstitutionUtils to restore nullability from both EXTENDS and SUPER wildcard bounds. Adds and re-enables several JSpecify wildcard-related tests.

Possibly related PRs

Suggested labels

jspecify

Suggested reviewers

  • lazaroclapp
  • yuxincs
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Initial inference for wildcards' directly and clearly describes the main objective of the PR, which implements initial support for wildcard type inference in generic method constraints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wildcard-inference-experiment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java (1)

386-395: ⚠️ Potential issue | 🟡 Minor

Verify that capture symbols in the inferred nullability map won't cause inefficiencies.

Removing the CapturedType exclusion means captured wildcards now create inference variables and their synthetic capture symbols end up as keys in the solution map. While downstream consumers like TypeSubstitutionUtils.substituteInferredNullabilityForTypeVariables safely handle these via symbol matching (capture symbols won't match any declared type parameters), having entries for symbols that never substitute anything adds unnecessary overhead. Consider whether filtering the result map to exclude non-type-parameter symbols would improve efficiency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`
around lines 386 - 395, The inferred-nullability map currently retains entries
for synthetic capture symbols (CapturedType) which never correspond to declared
type parameters; after building the map in ConstraintSolverImpl (the place that
previously relied on isTypeVariable), filter the map to only keep keys that are
actual declared type-parameter symbols (e.g., check symbol.getKind() ==
ElementKind.TYPE_PARAMETER or otherwise confirm the Symbol represents a declared
type parameter) before returning/using it so downstream code like
TypeSubstitutionUtils.substituteInferredNullabilityForTypeVariables doesn't
carry useless capture-symbol entries; update the map-construction/return site to
drop any symbol that is clearly a capture/non-type-parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 162-168: The visitWildcardType method currently always calls
constrainWildcardToSupertype(subtype, supertype) without checking the
localVariableType flag like visitType does; mirror visitType’s behavior by
adding the same Verify.verify(!localVariableType, "...") guard at the start of
visitWildcardType (or, if intended to allow wildcards for localVariableType, add
a clear comment and an explicit branch explaining why) so that entering
visitWildcardType via addSubtypeConstraint(..., true) is caught or intentionally
handled; reference visitWildcardType, visitType, localVariableType,
constrainWildcardToSupertype, and addSubtypeConstraint when making the change.
- Around line 209-228: In constrainContainedByWildcard, when supertypeWildcard
is SUPER and the actual subtypeTypeArg is a wildcard that is not BoundKind.SUPER
the code intentionally performs no constraint (the branch falls through) but
this silent no-op should be documented; add an inline comment inside the SUPER
case (near variables supertypeLowerBound, subtypeWildcard and the conditional
checking subtypeWildcard.kind == BoundKind.SUPER) explaining that containment
for formals of the shape '? super S' with actuals '? extends X' or unbound is
illegal in source Java, is reachable during inference, and is intentionally
elided here (mirroring the comment pattern in
CheckIdenticalNullabilityVisitor.superWildcardContains) so future inference work
will revisit it.
- Around line 261-267: The current constrainWildcardToSupertype method
over-constrains in the SUPER branch by calling
castToNonNull(subtypeWildcard.getSuperBound()).accept(this, supertype), which
effectively forces S <: T for "? super S <: T" (an unsound constraint w.r.t.
lower-bound extraction); change the SUPER branch to avoid deriving a constraint
on the super-bound (i.e., do not call castToNonNull(...).accept(...)) — either
make it a no-op or only add a conservative comment explaining the choice and
refer to issue `#1551` for follow-up; keep the existing non-SUPER behavior that
uses GenericsUtils.wildcardUpperBound(...).accept(this, supertype) and preserve
function names constrainWildcardToSupertype, castToNonNull, and
GenericsUtils.wildcardUpperBound in the diff for easy location.

---

Outside diff comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 386-395: The inferred-nullability map currently retains entries
for synthetic capture symbols (CapturedType) which never correspond to declared
type parameters; after building the map in ConstraintSolverImpl (the place that
previously relied on isTypeVariable), filter the map to only keep keys that are
actual declared type-parameter symbols (e.g., check symbol.getKind() ==
ElementKind.TYPE_PARAMETER or otherwise confirm the Symbol represents a declared
type parameter) before returning/using it so downstream code like
TypeSubstitutionUtils.substituteInferredNullabilityForTypeVariables doesn't
carry useless capture-symbol entries; update the map-construction/return site to
drop any symbol that is clearly a capture/non-type-parameter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a96d964c-0e65-4b6b-8fc2-341ef6a0ade8

📥 Commits

Reviewing files that changed from the base of the PR and between 2331d6f and de2e4c3.

📒 Files selected for processing (5)
  • nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java
  • nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java
  • nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 162-168: The override visitWildcardType currently returns early
when config.handleWildcardGenerics() is false, which drops the previous behavior
of delegating to the generic visitor and breaks equateTypeArguments for wildcard
subtype arguments; change visitWildcardType in ConstraintSolverImpl so that when
handleWildcardGenerics() is true it performs the existing Verify and
constrainWildcardToSupertype(...) logic, and otherwise it delegates to the
superclass/generic path by calling visitType(subtype, supertype) (preserving
localVariableType handling by not bypassing it), ensuring equateTypeArguments
behavior is unchanged when the flag is off.
- Around line 182-189: The code in ConstraintSolverImpl currently calls
GenericsUtils.asWildcard on subtype/supertype type args which unwraps
CapturedType back to its source wildcard and loses the captured variable
identity; change the logic to detect captured types (after isTypeVariable
expanded) and handle them in a dedicated branch instead of calling
GenericsUtils.asWildcard: when you see a CapturedType preserve the capture
variable and generate constraints using that captured variable (i.e., add
constraints involving the capture variable directly rather than delegating to
constrainContainedByWildcard or constrainWildcardContainedByConcrete), only
falling back to asWildcard for true wildcards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25858af5-6e3f-42e4-a641-d366ab4923b1

📥 Commits

Reviewing files that changed from the base of the PR and between de2e4c3 and 8a2f2aa.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java

Comment thread nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java Outdated
@msridhar msridhar force-pushed the wildcard-inference-experiment branch from 8a2f2aa to 2ebfce6 Compare April 28, 2026 19:46
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.

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

163-168: ⚠️ Potential issue | 🟠 Major

Preserve the pre-flag path in visitWildcardType().

Line 168 returns null when handleWildcardGenerics() is disabled, so wildcard subtype arguments stop delegating to the generic visitor path. That changes equateTypeArguments(...) behavior even though the feature is off by default. Please fall back to visitType(subtype, supertype) in the disabled branch.

Suggested fix
     `@Override`
     public `@Nullable` Void visitWildcardType(WildcardType subtype, Type supertype) {
-      if (config.handleWildcardGenerics()) {
-        Verify.verify(!localVariableType, "A wildcard type cannot be assigned to a local variable");
-        constrainWildcardToSupertype(subtype, supertype);
-      }
-      return null;
+      if (!config.handleWildcardGenerics()) {
+        return visitType(subtype, supertype);
+      }
+      Verify.verify(!localVariableType, "A wildcard type cannot be assigned to a local variable");
+      constrainWildcardToSupertype(subtype, supertype);
+      return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`
around lines 163 - 168, In visitWildcardType(WildcardType subtype, Type
supertype) ensure the disabled-flag path preserves the original delegation: when
config.handleWildcardGenerics() is false, call visitType(subtype, supertype)
instead of simply returning null so wildcard subtype arguments continue to
follow the generic visitor path (preserving behavior used by
equateTypeArguments); keep the existing branch that constrains wildcards when
the flag is true (constrainWildcardToSupertype) and maintain the Verify on
localVariableType.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 163-168: In visitWildcardType(WildcardType subtype, Type
supertype) ensure the disabled-flag path preserves the original delegation: when
config.handleWildcardGenerics() is false, call visitType(subtype, supertype)
instead of simply returning null so wildcard subtype arguments continue to
follow the generic visitor path (preserving behavior used by
equateTypeArguments); keep the existing branch that constrains wildcards when
the flag is true (constrainWildcardToSupertype) and maintain the Verify on
localVariableType.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea3386c2-b00e-4ab5-bfd8-dce0ce7b9a64

📥 Commits

Reviewing files that changed from the base of the PR and between 8a2f2aa and 2ebfce6.

📒 Files selected for processing (5)
  • nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java
  • nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java
  • nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 392-397: The change admits captured TypeVar instances into the
solver but they lack wildcard-aware nullability handling; revert to excluding
captures until handled properly by restoring the !tv.isCaptured() guard in
isTypeVariable(Type) OR, if you prefer to accept captures now, derive their
nullability from the originating wildcard using GenericsUtils.asWildcard(...)
when computing upperBoundIsNullable and in constrainTypeArgumentContainment()
(and ensure behavior respects handleWildcardGenerics flag) so that captures from
? super `@Nullable` T are treated nullable rather than using the raw upper bound;
update references to tv.isCaptured(), isTypeVariable, upperBoundIsNullable, and
constrainTypeArgumentContainment accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57def0bb-f64d-4872-afb1-6ce58d31babe

📥 Commits

Reviewing files that changed from the base of the PR and between 2ebfce6 and 2dcd522.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java

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.

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

390-395: ⚠️ Potential issue | 🟠 Major

Captured ? super @nullable ... variables are still modeled as non-nullable solver vars.

Removing the capture filter in isTypeVariable() admits captured wildcards into the solver, but nullableAllowed is still derived from the capture's raw upper bound. For a capture of ? super @nullable Foo, that upper bound is typically non-null, so a valid inference path can now fail with UnsatisfiableConstraintsException when nullability propagates onto the capture. This regresses behavior even with handleWildcardGenerics() off.

Either restore the old !tv.isCaptured() guard for now, or derive capture nullability from the source wildcard (for example via GenericsUtils.asWildcard(...)) instead of only from the raw upper bound.

Also applies to: 411-439

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`
around lines 390 - 395, The bug is that captured wildcards are being admitted as
solver type variables without preserving their source nullability, causing e.g.
? super `@Nullable` Foo to be treated non-nullable and leading to
UnsatisfiableConstraintsException; fix by either restoring the previous capture
guard in isTypeVariable (i.e., short-circuit with tv.isCaptured()) or, if you
want captures handled, derive the capture's nullableAllowed from the original
wildcard (use GenericsUtils.asWildcard(...) or similar to inspect the source
wildcard annotations) rather than the raw upper bound; ensure logic in
isTypeVariable and any code computing nullableAllowed (referencing tv,
isTypeVariable, nullableAllowed, handleWildcardGenerics, and
UnsatisfiableConstraintsException) consistently respects capture source
nullability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java`:
- Around line 390-395: The bug is that captured wildcards are being admitted as
solver type variables without preserving their source nullability, causing e.g.
? super `@Nullable` Foo to be treated non-nullable and leading to
UnsatisfiableConstraintsException; fix by either restoring the previous capture
guard in isTypeVariable (i.e., short-circuit with tv.isCaptured()) or, if you
want captures handled, derive the capture's nullableAllowed from the original
wildcard (use GenericsUtils.asWildcard(...) or similar to inspect the source
wildcard annotations) rather than the raw upper bound; ensure logic in
isTypeVariable and any code computing nullableAllowed (referencing tv,
isTypeVariable, nullableAllowed, handleWildcardGenerics, and
UnsatisfiableConstraintsException) consistently respects capture source
nullability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: be0067ae-94fe-4d36-8d59-0f00789e17de

📥 Commits

Reviewing files that changed from the base of the PR and between 2dcd522 and ea94de5.

📒 Files selected for processing (1)
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java

@msridhar msridhar force-pushed the wildcard-inference-experiment branch from ea94de5 to 2f392ee Compare April 29, 2026 21:36
@msridhar msridhar force-pushed the wildcard-inference-experiment branch from 2f392ee to adb858c Compare May 1, 2026 15:09
@msridhar
Copy link
Copy Markdown
Collaborator Author

msridhar commented May 1, 2026

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 looks good to me, but there is some code duplication, I think

upperBound =
wildcardType.bound == null
? Symtab.instance(state.context).objectType
: wildcardType.bound.getUpperBound();
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.

Does this API not work with wildcardType.kind == BoundKind.EXTENDS bounded wildcards too? I thought upper bound was defined to include the type on the RHS of the extends, but not super certain about the API here.

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.

There are some docs from the previous version in CheckIdenticalNullabilityVisitor.java‎ that seem lost here, but even those don't explain if wildcardType.bound.getUpperBound() would produce something different for ? extends ....

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've added some comments to explain here. The confusing thing is that bound holds the type variable to which the wildcard is being passed, not a bound on the wildcard type...

* {@code S <: supertypeTypeArg}. For {@code ? super S}, use the lower bound and require {@code
* S <: supertypeTypeArg}.
*/
private void constrainWildcardContainedByConcrete(
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 method and constrainWildcardToSupertype are identical, modulo the renaming of their second argument. If we don't expect this to change dramatically in the near future, they should probably be the same method?

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! I kept constrainWildcardToSupertype

@msridhar msridhar requested a review from lazaroclapp May 3, 2026 01:26
@msridhar
Copy link
Copy Markdown
Collaborator Author

msridhar commented May 3, 2026

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Reviews resumed.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java`:
- Around line 296-304: The current handling of Type.WildcardType in
TypeSubstitutionUtils ignores BoundKind.UNBOUND (plain '?'); update the wildcard
branch to explicitly handle BoundKind.UNBOUND by extracting the formal type
parameter's upper bound (as GenericsUtils.wildcardUpperBound(wt) or
wt.bound.getUpperBound()) and then call visit(t, thatUpperBound) just like the
EXTENDS/SUPER cases; modify the code around the Type.WildcardType branch to add
an else-if for wt.kind == BoundKind.UNBOUND that delegates to visit(t,
<upperBound>) so UNBOUND wildcards restore nullability annotations consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a428160-4866-457c-859a-1367261bace8

📥 Commits

Reviewing files that changed from the base of the PR and between 2dcd522 and 272c447.

📒 Files selected for processing (5)
  • nullaway/src/main/java/com/uber/nullaway/generics/CheckIdenticalNullabilityVisitor.java
  • nullaway/src/main/java/com/uber/nullaway/generics/ConstraintSolverImpl.java
  • nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java
  • nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java
  • nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

Comment on lines +296 to 304
if (other instanceof Type.WildcardType wt) {
// When the other type is a wildcard, restore nullability annotations from the bound that
// determines the functional interface type.
if (wt.kind == BoundKind.EXTENDS) {
return visit(t, wt.getExtendsBound());
} else if (wt.kind == BoundKind.SUPER) {
return visit(t, wt.getSuperBound());
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 3, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are test cases covering UNBOUND wildcards flowing through this visitor
rg -n --type=java 'visitClassType.*Wildcard|UNBOUND' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java
rg -n --type=java '\<\?\>' nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java | head -20

Repository: uber/NullAway

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Read the TypeSubstitutionUtils.java file to see the full context around lines 296-304
wc -l nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Read the relevant section of TypeSubstitutionUtils.java
sed -n '290,315p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 1129


🏁 Script executed:

#!/bin/bash
# Find GenericsUtils.java and check the wildcardUpperBound method
fd -n GenericsUtils.java nullaway/src/main/java

Repository: uber/NullAway

Length of output: 288


🏁 Script executed:

#!/bin/bash
# Search for UNBOUND keyword in the codebase
rg -n "BoundKind.UNBOUND" nullaway/src/main/java/

Repository: uber/NullAway

Length of output: 187


🏁 Script executed:

#!/bin/bash
# Search for test cases related to wildcards
fd -n "WildcardTests.java" nullaway/src/test/java

Repository: uber/NullAway

Length of output: 288


🏁 Script executed:

#!/bin/bash
# Find GenericsUtils.java
find nullaway/src/main/java -name "GenericsUtils.java"

Repository: uber/NullAway

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Find WildcardTests.java
find nullaway/src/test/java -name "WildcardTests.java"

Repository: uber/NullAway

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Search for wildcardUpperBound method
rg -n "wildcardUpperBound" nullaway/src/main/java/

Repository: uber/NullAway

Length of output: 1483


🏁 Script executed:

#!/bin/bash
# Search for how UNBOUND wildcards are handled in generics code
rg -n "BoundKind\|UNBOUND" nullaway/src/main/java/com/uber/nullaway/generics/ -A 2 -B 2

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Read the GenericsUtils.java file to understand wildcardUpperBound
cat -n nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java | head -80

Repository: uber/NullAway

Length of output: 3711


🏁 Script executed:

#!/bin/bash
# Search for UNBOUND handling patterns
rg -n "UNBOUND" nullaway/src/main/java/com/uber/nullaway/generics/

Repository: uber/NullAway

Length of output: 425


🏁 Script executed:

#!/bin/bash
# Check WildcardTests.java for test coverage of UNBOUND wildcards
cat -n nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java | head -100

Repository: uber/NullAway

Length of output: 4631


🏁 Script executed:

#!/bin/bash
# Search for similar wildcard handling patterns in other visitors
rg -n "instanceof.*WildcardType" nullaway/src/main/java/com/uber/nullaway/generics/ -A 5 -B 1

Repository: uber/NullAway

Length of output: 5547


🏁 Script executed:

#!/bin/bash
# Check if visitClassType is called with UNBOUND wildcards in practice
rg -n "visitClassType" nullaway/src/main/java/com/uber/nullaway/generics/ -B 2 -A 2

Repository: uber/NullAway

Length of output: 3135


🏁 Script executed:

#!/bin/bash
# Read more of TypeSubstitutionUtils.java to understand the context and purpose
sed -n '260,320p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 2698


🏁 Script executed:

#!/bin/bash
# Search for test cases with unbounded wildcards (just "?")
rg -n '\?\>' nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

Repository: uber/NullAway

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Check if there are any tests with super wildcards
rg -n '\? super' nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

Repository: uber/NullAway

Length of output: 2881


🏁 Script executed:

#!/bin/bash
# Search for any existing UNBOUND handling or comments about it
rg -n "UNBOUND" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if bound field can be null for WildcardType
rg -n "wildcardType.bound|wt.bound" nullaway/src/main/java/com/uber/nullaway/generics/ -B 2 -A 2

Repository: uber/NullAway

Length of output: 674


🏁 Script executed:

#!/bin/bash
# Look at the visitWildcardType method in TypeSubstitutionUtils to see how it handles wildcards
sed -n '321,345p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 830


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or documentation about why UNBOUND is not handled
sed -n '290,310p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java | cat -n

Repository: uber/NullAway

Length of output: 1046


🏁 Script executed:

#!/bin/bash
# Verify if wt.bound can be null and check the null handling in GenericsUtils pattern
rg -n "bound == null|bound != null" nullaway/src/main/java/com/uber/nullaway/generics/GenericsUtils.java

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if there are any other visitors that handle UNBOUND explicitly
rg -n "BoundKind.UNBOUND" nullaway/src/main/java/com/uber/nullaway/generics/ -B 2 -A 2

Repository: uber/NullAway

Length of output: 668


🏁 Script executed:

#!/bin/bash
# Check if there are tests for unbounded wildcards by looking for patterns like Foo<?>
rg -n 'Foo<.*>' nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java | grep -v extends | grep -v super | head -20

Repository: uber/NullAway

Length of output: 1458


🏁 Script executed:

#!/bin/bash
# Check if the code actually processes UNBOUND wildcards and what happens
rg -n "RestoreNullnessAnnotationsVisitor\|visitClassType" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java -B 5 -A 15 | head -60

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Verify what updateDirectNullabilityAnnotationsForType does with WildcardType
rg -n "updateDirectNullabilityAnnotationsForType" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java -A 5 | head -40

Repository: uber/NullAway

Length of output: 1206


🏁 Script executed:

#!/bin/bash
# Read the updateDirectNullabilityAnnotationsForType method fully
sed -n '362,390p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 1261


🏁 Script executed:

#!/bin/bash
# Check the git history or comments around this code
git log --oneline -20 -- nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Look for any test that specifically checks UNBOUND wildcard behavior
rg -n "unboundedFoo\|Foo<\?>" nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java -B 3 -A 8

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check what the actual test expectations are for unbounded wildcards
sed -n '270,320p' nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java

Repository: uber/NullAway

Length of output: 2626


🏁 Script executed:

#!/bin/bash
# Look at the context around line 275 test with unbounded foo
rg -n "unboundedFoo" nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java -B 5 -A 15

Repository: uber/NullAway

Length of output: 1671


🏁 Script executed:

#!/bin/bash
# Check if bound field is populated for UNBOUND wildcards
rg -n "\.bound\s*=" nullaway/src/main/java/com/uber/nullaway/generics/ -B 2 -A 2

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check the commit context for the RestoreNullnessAnnotationsVisitor code
git show 272c447 --stat

Repository: uber/NullAway

Length of output: 27203


🏁 Script executed:

#!/bin/bash
# Search for usages of RestoreNullnessAnnotationsVisitor to understand when it's invoked
rg -n "RestoreNullnessAnnotationsVisitor\|new RestoreNullnessAnnotationsVisitor" nullaway/src/main/java/ -B 2 -A 2

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Check if there's a way for UNBOUND to be processed through the formal type parameter bound
rg -n "wt.bound\|wildcardType.bound" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 39


🏁 Script executed:

#!/bin/bash
# Look at the broader context of how RestoreNullnessAnnotationsVisitor is used
rg -n "new RestoreNullnessAnnotationsVisitor\|RestoreNullnessAnnotationsVisitor(" nullaway/src/main/java/com/uber/nullaway/generics/ -B 3 -A 3

Repository: uber/NullAway

Length of output: 254


🏁 Script executed:

#!/bin/bash
# Look at the broader context of how RestoreNullnessAnnotationsVisitor is used
rg -n "RestoreNullnessAnnotationsVisitor" nullaway/src/main/java/com/uber/nullaway/generics/ -B 3 -A 3

Repository: uber/NullAway

Length of output: 2508


🏁 Script executed:

#!/bin/bash
# Check what happens in the UNBOUND case - does it fall through intentionally or by omission?
# Look for any comments or documentation about wildcard handling
sed -n '290,340p' nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java

Repository: uber/NullAway

Length of output: 1782


Handle UNBOUND wildcards by extracting the formal type parameter's upper bound.

The code explicitly extracts bounds for BoundKind.EXTENDS and BoundKind.SUPER, but BoundKind.UNBOUND (i.e., ?) falls through without extraction. This is inconsistent with GenericsUtils.wildcardUpperBound, which uses wt.bound.getUpperBound() for both SUPER and UNBOUND wildcards to access the formal type parameter's upper bound.

For UNBOUND wildcards, consider handling the case explicitly:

Suggested approach
      if (other instanceof Type.WildcardType wt) {
        // When the other type is a wildcard, restore nullability annotations from the bound that
        // determines the functional interface type.
        if (wt.kind == BoundKind.EXTENDS) {
          return visit(t, wt.getExtendsBound());
        } else if (wt.kind == BoundKind.SUPER) {
          return visit(t, wt.getSuperBound());
+       } else if (wt.kind == BoundKind.UNBOUND && wt.bound != null) {
+         return visit(t, wt.bound.getUpperBound());
+       }
      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java`
around lines 296 - 304, The current handling of Type.WildcardType in
TypeSubstitutionUtils ignores BoundKind.UNBOUND (plain '?'); update the wildcard
branch to explicitly handle BoundKind.UNBOUND by extracting the formal type
parameter's upper bound (as GenericsUtils.wildcardUpperBound(wt) or
wt.bound.getUpperBound()) and then call visit(t, thatUpperBound) just like the
EXTENDS/SUPER cases; modify the code around the Type.WildcardType branch to add
an else-if for wt.kind == BoundKind.UNBOUND that delegates to visit(t,
<upperBound>) so UNBOUND wildcards restore nullability annotations consistently.

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.

There cannot be explicit nullability annotations for the unbound case

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.

@msridhar, thanks for the clarification! That makes sense — an unbound wildcard ? carries no explicit nullability annotation, so there's nothing to restore for that case, and the fall-through to updateDirectNullabilityAnnotationsForType is intentionally correct. I was wrong to suggest handling BoundKind.UNBOUND there.


✏️ Learnings added
Learnt from: msridhar
Repo: uber/NullAway PR: 1549
File: nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java:296-304
Timestamp: 2026-05-03T17:32:46.673Z
Learning: In uber/NullAway, UNBOUND wildcards (`?`) cannot carry explicit nullability annotations (you cannot write `? Nullable`). Therefore, in `RestoreNullnessAnnotationsVisitor.visitClassType` in `TypeSubstitutionUtils.java`, it is correct and intentional for the `BoundKind.UNBOUND` case to fall through without extracting a bound — there is nothing to restore. Only `BoundKind.EXTENDS` and `BoundKind.SUPER` need explicit bound extraction for nullability annotation restoration.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: msridhar
Repo: uber/NullAway PR: 1485
File: nullaway/src/main/java/com/uber/nullaway/NullAway.java:2119-2128
Timestamp: 2026-03-20T15:34:10.657Z
Learning: In this repo (uber/NullAway), don’t annotate local variables with `Nullable` or `NonNull`. NullAway infers nullability for local variables automatically, so adding these annotations to locals is incorrect style for this codebase.

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.

Makes sense. LGTM!

@msridhar msridhar merged commit 29a896a into master May 3, 2026
12 of 22 checks passed
@msridhar msridhar deleted the wildcard-inference-experiment branch May 3, 2026 19:10
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