diff --git a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java index 069ea3fdd1..c2ce7d050c 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -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; @@ -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); } /** @@ -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; } @@ -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); } /** @@ -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( @@ -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); } } @@ -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} or {@code Foo} 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. 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 4d00e26882..e79fe51fe5 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -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 { + void set(T t) {} + } + static void testNullableExtendsBound(Foo f) { + // BUG: Diagnostic contains: passing @Nullable parameter 'null' + f.set(null); + } + static void testNonNullExtendsBound(Foo f) { + // BUG: Diagnostic contains: passing @Nullable parameter 'null' + f.set(null); + } + static void testNullableSuperBound(Foo f) { + // this is legal + f.set(null); + } + static void testNonNullSuperBound(Foo 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 get() { throw new RuntimeException(); } + } + static void testNullableExtendsBound(Foo f) { + // BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable + f.get().hashCode(); + } + static void testNonNullExtendsBound(Foo f) { + // this is legal + f.get().hashCode(); + } + static void testNullableSuperBound(Foo f) { + // BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable + f.get().hashCode(); + } + static void testNonNullSuperBound(Foo 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 get() { throw new RuntimeException(); } + } + static class NullableBound { + void test(Foo f) { + // BUG: Diagnostic contains: dereferenced expression f.get() is @Nullable + f.get().hashCode(); + } + } + static class NonNullBound { + void test(Foo 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 get() { throw new RuntimeException(); } + } + static void testNullableExtendsBound(Foo f) { + Object x = f.get(); + // BUG: Diagnostic contains: dereferenced expression x is @Nullable + x.hashCode(); + } + static void testNonNullExtendsBound(Foo f) { + Object x = f.get(); + // this is legal + x.hashCode(); + } + static void testNullableSuperBound(Foo f) { + Object x = f.get(); + // BUG: Diagnostic contains: dereferenced expression x is @Nullable + x.hashCode(); + } + static void testNonNullSuperBound(Foo f) { + Object x = f.get(); + // BUG: Diagnostic contains: dereferenced expression x is @Nullable + x.hashCode(); + } + }""") + .doTest(); + } + @Test public void wildcardSuperBoundsAndInference() { makeHelperWithInferenceFailureWarning()