diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java index 5ff89d51d1..a7f0899ba6 100644 --- a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java +++ b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java @@ -60,7 +60,8 @@ public enum MessageTypes { WRONG_OVERRIDE_PARAM_GENERIC, ASSIGN_NULLABLE_TO_NONNULL_ARRAY, GENERIC_INFERENCE_FAILURE, - NULLABLE_ON_WRONG_NESTED_CLASS_LEVEL + NULLABLE_ON_WRONG_NESTED_CLASS_LEVEL, + UNRECOGNIZED_NULLNESS_ANNOTATION_LOCATION } public String getMessage() { diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index 6773c68808..ca8296810e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -186,6 +186,7 @@ public class NullAway extends BugChecker BugChecker.SwitchExpressionTreeMatcher, BugChecker.TypeCastTreeMatcher, BugChecker.ParameterizedTypeTreeMatcher, + BugChecker.AnnotatedTypeTreeMatcher, BugChecker.SynchronizedTreeMatcher { static final String INITIALIZATION_CHECK_NAME = "NullAway.Init"; @@ -778,6 +779,14 @@ public Description matchParameterizedType(ParameterizedTypeTree tree, VisitorSta return Description.NO_MATCH; } + @Override + public Description matchAnnotatedType(AnnotatedTypeTree tree, VisitorState state) { + if (withinAnnotatedCode(state) && config.isJSpecifyMode() && config.handleWildcardGenerics()) { + genericsChecks.checkForNullnessAnnotationsOnWildcards(tree, state); + } + return Description.NO_MATCH; + } + /** * checks that an overriding method does not override a {@code @Nullable} parameter with a * {@code @NonNull} parameter 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 de818c77f7..400f32431a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -28,6 +28,7 @@ import com.sun.source.tree.ReturnTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WildcardTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; import com.sun.tools.javac.code.Attribute; @@ -163,6 +164,11 @@ public void checkInstantiationForParameterizedTypedTree( for (int i = 0; i < typeArguments.size(); i++) { Tree curTypeArg = typeArguments.get(i); if (curTypeArg instanceof AnnotatedTypeTree annotatedType) { + // Annotations directly on wildcards are illegal in JSpecify mode and are diagnosed in + // checkForNullnessAnnotationsOnWildcards(). + if (annotatedType.getUnderlyingType() instanceof WildcardTree) { + continue; + } for (AnnotationTree annotation : annotatedType.getAnnotations()) { Type annotationType = ASTHelpers.getType(annotation); if (annotationType != null @@ -190,6 +196,36 @@ public void checkInstantiationForParameterizedTypedTree( } } + /** + * Reports nullness annotations written directly on a wildcard, which is not a legal annotation + * location under JSpecify. + */ + public void checkForNullnessAnnotationsOnWildcards( + AnnotatedTypeTree annotatedType, VisitorState state) { + if (!(annotatedType.getUnderlyingType() instanceof WildcardTree)) { + return; + } + for (AnnotationTree annotation : annotatedType.getAnnotations()) { + Type annotationType = ASTHelpers.getType(annotation); + if (annotationType == null) { + continue; + } + String annotationName = annotationType.toString(); + if (!Nullness.isNullableAnnotation(annotationName, config) + && !Nullness.isNonNullAnnotation(annotationName, config)) { + continue; + } + ErrorBuilder errorBuilder = analysis.getErrorBuilder(); + ErrorMessage errorMessage = + new ErrorMessage( + ErrorMessage.MessageTypes.UNRECOGNIZED_NULLNESS_ANNOTATION_LOCATION, + "illegal location for annotation: nullness annotations cannot be written directly on a wildcard; annotate the wildcard bound instead"); + state.reportMatch( + errorBuilder.createErrorDescription( + errorMessage, analysis.buildDescription(annotation), state, null)); + } + } + private boolean[] getTypeParamsWithNullableUpperBound(Type type) { Symbol.TypeSymbol tsym = type.tsym; com.sun.tools.javac.util.List baseTypeArgs = tsym.type.getTypeArguments(); 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 deccdba575..81293edb32 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -903,6 +903,36 @@ Foo test(Foo<@Nullable Void> foo) { .doTest(); } + @Test + public void nullableOnWildcard() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.NonNull; + import org.jspecify.annotations.Nullable; + import java.util.function.Function; + @NullMarked + class Test { + @Nullable V testPositive(@Nullable K k, + Function< + // BUG: Diagnostic contains: illegal location for annotation + @Nullable ? super K, + // BUG: Diagnostic contains: illegal location for annotation + @NonNull ? extends V> function) { + // BUG: Diagnostic contains: passing @Nullable parameter 'k' where @NonNull is required + return function.apply(k); + } + + @Nullable V testNegative(@Nullable K k, Function function) { + return function.apply(k); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs(