Re-introduce annotations elided by javac for certain cases#1473
Re-introduce annotations elided by javac for certain cases#1473
javac for certain cases#1473Conversation
WalkthroughThis pull request adds support for restoring missing type-use nullability annotations on type variables during generic method type inference. The changes include introducing a new Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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: 1
🤖 Fix all issues with AI agents
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 1615-1697: The method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments currently only
updates parameter types but can leave return and thrown types inconsistent when
a method type variable also appears there; after computing
repairedTopLevelSubstitutions, apply those substitutions to
methodTypeAtCallSite.getReturnType() and each type in
methodTypeAtCallSite.getThrownTypes() using
TypeSubstitutionUtils.subst(state.getTypes(), type, fromMap, toMap, config) (or
equivalent API) to produce repairedReturnType and repairedThrownTypes, set
changed if any differ, and use those repaired types when constructing the new
Type.MethodType so return/thrown nullability stays consistent with the repaired
parameters.
| /** | ||
| * For some calls, javac drops nested type-use nullability annotations in inferred substitutions | ||
| * for method type variables. Recover these annotations from the corresponding actual argument | ||
| * types, while preserving one consistent top-level substitution per method type variable. | ||
| */ | ||
| @SuppressWarnings("ReferenceEquality") | ||
| private Type.MethodType restoreMissingNullabilityFromSingleTopLevelTypeVarArguments( | ||
| MethodInvocationTree invocationTree, | ||
| Type.MethodType origMethodType, | ||
| Type.MethodType methodTypeAtCallSite, | ||
| VisitorState state) { | ||
| Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invocationTree); | ||
| if (methodSymbol == null || methodSymbol.isVarArgs()) { | ||
| // TODO handle varargs methods | ||
| return methodTypeAtCallSite; | ||
| } | ||
| com.sun.tools.javac.util.List<Type> origArgTypes = origMethodType.getParameterTypes(); | ||
| com.sun.tools.javac.util.List<Type> callSiteArgTypes = methodTypeAtCallSite.getParameterTypes(); | ||
| List<? extends ExpressionTree> callArgs = invocationTree.getArguments(); | ||
| if (origArgTypes.size() != callSiteArgTypes.size() || callArgs.size() != origArgTypes.size()) { | ||
| return methodTypeAtCallSite; | ||
| } | ||
|
|
||
| // use this map to store repaired substitutions for method type variables, to ensure we use the | ||
| // same repaired | ||
| // substitution for all occurrences of the same method type variable | ||
| Map<Symbol.TypeVariableSymbol, Type> repairedTopLevelSubstitutions = new HashMap<>(); | ||
| ListBuffer<Type> updatedArgTypes = new ListBuffer<>(); | ||
| boolean changed = false; | ||
| for (int i = 0; i < origArgTypes.size(); i++) { | ||
| Type updatedType = callSiteArgTypes.get(i); | ||
| Type origArgType = origArgTypes.get(i); | ||
| if (origArgType instanceof Type.TypeVar typeVar | ||
| && typeVar.tsym.owner == methodSymbol | ||
| && !(updatedType instanceof Type.TypeVar)) { | ||
| Symbol.TypeVariableSymbol typeVarSymbol = (Symbol.TypeVariableSymbol) typeVar.tsym; | ||
| Type repairedSubstitution = repairedTopLevelSubstitutions.get(typeVarSymbol); | ||
| if (repairedSubstitution != null) { | ||
| if (!state | ||
| .getTypes() | ||
| .isSameType( | ||
| state.getTypes().erasure(repairedSubstitution), | ||
| state.getTypes().erasure(updatedType))) { | ||
| // Inconsistent substitution for the same top-level type variable; bail out. | ||
| return methodTypeAtCallSite; | ||
| } | ||
| if (repairedSubstitution != updatedType) { | ||
| changed = true; | ||
| updatedType = repairedSubstitution; | ||
| } | ||
| } else { // need to compute the substitution | ||
| Type actualArgType = getTreeType(callArgs.get(i), state); | ||
| if (actualArgType != null | ||
| && !actualArgType.isRaw() | ||
| && state | ||
| .getTypes() | ||
| .isSameType( | ||
| state.getTypes().erasure(actualArgType), | ||
| state.getTypes().erasure(updatedType))) { | ||
| Type restoredType = | ||
| TypeSubstitutionUtils.restoreExplicitNullabilityAnnotations( | ||
| actualArgType, updatedType, config, Collections.emptyMap()); | ||
| repairedTopLevelSubstitutions.put(typeVarSymbol, restoredType); | ||
| if (restoredType != updatedType) { | ||
| changed = true; | ||
| updatedType = restoredType; | ||
| } | ||
| } else { | ||
| repairedTopLevelSubstitutions.put(typeVarSymbol, updatedType); | ||
| } | ||
| } | ||
| } | ||
| updatedArgTypes.append(updatedType); | ||
| } | ||
| if (!changed) { | ||
| return methodTypeAtCallSite; | ||
| } | ||
| return new Type.MethodType( | ||
| updatedArgTypes.toList(), | ||
| methodTypeAtCallSite.getReturnType(), | ||
| methodTypeAtCallSite.getThrownTypes(), | ||
| methodTypeAtCallSite.tsym); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, locate existing usage patterns of Types.subst and TypeSubstitutionUtils.subst
rg -n "Types\.subst\(|TypeSubstitutionUtils\.subst" -S --type java | head -30Repository: uber/NullAway
Length of output: 171
🏁 Script executed:
#!/bin/bash
# Check the broader context of the GenericsChecks.java file to understand the pattern
rg -n "TypeSubstitutionUtils|Types.*subst" nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javaRepository: uber/NullAway
Length of output: 1390
🏁 Script executed:
#!/bin/bash
# Look for usages of getTypeParameters and method type variable handling in the file
rg -n "getTypeParameters|TypeVariableSymbol" nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java -A 2 -B 2Repository: uber/NullAway
Length of output: 1302
🏁 Script executed:
#!/bin/bash
# Check if there are existing tests for the restoreMissingNullabilityFromSingleTopLevelTypeVarArguments method
fd -t f "Test.*\.java" nullaway | xargs grep -l "restoreMissingNullabilityFromSingleTopLevelTypeVarArguments" 2>/dev/nullRepository: uber/NullAway
Length of output: 41
🏁 Script executed:
#!/bin/bash
# Check the context around line 1611 to see how TypeSubstitutionUtils.subst is used
sed -n '1605,1620p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javaRepository: uber/NullAway
Length of output: 784
🏁 Script executed:
#!/bin/bash
# Look at the TypeSubstitutionUtils class to understand available subst methods
grep -n "\.subst\|public.*subst" nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java | head -20Repository: uber/NullAway
Length of output: 210
🏁 Script executed:
#!/bin/bash
# Examine the entire method being reviewed to check if return types are mentioned
sed -n '1615,1697p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javaRepository: uber/NullAway
Length of output: 3886
🏁 Script executed:
#!/bin/bash
# Find where restoreMissingNullabilityFromSingleTopLevelTypeVarArguments is called
rg -n "restoreMissingNullabilityFromSingleTopLevelTypeVarArguments" nullaway/Repository: uber/NullAway
Length of output: 364
🏁 Script executed:
#!/bin/bash
# Look for tests related to this method or similar return type scenarios
fd -t f ".*Test.*\.java" nullaway | xargs grep -l "restoreMissing\|return.*type.*var" 2>/dev/null | head -5Repository: uber/NullAway
Length of output: 39
🏁 Script executed:
#!/bin/bash
# Check if there are any TODOs or comments about return type handling near this method
sed -n '1597,1700p' nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java | grep -i "todo\|fixme\|return\|thrown"Repository: uber/NullAway
Length of output: 615
Consider return/thrown type repair as a follow-up enhancement.
The method currently repairs parameter substitutions but leaves return and thrown types untouched. While technically consistent within this method's narrow scope, if a method type variable appears in the return type (e.g., <T extends Supplier<?>> T acceptSup(T)), parameter repairs to Supplier<@Nullable OuterT> paired with an unrepaired return Supplier<OuterT> could create inconsistent nullability in inferred types.
The suggested approach—applying repaired substitutions to return/thrown types using TypeSubstitutionUtils.subst(state.getTypes(), type, from, to, config)—is sound. However, this extends the method's scope beyond single top-level type var arguments. Consider addressing this in a follow-up PR to keep the current change focused.
🤖 Prompt for AI Agents
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java` around
lines 1615 - 1697, The method
restoreMissingNullabilityFromSingleTopLevelTypeVarArguments currently only
updates parameter types but can leave return and thrown types inconsistent when
a method type variable also appears there; after computing
repairedTopLevelSubstitutions, apply those substitutions to
methodTypeAtCallSite.getReturnType() and each type in
methodTypeAtCallSite.getThrownTypes() using
TypeSubstitutionUtils.subst(state.getTypes(), type, fromMap, toMap, config) (or
equivalent API) to produce repairedReturnType and repairedThrownTypes, set
changed if any differ, and use those repaired types when constructing the new
Type.MethodType so return/thrown nullability stays consistent with the repaired
parameters.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1473 +/- ##
============================================
- Coverage 88.48% 88.43% -0.05%
- Complexity 2723 2734 +11
============================================
Files 99 99
Lines 9073 9133 +60
Branches 1812 1825 +13
============================================
+ Hits 8028 8077 +49
- Misses 518 522 +4
- Partials 527 534 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
javac for certain casesjavac for certain cases
Fixes #1455
For reference here is the test case:
What goes wrong here is at the call
acceptSup(sup),javacinfers the type argument to beSupplier<OuterT>, dropping the@Nullableannotation onOuterTseen in the declaration ofsup. Our inference correctly infers thatTitself should be@NonNull, but then we still report an error sinceSupplier<@Nullable OuterT>(the type ofsup) is incompatible withSupplier<OuterT>(what we think is the formal parameter type).This started happening in 0.13.0 due to #1348, because we started computing the proper method types at call sites to report better error messages. Unfortunately, this is leading to unforeseen interactions with
javac's type inference like the issue above; but I'd like to see if we can work around the corner cases since it's important to have good error messages.For this case, we add a method
restoreMissingNullabilityFromSingleTopLevelTypeVarArgumentsthat repairs the method type by re-introducing explicit nullability annotations from actual parameters onto the formal parameter types.We also do a drive-by fix in
GenericTypePrettyPrintingVisitorto improve how we print type variables (not printing fully-qualified names of annotations).Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Tests