JSpecify: support annotated type arguments from an enclosing class - #1699
JSpecify: support annotated type arguments from an enclosing class#1699dbwiddis wants to merge 1 commit 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)
Walkthrough
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
…ber#836) The previously-@ignore'd test overrideAnonymousNestedClass exercises an anonymous class instantiated via a qualified new expression with two levels of generics, e.g. (this.new Wrapper<@nullable String>()).new Fn<String>() {...}, where the abstract method being overridden, Fn.apply(P p), references the OUTER class Wrapper's type variable P. Running the test (rather than assuming from the issue text) showed this was a hard, uncaught VerifyException crash, not a silently-missed diagnostic: GenericsChecks.getTreeType's NewClassTree handling only ever consulted newClassTree.getIdentifier() (Fn<String>) via PreservedAnnotationTreeVisitor, and PreservedAnnotationTreeVisitor's visitParameterizedType built the enclosing type from baseType.getEnclosingType() -- Fn's statically-declared enclosing type Wrapper<P>, with the raw type variable P, completely unconnected to the qualifier expression's actual instantiation. getTypeForSymbol's sanity-check verify() correctly caught the resulting type mismatch and threw. Adds GenericsChecks.withEnclosingTypeFromQualifier, which recovers the qualifier expression's own correctly-substituted type (recursing via the existing getTreeType, so arbitrary nesting depth is handled for free) and splices it in as the enclosing type, replacing baseType.getEnclosingType()'s incorrect result. This is separate from, and does not touch, the diamond-operator anonymous-class gap tracked in uber#1475.
d9aedea to
486a447
Compare
Summary
Un-ignores
overrideAnonymousNestedClass, which exercises an anonymous class instantiated via a qualifiednewexpression with two levels of generics:Fn.apply(P p)references the outer classWrapper's type variableP. The@Nullableannotation forPlives in the qualifier sub-expression (this.new Wrapper<@Nullable String>()), not in theFn<String>part naming the class actually being instantiated.Diagnosis
Running the ignored test (rather than assuming the issue title described the actual behavior) showed this is a hard, uncaught crash, not a silently-missed diagnostic:
GenericsChecks.getTreeType'sNewClassTreehandling only ever consultednewClassTree.getIdentifier()(Fn<String>) viaPreservedAnnotationTreeVisitor, nevernewClassTree.getEnclosingExpression()(the qualifierthis.new Wrapper<@Nullable String>()).PreservedAnnotationTreeVisitor.visitParameterizedTypebuilt the enclosing type frombaseType.getEnclosingType()—Fn's statically-declared enclosing type,Wrapper<P>, with the raw, unsubstituted type variablePstill present, completely unconnected to what the qualifier expression actually evaluates to. This is a materially different (and worse) bug than "annotations get dropped" — the enclosing type was never resolved to any real instantiation at all.getTypeForSymbol's own sanity-checkverify()correctly caught the resulting type mismatch and threw.This gap was left open deliberately by #837 ("JSpecify: initial handling of generic enclosing types for inner classes"), whose commit message states it fixed the enclosing-type case for ordinary declared types but explicitly did not yet handle
NewClassTrees.Fix
Adds
GenericsChecks.withEnclosingTypeFromQualifier, called fromgetTreeType'sNewClassTreebranch. It readsnewClassTree.getEnclosingExpression(); if present, it recursively calls the existinggetTreeTypeon that qualifier to get its own correctly-substituted, annotated type (this recursion is bounded by the qualifier-chain depth in the source, so arbitrarily deep nesting is handled for free with no new logic), then splices that in as the enclosing type viaTypeMetadataBuilder.createClassType, replacing the brokenbaseType.getEnclosingType()result. If there's no qualifying expression (the ordinary, unqualified anonymous-class case #808 already handles correctly), the type is returned unchanged.This is separate from, and does not touch, the diamond-operator anonymous-class gap tracked in #1475 (a distinct, early-bailout branch in the same method).
Fixes #836
Testing
overrideAnonymousNestedClasswith@Ignoreremoved before writing any fix, to confirm the actual failure mode (the crash above) rather than assume it from the issue text.overrideAnonymousNestedClassagain: passes, no crash, expected diagnostic reported.overrideAnonymousDeeplyNestedClass, a 3-level-deep qualified-nesting case (Wrapper.Middle.Fn), to confirm the recursive approach generalizes beyond the 2-level case in the original test.getEnclosingExpression()tonullinside the helper) and reran both tests — both failed (reproducing the original crash) without the fix, confirming they're load-bearing rather than passing vacuously. Reverted both before finalizing../gradlew :nullaway:test(full module suite): 914 tests, 0 failures, 0 errors../gradlew :nullaway:spotlessJavaCheck.AI usage disclosure
Summary by CodeRabbit
Bug Fixes
Tests