Skip to content

Commit 6bba0e5

Browse files
authored
Add regression test for inner classes inheriting enclosing type arguments (#1703)
## Summary #1274 reports that NullAway misses a warning when a qualified inner-class creation is assigned to a variable whose *enclosing* type argument is incompatible: ```java class A<X extends @nullable Object> { class B<Y extends @nullable Object> {} } class C<X extends @nullable Object> extends A<X> { class D<Y extends @nullable Object> extends A<X>.B<Y> {} } C<@nullable String> c = new C<>(); A<String>.B<@nullable String> unused = c.new D<@nullable String>(); // should warn ``` Rather than assume the issue title described current behavior, I ran the example verbatim as a test first. On current `master` NullAway **does** report the expected error: ``` warning: [NullAway] incompatible types: Test.C<@nullable String>.D<@nullable String> cannot be converted to Test.A<String>.B<@nullable String> (Test.C<@nullable String>.D<@nullable String> is a subtype of Test.A<@nullable String>.B<@nullable String>) ``` Running the same test against `56bd5ad2` — the commit immediately preceding #1699 — it reports nothing. So this false negative was fixed as a side effect of #1699. That makes sense mechanically. Before #1699, `getTreeType` typed `c.new D<@nullable String>()` from the `D<@nullable String>` identifier alone, and `PreservedAnnotationTreeVisitor` took the enclosing type from `baseType.getEnclosingType()` — `D`'s statically-declared enclosing type `C<X>`, with `X` unsubstituted. Comparing `C<X>.D<@nullable String>` against `A<String>.B<@nullable String>` surfaces no nullability conflict, since `X` carries no annotation either way. #1699's `withEnclosingTypeFromQualifier` types the qualifier `c` instead, yielding `C<@nullable String>.D<@nullable String>`; `D`'s supertype then substitutes to `A<@nullable String>.B<@nullable String>` and the mismatch against the declared `A<String>.B<...>` becomes visible. This PR adds a regression test so the behavior stays fixed. No production code changes. ## Testing - Added `innerClassInheritingEnclosingTypeArgs` in `GenericsTests.java`, covering the reported error case plus two negative controls: a correctly-typed `@Nullable` enclosing instance, and a non-`@Nullable` one. Both must stay silent, so the test also pins down that we don't over-report here. - Negative control on the assertion itself: temporarily changed the erroring line's declared type to the correct one and confirmed the test fails with `Did not see an error on line 14 ... There were no errors`, verifying the expectation is load-bearing and not incidentally satisfied. - Confirmed the test fails (reports nothing) at `56bd5ad2` and passes on `master`. - `./gradlew :nullaway:test` — 919 tests, 0 failures. Fixes #1274 ## AI usage disclosure > I used Claude Code for this PR. It ran the issue's repro, bisected to determine the false negative had > already been fixed by my earlier PR #1699, explained the mechanism, and drafted the regression test. > I asked follow-up questions about the enclosing-type handling in `getTreeType` and > `PreservedAnnotationTreeVisitor`, and verified the test is load-bearing by mutating it locally. > I have read and understood all the changes in this PR. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added regression coverage for generic type arguments inherited by inner classes. * Verified compatible nullable assignments and detection of incompatible nullable-to-non-null assignments. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent daa5828 commit 6bba0e5

1 file changed

Lines changed: 29 additions & 0 deletions

File tree

nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,35 @@ void anonymousDeeplyNestedClasses() {
16791679
.doTest();
16801680
}
16811681

1682+
/** See https://github.com/uber/NullAway/issues/1274 */
1683+
@Test
1684+
public void innerClassInheritingEnclosingTypeArgs() {
1685+
makeHelper()
1686+
.addSourceLines(
1687+
"Test.java",
1688+
"""
1689+
package com.uber;
1690+
import org.jspecify.annotations.Nullable;
1691+
class Test {
1692+
class A<X extends @Nullable Object> {
1693+
class B<Y extends @Nullable Object> {}
1694+
}
1695+
class C<X extends @Nullable Object> extends A<X> {
1696+
class D<Y extends @Nullable Object> extends A<X>.B<Y> {}
1697+
}
1698+
void test() {
1699+
C<@Nullable String> c1 = new C<>();
1700+
A<@Nullable String>.B<@Nullable String> ok1 = c1.new D<@Nullable String>();
1701+
// BUG: Diagnostic contains: incompatible types: Test.C<@Nullable String>.D<@Nullable String> cannot be converted to Test.A<String>.B<@Nullable String>
1702+
A<String>.B<@Nullable String> bad = c1.new D<@Nullable String>();
1703+
C<String> c2 = new C<>();
1704+
A<String>.B<@Nullable String> ok2 = c2.new D<@Nullable String>();
1705+
}
1706+
}
1707+
""")
1708+
.doTest();
1709+
}
1710+
16821711
@Test
16831712
public void otherTypeUseNullableAnnotation() {
16841713
makeHelper()

0 commit comments

Comments
 (0)