From f35981de8cc87671fb0a24c4f0ebad101eb38af4 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 17:32:25 -0700 Subject: [PATCH 1/4] 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 f0775530e19c2f291952f8e483b71673128c0697 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 18:05:45 -0700 Subject: [PATCH 2/4] 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 951bd9bbd3..3eed092b7b 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 4794e3fe8dee6e5863976c841324db8b244a6b08 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Thu, 30 Jul 2026 18:10:00 -0700 Subject: [PATCH 3/4] 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 3eed092b7b..4f05bda795 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", From 06c349fca94ad3900b8cf5c8cdcef4971f381a06 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 5 Aug 2026 11:52:53 -0700 Subject: [PATCH 4/4] add a safe version --- .../test/java/com/uber/nullaway/jspecify/WildcardTests.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 4f05bda795..34fe87d32c 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -466,6 +466,12 @@ Nested testWithVar(Nested receiver) { // BUG: Diagnostic contains: incompatible types return local.wildcardUpperTypeVariable(); } + + Nested testWithVarSafe(Nested receiver) { + var local = receiver; + // safe + return local.wildcardUpperTypeVariable(); + } } """) .doTest();