Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,21 @@ public Type visitWildcardType(Type.WildcardType t, Integer pathIndex) {
if (entry.kind() != NestedAnnotationInfo.TypePathEntry.Kind.WILDCARD_BOUND) {
return t;
}
int boundIndex = entry.index();
if (t.kind == BoundKind.UNBOUND) {
// TODO we need to add logic to _introduce_ a bound if none exists (add follow-up issue)
return t;
if (boundIndex != 0) {
// An unbounded wildcard has an implicit upper bound, but no lower bound.
return t;
}
Type.TypeVar formalTypeVariable =
Verify.verifyNotNull(
t.bound, "unbounded wildcard has no corresponding formal type variable");
Type upperBound = formalTypeVariable.getUpperBound();
Type updatedUpperBound = upperBound.accept(this, pathIndex + 1);
return updatedUpperBound == upperBound
? t
: TypeSubstitutionUtils.replaceUnboundedWildcardUpperBound(t, updatedUpperBound);
}
int boundIndex = entry.index();
if (boundIndex == 0 && t.kind == BoundKind.EXTENDS) {
Type newBound = t.type.accept(this, pathIndex + 1);
return newBound == t.type ? t : TYPE_METADATA_BUILDER.createWildcardType(t, newBound);
Expand Down Expand Up @@ -132,11 +142,17 @@ public Type visitCapturedType(Type.CapturedType t, Integer pathIndex) {
} else {
Verify.verify(pathIndex == typePath.size(), "path index out of bounds");
if (t.wildcard.kind == BoundKind.UNBOUND) {
// Do not turn javac's placeholder bound for an unbounded wildcard into an explicit bound.
return t;
Type.TypeVar formalTypeVariable =
Verify.verifyNotNull(
t.wildcard.bound, "unbounded wildcard has no corresponding formal type variable");
Type updatedUpperBound =
TypeSubstitutionUtils.typeWithAnnot(formalTypeVariable.getUpperBound(), annotationType);
updatedWildcard =
TypeSubstitutionUtils.replaceUnboundedWildcardUpperBound(t.wildcard, updatedUpperBound);
} else {
Type updatedBound = TypeSubstitutionUtils.typeWithAnnot(t.wildcard.type, annotationType);
updatedWildcard = TYPE_METADATA_BUILDER.createWildcardType(t.wildcard, updatedBound);
}
Type updatedBound = TypeSubstitutionUtils.typeWithAnnot(t.wildcard.type, annotationType);
updatedWildcard = TYPE_METADATA_BUILDER.createWildcardType(t.wildcard, updatedBound);
}
if (updatedWildcard == t.wildcard) {
return t;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,49 @@ Nested<? extends String> testWithVar(Nested<? extends String> receiver) {
.doTest();
}

@Test
public void annotationRestoredFromUpperBoundToCapturedUnboundedWildcard() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Nested<T extends @Nullable Object> {
Nested<T> self() {
return this;
}
Nested<? extends @Nullable T> wildcardUpperTypeVariable() {
throw new RuntimeException();
}
}

Nested<? extends Object> testDirect(Nested<?> receiver) {
// BUG: Diagnostic contains: incompatible types
return receiver.wildcardUpperTypeVariable();
}

Nested<? extends Object> testWithSelf(Nested<?> receiver) {
// BUG: Diagnostic contains: incompatible types
return receiver.self().wildcardUpperTypeVariable();
}

Nested<? extends Object> testWithVar(Nested<?> receiver) {
var local = receiver;
// BUG: Diagnostic contains: incompatible types
return local.wildcardUpperTypeVariable();
}

Nested<? extends @Nullable Object> testCompatible(Nested<?> receiver) {
return receiver.wildcardUpperTypeVariable();
}
}
""")
.doTest();
}

@Test
public void wildcardCaptureReturnWithTypeVariableUpperBound() {
makeHelper()
Expand Down Expand Up @@ -1044,6 +1087,36 @@ static class Analysis<
.doTest();
}

@Test
public void annotationRestorationForUnboundedWildcardWithFBoundedFormalDoesNotRecurse() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
@NullMarked
class Test {
interface Value<V extends Value<V>> {}
interface Store<S extends Store<S>> {}
interface Transfer<V extends Value<V>, S extends Store<S>> {}
static class Analysis<
V extends Value<V>,
S extends Store<S>,
T extends Transfer<V, S>> {}
static class Cache<K, V> {
V getUnchecked(K key) {
throw new UnsupportedOperationException();
}
}
final Cache<Object, Analysis<?, ?, ?>> cache = new Cache<>();
void test(Object key) {
Analysis<?, ?, ?> analysis = cache.getUnchecked(key);
}
}
""")
.doTest();
}

/** ensures we avoid a crash related to wildcards when wildcard handling is disabled */
@Test
public void methodRefParameterSuperWildcardWithHandlingDisabled() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.uber.lib.unannotated;

/* @NullMarked */
@SuppressWarnings("DoNotCallSuggester")
public class UnboundWildcards<T /* extends @Nullable Object */> {

public UnboundWildcards<T> self() {
return this;
}

public static UnboundWildcards<? extends /* @Nullable */ Object> literalWildcard() {
throw new RuntimeException();
}

public UnboundWildcards</* @Nullable */ T> typeVariable() {
throw new RuntimeException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBound
"com.uber.lib.unannotated.ProviderNullMarkedViaModel",
0,
"com.uber.lib.unannotated.NestedAnnots",
0,
"com.uber.lib.unannotated.UnboundWildcards",
0);
}

Expand All @@ -181,7 +183,8 @@ public ImmutableSet<String> nullMarkedClasses() {
"com.uber.lib.unannotated.LambdaConsumer",
"com.uber.lib.unannotated.LambdaModel",
"com.uber.lib.unannotated.NestedAnnots",
"com.uber.lib.unannotated.NullMarkedVarargsWithModel");
"com.uber.lib.unannotated.NullMarkedVarargsWithModel",
"com.uber.lib.unannotated.UnboundWildcards");
}

@Override
Expand Down Expand Up @@ -262,6 +265,21 @@ public ImmutableSetMultimap<MethodRef, Integer> methodTypeVariablesWithNullableU
ImmutableList.of(
new TypePathEntry(TYPE_ARGUMENT, 0),
new TypePathEntry(WILDCARD_BOUND, 0)))))
.put(
methodRef("com.uber.lib.unannotated.UnboundWildcards", "literalWildcard()"),
ImmutableSetMultimap.of(
-1,
new NestedAnnotationInfo(
Annotation.NULLABLE,
ImmutableList.of(
new TypePathEntry(TYPE_ARGUMENT, 0),
new TypePathEntry(WILDCARD_BOUND, 0)))))
.put(
methodRef("com.uber.lib.unannotated.UnboundWildcards", "typeVariable()"),
ImmutableSetMultimap.of(
-1,
new NestedAnnotationInfo(
Annotation.NULLABLE, ImmutableList.of(new TypePathEntry(TYPE_ARGUMENT, 0)))))
.put(
methodRef(
"com.uber.lib.unannotated.NestedAnnots",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,46 @@ NestedAnnots<? extends String> testWithVar(
.doTest();
}

@Test
public void nestedAnnotationsOnUnboundedWildcardBounds() {
makeLibraryModelsTestHelperWithArgs(
JSpecifyJavacConfig.withJSpecifyModeArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:OnlyNullMarked=true")))
.addSourceLines(
"Test.java",
"""
import com.uber.lib.unannotated.UnboundWildcards;
import org.jspecify.annotations.*;
@NullMarked
public class Test {

UnboundWildcards<? extends Object> testLiteral() {
// BUG: Diagnostic contains: incompatible types
return UnboundWildcards.literalWildcard();
}

UnboundWildcards<? extends @Nullable Object> testLiteralCompatible() {
return UnboundWildcards.literalWildcard();
}

UnboundWildcards<? extends Object> testCaptured(
UnboundWildcards<?> receiver) {
// BUG: Diagnostic contains: incompatible types
return receiver.self().typeVariable();
}

UnboundWildcards<? extends @Nullable Object> testCapturedCompatible(
UnboundWildcards<?> receiver) {
return receiver.self().typeVariable();
}
}
""")
.doTest();
}

@Test
public void multipleArgs() {
makeLibraryModelsTestHelperWithArgs(
Expand Down
Loading