Bug fix with type substitutions after inference#1277
Conversation
WalkthroughCollect all occurrences of a type variable (by symbol) during nullability substitution, substitute each occurrence with a synthetic @nullable type cached on the GenericsChecks instance, add a TypeVarWithSymbolCollector visitor to find matches, and remove a test suppression/comment in a JSpecify test. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Caller as Caller
participant GC as GenericsChecks
participant TVC as TypeVarWithSymbolCollector
Caller->>GC: substituteInferredNullabilityForTypeVariables(targetType, symbol, inferredMap)
GC->>TVC: new TypeVarWithSymbolCollector(symbol)
GC->>TVC: TVC.scan(targetType)
TVC-->>GC: matches (Set<TypeVar>)
alt no matches
GC-->>Caller: return targetType
else matches found
GC->>GC: build typeVarsToReplace & inferredTypes (use getSyntheticNullAnnotType(state))
GC->>GC: apply TypeSubstitutionUtils.subst to produce substitutedType
GC-->>Caller: return substitutedType
end
note over GC: synthetic @Nullable Type is lazily cached on GC instance
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Assessment against linked issues
Possibly related PRs
Suggested reviewers
✨ Finishing Touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java (2)
47-53: Reference equality is intended; suppress the warning locally.t.tsym == symbol is correct in javac land; add suppression to avoid static-analysis churn.
@@ - @Override - public Void visitTypeVar(TypeVar t, Void p) { + @Override + @SuppressWarnings("ReferenceEquality") + public Void visitTypeVar(TypeVar t, Void p) {
58-65: Optional: consider explicit handling of IntersectionClassType.Bounds like Object & Comparable are IntersectionClassType; current visitClassType only scans type args/enclosing type. Not strictly required for substitution, but handling components would make the collector more robust.
📜 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/GenericsChecks.java(1 hunks)nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java(1 hunks)nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
Applied to files:
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javanullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
📚 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
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
TypeSubstitutionUtils(22-287)
⏰ 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). (1)
- GitHub Check: Build and test on windows-latest
🔇 Additional comments (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
660-671: Good: substitute by symbol across all matching TypeVars.Collecting all TypeVar occurrences by tsym and substituting them fixes the identity-mismatch bug described in #1267.
nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java (1)
1113-1113: LGTM: assertion comment updated to reflect inference now working.The Caffeine-inspired case documents the behavior without suppressions.
nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
660-671: Solid fix: symbol-based TypeVar substitution; consider tiny perf tweak.Current approach correctly substitutes all occurrences matching the same symbol, fixing the identity mismatch bug. To avoid repeated creation of the synthetic annotation type, cache it once per method call.
for (Map.Entry<Element, ConstraintSolver.InferredNullability> entry : typeVarNullability.entrySet()) { if (entry.getValue() == NULLABLE) { + // Create once per method invocation + Type syntheticNullAnnot = GenericsChecks.getSyntheticNullAnnotType(state); // find all TypeVars occurring in targetType with the same symbol and substitute for those. // we can have multiple such TypeVars due to previous substitutions that modified the type // in some way, e.g., by changing its bounds Element symbol = entry.getKey(); TypeVarWithSymbolCollector tvc = new TypeVarWithSymbolCollector(symbol); targetType.accept(tvc, null); for (Type.TypeVar tv : tvc.getMatches()) { typeVars.append(tv); inferredTypes.append( - TypeSubstitutionUtils.typeWithAnnot( - tv, GenericsChecks.getSyntheticNullAnnotType(state))); + TypeSubstitutionUtils.typeWithAnnot(tv, syntheticNullAnnot)); } } }
📜 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/main/java/com/uber/nullaway/generics/GenericsChecks.java(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
Applied to files:
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
TypeSubstitutionUtils(22-287)
⏰ 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). (4)
- GitHub Check: Build and test on macos-latest
- GitHub Check: Build and test on windows-latest
- GitHub Check: Build and test on ubuntu-latest
- GitHub Check: Build caffeine with snapshot
🔇 Additional comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
674-680: Return original type when no substitutions — good fix.This resolves the earlier compile-time missing-return issue and preserves identity when no changes are needed.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1277 +/- ##
============================================
- Coverage 88.55% 88.44% -0.12%
- Complexity 2431 2448 +17
============================================
Files 91 92 +1
Lines 8022 8075 +53
Branches 1598 1603 +5
============================================
+ Hits 7104 7142 +38
- Misses 459 472 +13
- Partials 459 461 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
673-679: Good: safe fallback when no substitutions are needed.Returning
targetTypeif nothing to replace addresses the prior compile error.
🧹 Nitpick comments (6)
nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java (3)
36-41: Add a public entrypoint that resets the seen set and uses the guard from the root.Currently callers do
root.accept(this, null), which bypasses the guard for the root node and keepsseenacross multiple traversals. Provide an entrypoint that clearsseenand starts traversal viascan.Apply:
@@ - /** Walk a (possibly null) type. Safe to call multiple times on different roots. */ + /** Walk a (possibly null) type. Internal recursive step. */ private void scan(@Nullable Type t) { if (t != null && seen.add(t)) { t.accept(this, null); } } + + /** Public entrypoint: traverse from root, resetting cycle guard each time. */ + public void collectFrom(@Nullable Type root) { + seen.clear(); + scan(root); + }
12-16: Minor import cleanup.Prefer importing
IdentityHashMapand using the already-importedCollectionsfor readability.Apply:
@@ -import java.util.Collections; +import java.util.Collections; +import java.util.IdentityHashMap; @@ - private final Set<Type> seen = - java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); + private final Set<Type> seen = Collections.newSetFromMap(new IdentityHashMap<>());
114-121: Consider explicit handling for union/intersection types.If
UnionTypeor intersection types appear in bounds, relying ongetTypeArguments()may miss components depending on javac version. Add explicit overrides to visit union/intersection components if needed.If helpful, I can draft the exact overrides based on the javac version you target.
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (3)
664-666: Use the collector’s entrypoint to leverage the cycle guard and reset semantics.After adding
collectFrom(...), prefer it overaccept(...).Apply:
- targetType.accept(tvc, null); + tvc.collectFrom(targetType);
651-679: Avoid N traversals over the same target type (micro-optimization).You currently traverse
targetTypeonce per NULLABLE entry. For large signatures, consider a single pass that collects matches for all NULLABLE symbols (e.g., a collector that takes aSet<Element>), then build the parallel lists. Low priority.I can provide a single-pass collector if you’d like.
1451-1476: Narrow getSyntheticNullAnnotType visibility to privateThe method is only invoked internally (at line 669) and there are no external or static call sites—making it private is safe. Apply:
- public Type getSyntheticNullAnnotType(VisitorState state) { + private Type getSyntheticNullAnnotType(VisitorState state) {
📜 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 (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java(3 hunks)nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
Applied to files:
nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.javanullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
🔇 Additional comments (3)
nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java (2)
33-41: Cycle guard looks good; prevents recursion on self-referential bounds.Using an IdentityHashSet via
seen.add(t)avoids infinite recursion and duplicate visits. LGTM.
48-57: Matching by symbol is correct.Comparing
t.tsymby identity against the providedElementaligns with javac symbol semantics and fixes the identity-mismatch issue. LGTM.nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
660-671: Robust substitution by symbol; resolves post-inference TypeVar identity drift.Collecting all
TypeVars by symbol and substituting each occurrence fixes the mismatch. LGTM.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
673-679: Return original type when no replacements — fix looks goodThe early return prevents the prior compile-time “missing return” issue.
🧹 Nitpick comments (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (2)
657-666: Minor perf: traverse targetType once for all NULLABLE symbolsYou currently walk targetType once per NULLABLE entry. Consider a single-pass collector that accepts a Set and returns a Set for all matches to reduce repeated traversals. Low-impact, but cleaner under many type vars.
1451-1452: Make cached field volatile to avoid rare visibility racesNot strictly needed here, but volatile is a cheap guard if analyses ever parallelize.
- private @Nullable Type syntheticNullAnnotType; + private volatile @Nullable Type syntheticNullAnnotType;
📜 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 (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java(3 hunks)nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- nullaway/src/main/java/com/uber/nullaway/generics/TypeVarWithSymbolCollector.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
📚 Learning: 2025-08-28T04:54:20.953Z
Learnt from: msridhar
PR: uber/NullAway#1248
File: nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java:847-857
Timestamp: 2025-08-28T04:54:20.953Z
Learning: In NullAway's GenericsChecks.java, NewClassTree support for explicit type argument substitution requires more extensive changes beyond just modifying the conditional in compareGenericTypeParameterNullabilityForCall. The maintainers prefer to handle NewClassTree support in a separate follow-up rather than expanding the scope of PRs focused on specific issues like super constructor calls.
Applied to files:
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
🧬 Code graph analysis (1)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (1)
nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java (1)
TypeSubstitutionUtils(22-287)
⏰ 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). (5)
- GitHub Check: Build spring-framework with snapshot
- GitHub Check: Build and test on macos-latest
- GitHub Check: Build and test on windows-latest
- GitHub Check: Build caffeine with snapshot
- GitHub Check: Build and test on ubuntu-latest
🔇 Additional comments (3)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java (3)
660-671: Symbol-based multi-occurrence substitution fixes inference mismatch — LGTMCollecting all TypeVars by symbol and substituting each occurrence addresses the object-identity pitfall and aligns with the PR objective.
1466-1476: Instance-cached synthetic @nullable type — LGTMLazily creating and caching an ErrorType in a synthetic package is reasonable and avoids repeated allocations.
1466-1476: No stale static calls found. ConfirmedgetSyntheticNullAnnotTypehas no remaining static invocations or external callers after privatization.
| Name simpleName = names.fromString("Nullable"); | ||
| return new Type.ErrorType(simpleName, packageSymbol, Type.noType); | ||
| private Type getSyntheticNullAnnotType(VisitorState state) { | ||
| if (syntheticNullAnnotType == null) { |
There was a problem hiding this comment.
Are we comparing this for identity in any way in which we would need to care about synchronization here? Or is either the case that (a) we don't care if we end up with multiple copies of the synthetic type / this is only a performance optimization, and/or (b) this class is never shared across threads (I actually don't fully recall how much parallelism there is in NullAway / EP overall beyond different javac instances in a parallel build having a copy of the plugin :) ).
There was a problem hiding this comment.
This is a combo of everything 🙂 Types do get compared by object equality in certain places (as I learned when fixing this issue), so seems good to only have one of these synthetic type objects floating around. Should be a small improvement in performance. And, this class should not be shared across threads; in a parallel build each thread should have its own NullAway instance, which creates its own GenericsChecks instance. javac itself doesn't support multithreading in a single instance AFAIK
There was a problem hiding this comment.
That matches what I thought re: NullAway / javac plugins, but it's good to have confirmation. Cool! :)
Fixes #1267
Due to possible type mutations, e.g., when substituting for bounds, sometimes we see
TypeVars in types that are not the same object as in the original method type for which we performed inference. Previously, we wouldn't substitute properly in such cases. Here, we collect allTypeVars with the sameSymbolobserved during inference and substitute for all of them, fixing the problem. This makes the Caffeine integration test green again.Summary by CodeRabbit