From 7d3d06159179bf1ae164d73160444296437f4f9e Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 17:32:25 -0700 Subject: [PATCH 1/3] failing tests --- .../nullaway/CustomLibraryModelsTests.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test-library-models/src/test/java/com/uber/nullaway/CustomLibraryModelsTests.java b/test-library-models/src/test/java/com/uber/nullaway/CustomLibraryModelsTests.java index e3b63bba08..ab11446b40 100644 --- a/test-library-models/src/test/java/com/uber/nullaway/CustomLibraryModelsTests.java +++ b/test-library-models/src/test/java/com/uber/nullaway/CustomLibraryModelsTests.java @@ -584,7 +584,17 @@ public void nestedWildcardWithCapturedTypeVariableBound() { import org.jspecify.annotations.*; @NullMarked public class Test { - NestedAnnots test( + + NestedAnnots testDirect( + NestedAnnots receiver) { + // should reject since return type of wildcardUpperTypeVariable + // is modeled to be NestedAnnots, which + // here is incompatible with the return type NestedAnnots + // BUG: Diagnostic contains: incompatible types + return receiver.wildcardUpperTypeVariable(); + } + + NestedAnnots testWithSelf( NestedAnnots receiver) { // should reject since return type of wildcardUpperTypeVariable // is modeled to be NestedAnnots, which @@ -592,6 +602,18 @@ NestedAnnots test( // BUG: Diagnostic contains: incompatible types return receiver.self().wildcardUpperTypeVariable(); } + + + NestedAnnots testWithVar( + NestedAnnots receiver) { + var foo = receiver; + // should reject since return type of wildcardUpperTypeVariable + // is modeled to be NestedAnnots, which + // here is incompatible with the return type NestedAnnots + // BUG: Diagnostic contains: incompatible types + return foo.wildcardUpperTypeVariable(); + } + } """) .doTest(); From 13f9ccbacb1d3e7f0bf3182c5e0650ae8fa309f2 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 18:05:45 -0700 Subject: [PATCH 2/3] fix --- .../generics/TypeSubstitutionUtils.java | 19 +++++++-- .../uber/nullaway/jspecify/WildcardTests.java | 39 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java index 436277ea5c..111e43d21b 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/TypeSubstitutionUtils.java @@ -360,17 +360,28 @@ public Type visitTypeVar(Type.TypeVar t, Type other) { *

The corresponding type {@code other} may be an ordinary wildcard because javac can * capture-convert {@code t} without capture-converting {@code other}. In such cases, the * annotation on the bound of {@code other} should be restored to the bound of the wildcard - * corresponding to {@code t}. + * corresponding to {@code t}. Alternatively, nested capture conversion can make {@code t} a + * captured {@code extends} wildcard while the same position in {@code other} is a non-wildcard + * type. In that case, annotations from {@code other} must be restored to the backing wildcard's + * upper bound, since wildcard-aware checks use that bound rather than annotations directly on + * the captured type. */ @Override public Type visitCapturedType(Type.CapturedType t, Type other) { Type updated = updateDirectNullabilityAnnotationsForType(t, other); Type.WildcardType otherWildcard = GenericsUtils.asWildcard(other); - if (otherWildcard == null) { + Type.WildcardType updatedWildcard; + if (otherWildcard != null) { + updatedWildcard = (Type.WildcardType) t.wildcard.accept(this, otherWildcard); + } else if (t.wildcard.kind == BoundKind.EXTENDS) { + Type updatedBound = t.wildcard.type.accept(this, other); + if (updatedBound == t.wildcard.type) { + return updated; + } + updatedWildcard = TYPE_METADATA_BUILDER.createWildcardType(t.wildcard, updatedBound); + } else { return updated; } - Type.WildcardType updatedWildcard = - (Type.WildcardType) t.wildcard.accept(this, otherWildcard); if (updatedWildcard == t.wildcard) { return updated; } diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index e9f253ead7..5bb55ab42d 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -432,6 +432,45 @@ static void test(Holder> holder) { .doTest(); } + @Test + public void wildcardCaptureRestoresConcreteAnnotatedBound() { + makeHelper() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + class Test { + static class Nested { + Nested self() { + return this; + } + Nested wildcardUpperTypeVariable() { + throw new RuntimeException(); + } + } + + Nested testDirect(Nested receiver) { + // BUG: Diagnostic contains: incompatible types + return receiver.wildcardUpperTypeVariable(); + } + + Nested testWithSelf(Nested receiver) { + // BUG: Diagnostic contains: incompatible types + return receiver.self().wildcardUpperTypeVariable(); + } + + Nested testWithVar(Nested receiver) { + var local = receiver; + // BUG: Diagnostic contains: incompatible types + return local.wildcardUpperTypeVariable(); + } + } + """) + .doTest(); + } + @Test public void wildcardCaptureReturnWithTypeVariableUpperBound() { makeHelper() From 78d77c1d459ea76f2e4ec91d57f62df3cbd7274c Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 18:10:00 -0700 Subject: [PATCH 3/3] better name --- .../src/test/java/com/uber/nullaway/jspecify/WildcardTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java index 5bb55ab42d..3f7153c07c 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -433,7 +433,7 @@ static void test(Holder> holder) { } @Test - public void wildcardCaptureRestoresConcreteAnnotatedBound() { + public void annotationRestoredFromUpperBoundToCapturedWildcard() { makeHelper() .addSourceLines( "Test.java",