Skip to content
Merged
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 @@ -30,6 +30,7 @@
import com.sun.source.util.TreePath;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.BoundKind;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.TargetType;
Expand Down Expand Up @@ -1801,7 +1802,7 @@ public Nullness getGenericMethodReturnTypeNullness(
overriddenMethodType instanceof ExecutableType,
"expected ExecutableType but instead got %s",
overriddenMethodType.getClass());
return getTypeNullness(overriddenMethodType.getReturnType());
return getReturnTypeNullness(overriddenMethodType.getReturnType(), state);
}

/**
Expand Down Expand Up @@ -2198,7 +2199,8 @@ public Nullness getGenericParameterNullnessAtInvocation(
// type variables declared on the enclosing class
if (substitutedParamTypes != null
&& Objects.equals(
getParameterTypeNullness(substitutedParamTypes.get(paramIndex), isVarargsParam),
getParameterTypeNullness(
substitutedParamTypes.get(paramIndex), isVarargsParam, state),
Nullness.NULLABLE)) {
return Nullness.NULLABLE;
}
Expand Down Expand Up @@ -2335,7 +2337,7 @@ public Nullness getGenericMethodParameterNullness(
Type methodType =
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, method, config);
Type paramType = methodType.getParameterTypes().get(parameterIndex);
return getParameterTypeNullness(paramType, isVarargsParam);
return getParameterTypeNullness(paramType, isVarargsParam, state);
}

/**
Expand Down Expand Up @@ -2402,7 +2404,7 @@ private void checkTypeParameterNullnessForOverridingMethodReturnType(
* @param isVarargsParam true if the parameter is a varargs parameter
* @return The nullness of the parameter type
*/
private Nullness getParameterTypeNullness(Type type, boolean isVarargsParam) {
private Nullness getParameterTypeNullness(Type type, boolean isVarargsParam, VisitorState state) {
if (isVarargsParam) {
// type better be an array type
verify(
Expand All @@ -2412,9 +2414,14 @@ private Nullness getParameterTypeNullness(Type type, boolean isVarargsParam) {
// use the component type to determine nullness
Type.ArrayType arrayType = (Type.ArrayType) type;
Type componentType = arrayType.getComponentType();
return getTypeNullness(componentType);
return getParameterTypeNullness(componentType, false, state);
} else {
// For non-varargs, we just check the type itself
if (config.handleWildcardGenerics()) {
Type.WildcardType wildcardType = GenericsUtils.asWildcard(type);
if (wildcardType != null && wildcardType.kind == BoundKind.SUPER) {
return getTypeNullness(castToNonNull(wildcardType.getSuperBound()));
}
}
return getTypeNullness(type);
}
}
Expand All @@ -2432,6 +2439,33 @@ private Nullness getTypeNullness(Type type) {
return Nullness.NONNULL;
}

/**
* Returns the nullness of a return type. For wildcard and javac captured wildcard types, use the
* effective upper bound: a read from {@code Foo<? extends @Nullable Object>} or {@code Foo<?
* super String>} can produce any value permitted by the capture's upper bound.
*/
private Nullness getReturnTypeNullness(Type type, VisitorState state) {
return getReturnTypeNullness(type, state, false);
}

private Nullness getReturnTypeNullness(
Type type, VisitorState state, boolean followTypeVarUpperBound) {
if (getTypeNullness(type).equals(Nullness.NULLABLE)) {
return Nullness.NULLABLE;
}
if (config.handleWildcardGenerics() && GenericsUtils.asWildcard(type) != null) {
Type effectiveUpperBound = GenericsUtils.effectiveWildcardUpperBound(type, state);
return getReturnTypeNullness(effectiveUpperBound, state, true);
}
if (followTypeVarUpperBound && type instanceof Type.TypeVar typeVar) {
Type upperBound = typeVar.getUpperBound();
if (upperBound != null) {
return getReturnTypeNullness(upperBound, state, true);
}
}
return Nullness.NONNULL;
}

/**
* Returns a pretty-printed representation of type suitable for error messages. The representation
* uses simple names rather than fully-qualified names, and retains all type-use annotations.
Expand Down
132 changes: 132 additions & 0 deletions nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,138 @@ void testLocals(
.doTest();
}

@Test
public void wildcardCaptureParameters() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Foo<T extends @Nullable Object> {
void set(T t) {}
}
static void testNullableExtendsBound(Foo<? extends @Nullable Object> f) {
// BUG: Diagnostic contains: passing @Nullable parameter 'null'
f.set(null);
}
static void testNonNullExtendsBound(Foo<? extends Object> f) {
// BUG: Diagnostic contains: passing @Nullable parameter 'null'
f.set(null);
}
static void testNullableSuperBound(Foo<? super @Nullable String> f) {
// this is legal
f.set(null);
}
static void testNonNullSuperBound(Foo<? super String> f) {
// BUG: Diagnostic contains: passing @Nullable parameter 'null'
f.set(null);
}
}""")
.doTest();
}

@Test
public void wildcardCaptureReturns() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Foo<T extends @Nullable Object> {
T get() { throw new RuntimeException(); }
}
static void testNullableExtendsBound(Foo<? extends @Nullable Object> f) {
// BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable
f.get().hashCode();
}
static void testNonNullExtendsBound(Foo<? extends Object> f) {
// this is legal
f.get().hashCode();
}
static void testNullableSuperBound(Foo<? super @Nullable String> f) {
// BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable
f.get().hashCode();
}
static void testNonNullSuperBound(Foo<? super String> f) {
// BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable
f.get().hashCode();
}
}""")
.doTest();
}

@Test
public void wildcardCaptureReturnWithTypeVariableUpperBound() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Foo<T extends @Nullable Object> {
T get() { throw new RuntimeException(); }
}
static class NullableBound<U extends @Nullable Object> {
void test(Foo<? extends U> f) {
// BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable
f.get().hashCode();
}
}
static class NonNullBound<U> {
void test(Foo<? extends U> f) {
// this is legal
f.get().hashCode();
}
}
}""")
.doTest();
}

@Test
public void wildcardCaptureLocals() {
makeHelper()
.addSourceLines(
"Test.java",
"""
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
class Test {
static class Foo<T extends @Nullable Object> {
T get() { throw new RuntimeException(); }
}
static void testNullableExtendsBound(Foo<? extends @Nullable Object> f) {
Object x = f.get();
// BUG: Diagnostic contains: dereferenced expression x is @Nullable
x.hashCode();
}
static void testNonNullExtendsBound(Foo<? extends Object> f) {
Object x = f.get();
// this is legal
x.hashCode();
}
static void testNullableSuperBound(Foo<? super @Nullable String> f) {
Object x = f.get();
// BUG: Diagnostic contains: dereferenced expression x is @Nullable
x.hashCode();
}
static void testNonNullSuperBound(Foo<? super String> f) {
Object x = f.get();
// BUG: Diagnostic contains: dereferenced expression x is @Nullable
x.hashCode();
}
}""")
.doTest();
}

@Test
public void wildcardSuperBoundsAndInference() {
makeHelperWithInferenceFailureWarning()
Expand Down
Loading