From 60e8006171229020a14de37afc98d0c72366bd33 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 15 May 2026 16:43:27 -0700 Subject: [PATCH 1/7] test case --- .../uber/nullaway/jspecify/WildcardTests.java | 19 +++++++++++++++++++ 1 file changed, 19 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 deccdba575..8cef628770 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,25 @@ Foo test(Foo<@Nullable Void> foo) { .doTest(); } + @Test + public void nullableOnWildcard() { + makeHelperWithInferenceFailureWarning() + .addSourceLines( + "Test.java", + """ + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + import java.util.function.Function; + @NullMarked + class Test { + @Nullable V run(@Nullable K k, Function<@Nullable ? super K, @Nullable ? extends V> function) { + return function.apply(k); + } + } + """) + .doTest(); + } + private CompilationTestHelper makeHelper() { return makeTestHelperWithArgs( JSpecifyJavacConfig.withJSpecifyModeArgs( From 29335aed50b80248858f89581e03eb5004f4062a Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 15 May 2026 17:09:07 -0700 Subject: [PATCH 2/7] fix --- .../java/com/uber/nullaway/ErrorMessage.java | 3 +- .../main/java/com/uber/nullaway/NullAway.java | 9 +++++ .../nullaway/generics/GenericsChecks.java | 36 +++++++++++++++++++ .../uber/nullaway/jspecify/WildcardTests.java | 9 ++++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java index 5ff89d51d1..fd792a7a86 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, + NULLNESS_ANNOTATION_ON_WILDCARD } 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..4989e8ad1c 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()) { + 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..c15153c96b 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.NULLNESS_ANNOTATION_ON_WILDCARD, + "illegal location for annotation: nullness annotations cannot be written directly on a wildcard"); + 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 8cef628770..e68474a076 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -910,11 +910,18 @@ public void nullableOnWildcard() { "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 run(@Nullable K k, Function<@Nullable ? super K, @Nullable ? extends V> function) { + @Nullable V run(@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); } } From 7463667588d53ac5e757ef198bb51cf5437cfe60 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 15 May 2026 17:16:15 -0700 Subject: [PATCH 3/7] expand the error message --- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c15153c96b..b6c40525b2 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -219,7 +219,7 @@ public void checkForNullnessAnnotationsOnWildcards( ErrorMessage errorMessage = new ErrorMessage( ErrorMessage.MessageTypes.NULLNESS_ANNOTATION_ON_WILDCARD, - "illegal location for annotation: nullness annotations cannot be written directly on a wildcard"); + "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)); From 44a4201ca13e147c8703c4446a9ec64d82b6989b Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Fri, 15 May 2026 17:20:24 -0700 Subject: [PATCH 4/7] improve test --- .../test/java/com/uber/nullaway/jspecify/WildcardTests.java | 6 +++++- 1 file changed, 5 insertions(+), 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 e68474a076..81293edb32 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/WildcardTests.java @@ -915,7 +915,7 @@ public void nullableOnWildcard() { import java.util.function.Function; @NullMarked class Test { - @Nullable V run(@Nullable K k, + @Nullable V testPositive(@Nullable K k, Function< // BUG: Diagnostic contains: illegal location for annotation @Nullable ? super K, @@ -924,6 +924,10 @@ class Test { // 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(); From 04d2f4c873944df861a22c7026916988b74fbd9f Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 16 May 2026 11:20:06 -0700 Subject: [PATCH 5/7] only apply new check when wildcard generics flag is set --- nullaway/src/main/java/com/uber/nullaway/NullAway.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index 4989e8ad1c..ca8296810e 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -781,7 +781,7 @@ public Description matchParameterizedType(ParameterizedTypeTree tree, VisitorSta @Override public Description matchAnnotatedType(AnnotatedTypeTree tree, VisitorState state) { - if (withinAnnotatedCode(state) && config.isJSpecifyMode()) { + if (withinAnnotatedCode(state) && config.isJSpecifyMode() && config.handleWildcardGenerics()) { genericsChecks.checkForNullnessAnnotationsOnWildcards(tree, state); } return Description.NO_MATCH; From 9e0debd25447390720ef7e2be4f14c7acb2d97d9 Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Sat, 16 May 2026 14:26:22 -0700 Subject: [PATCH 6/7] rename error message type --- nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java | 2 +- .../main/java/com/uber/nullaway/generics/GenericsChecks.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java index fd792a7a86..a7f0899ba6 100644 --- a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java +++ b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java @@ -61,7 +61,7 @@ public enum MessageTypes { ASSIGN_NULLABLE_TO_NONNULL_ARRAY, GENERIC_INFERENCE_FAILURE, NULLABLE_ON_WRONG_NESTED_CLASS_LEVEL, - NULLNESS_ANNOTATION_ON_WILDCARD + UNRECOGNIZED_NULLNESS_ANNOTATION_LOCATION } public String getMessage() { 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 b6c40525b2..400f32431a 100644 --- a/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java +++ b/nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java @@ -218,7 +218,7 @@ public void checkForNullnessAnnotationsOnWildcards( ErrorBuilder errorBuilder = analysis.getErrorBuilder(); ErrorMessage errorMessage = new ErrorMessage( - ErrorMessage.MessageTypes.NULLNESS_ANNOTATION_ON_WILDCARD, + 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( From 06f6580000e695efcfbb09a7675f4349bcbcc7df Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Mon, 25 May 2026 11:49:36 -0700 Subject: [PATCH 7/7] Empty commit for CI