Fix JSpecify false negative when override narrows method type variable bound - #1682
Fix JSpecify false negative when override narrows method type variable bound#1682arimu1 wants to merge 4 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughOverride checks now compare nullability on corresponding method type-variable upper bounds. The checks resolve substituted bounds, skip unannotated methods and mismatched type-variable counts, and report Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 2400-2423: Add Javadoc to
reportMismatchedMethodTypeVariableBoundError describing that it reports an error
when overriding and overridden method type variables have mismatched nullability
upper bounds, and document errorTree, overridingTv, overridingNullable,
overriddenMethod, overriddenNullable, and state.
- Around line 2350-2351: Update
checkMethodTypeVariableUpperBoundNullnessForOverriding and its call from
checkTypeParameterNullnessForMethodOverriding to accept the overridden method
type after member-type substitution in the overriding class context. Read
type-variable upper bounds from this contextual type instead of
overriddenMethod.getTypeParameters(), preserving correct nullable/non-null
instantiation behavior and JDK suppression handling. Add regressions covering
both nullable and non-null enclosing-class type-variable instantiations.
🪄 Autofix
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 Plus
Run ID: 909e93f4-3d4b-4773-bd3f-a2f2fe8a8d7b
📒 Files selected for processing (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javanullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
msridhar
left a comment
There was a problem hiding this comment.
Thanks for the contribution! Beyond the comment below, if you run ./gradlew :nullaway:buildWithNullAway you'll see several new warnings, and also integration tests are failing. I think we may need to special-case overrides of methods from @NullUnmarked code; if you could take a look that'd be great
…e bound In JSpecify mode, compare upper-bound nullability of corresponding method type variables between an overriding method and the method it overrides. Narrowing `<T extends @nullable Object>` to `<T>` (or the reverse) is unsound because callers can still instantiate the type variable via the overridden signature. Fixes uber#1512
…thods Read overridden method type-variable upper bounds from the method type after member-type substitution in the overriding class, so bounds that reference enclosing-class type variables compare correctly after instantiation (e.g. <T extends X> on Foo<@nullable Object>). Skip the check when the overridden method is from @NullUnmarked / unannotated code to avoid false positives from unmarked bounds. Add regressions for nullable and non-null enclosing-class instantiations, narrowing after substitution, and NullUnmarked overrides.
d3497a0 to
baebf51
Compare
|
@msridhar Thanks for the review and the Fixed on the branch tip
Local verification (JDK 21): |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java`:
- Around line 2386-2389: Update
checkMethodTypeVariableUpperBoundNullnessForOverriding so differing
type-parameter counts do not bypass validation for erasure-compatible overrides
such as a concrete Object parameter overriding a generic T parameter. Validate
the erased parameter and return nullness contracts before any unsupported-count
exit, while preserving the existing compiler-handled behavior for genuinely
incompatible overrides. Add a regression covering `@Override` void bar(Object arg)
{ arg.hashCode(); } with a nullable generic call path.
🪄 Autofix
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 Plus
Run ID: acc6975b-f749-46c6-bbea-a8d6e3dfeb6b
📒 Files selected for processing (2)
nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.javanullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1682 +/- ##
============================================
- Coverage 87.87% 87.84% -0.04%
- Complexity 3167 3180 +13
============================================
Files 109 109
Lines 10739 10791 +52
Branches 2171 2184 +13
============================================
+ Hits 9437 9479 +42
- Misses 617 621 +4
- Partials 685 691 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
msridhar
left a comment
There was a problem hiding this comment.
Thanks for the revisions! I have some more feedback
| substitutedMethodTypeVarUpperBoundIsNullable( | ||
| overriddenTypeVar, overriddenMethod, i, state); | ||
| if (overridingNullable != overriddenNullable) { | ||
| Tree errorTree = i < typeParameterTrees.size() ? typeParameterTrees.get(i) : tree; |
There was a problem hiding this comment.
Why do we need this i < typeParameterTrees.size() check here?
| if (methodType instanceof Type.ForAll forAll) { | ||
| return forAll.tvars; | ||
| } | ||
| return com.sun.tools.javac.util.List.nil(); |
There was a problem hiding this comment.
Is this actually reachable? This method is only used to get the type variables of the overridden method. I guess it might be reached if the overriding method introduces a type variable?
In any case, assuming this is reachable, then rather than using this method please explicitly check for no type variables in the caller and bail out there
| if (handler.onOverrideMethodTypeVariableUpperBound(overriddenMethod, typeVarIndex, state)) { | ||
| return true; | ||
| } | ||
| if (!(substitutedTypeVar instanceof Type.TypeVar typeVar)) { |
There was a problem hiding this comment.
Just make the declared type of the parameter Type.TypeVar rather than having this bailout
| // javac member-type substitution can drop type-use annotations on method type-variable | ||
| // bounds. If the original bound was a concrete type with an explicit @Nullable, honor that | ||
| // declaration. Do not consult original bounds that are still type variables — those must be | ||
| // resolved via substitution (or the free type-var path above). | ||
| List<Symbol.TypeVariableSymbol> originalTypeParams = overriddenMethod.getTypeParameters(); | ||
| if (typeVarIndex >= 0 && typeVarIndex < originalTypeParams.size()) { | ||
| Type originalBound = | ||
| (Type) ((TypeVariable) originalTypeParams.get(typeVarIndex).asType()).getUpperBound(); | ||
| if (originalBound.getKind() != TypeKind.TYPEVAR | ||
| && Nullness.hasNullableAnnotation( | ||
| originalBound.getAnnotationMirrors().stream(), config)) { | ||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
I don't understand when this code would apply. Can you please explain it?
Summary
Fixes #1512.
In JSpecify mode, NullAway did not compare method type-variable upper-bound nullability between an overriding method and the method it overrides. That allowed unsound overrides such as:
Callers can still invoke the method via the super type with a
@Nullabletype argument (e.g.f.<@Nullable String>bar(null)), so treating the override's parameter as non-null is incorrect.This change, in
GenericsChecks.checkTypeParameterNullnessForMethodOverriding, compares upper-bound nullability of corresponding method type variables (usingGenericsUtils.upperBoundIsNullable) and reportsWRONG_OVERRIDE_PARAM_GENERICwhen they differ—whether the override narrows@Nullable→ non-null or widens non-null →@Nullable.Tests
overrideNarrowsNullableMethodTypeVariableBound— issue JSpecify: False negative when overriding narrows@Nullabletype variable bound #1512 repro (param position)overrideWidensNonNullMethodTypeVariableBound— reverse mismatchoverridePreservesNullableMethodTypeVariableBound/overridePreservesNonNullMethodTypeVariableBound— matching bounds remain legaloverrideNarrowsNullableMethodTypeVariableBoundOnReturn— return-only type variable(JDK 21)
AI disclosure
I used AI tools (Grok) to help draft the fix and tests. I reviewed all changes, ran the tests above, and understand the code.
@Nullabletype variable bound #1512Summary by CodeRabbit
Bug Fixes
Tests