Skip to content

Commit 13fbba9

Browse files
committed
Nullness: ignore Jakarta Validation
Teach checks like `MultipleNullnessAnnotations` and `RedundantNullCheck` to disregard annotations that look like nullness specification annotations but that actually are not; namely the Jakarta Validation `@NotNull` constraint. Resolves: #4334
1 parent d6304e3 commit 13fbba9

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

check_api/src/main/java/com/google/errorprone/dataflow/nullnesspropagation/NullnessAnnotations.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static javax.lang.model.element.ElementKind.TYPE_PARAMETER;
2121

2222
import com.google.common.collect.ImmutableList;
23+
import com.google.common.collect.ImmutableSet;
2324
import com.google.errorprone.util.MoreAnnotations;
2425
import com.sun.source.tree.AnnotationTree;
2526
import com.sun.source.tree.IdentifierTree;
@@ -38,6 +39,7 @@
3839
import javax.lang.model.element.ExecutableElement;
3940
import javax.lang.model.element.Name;
4041
import javax.lang.model.element.Parameterizable;
42+
import javax.lang.model.element.QualifiedNameable;
4143
import javax.lang.model.element.TypeParameterElement;
4244
import javax.lang.model.type.IntersectionType;
4345
import javax.lang.model.type.TypeMirror;
@@ -60,6 +62,9 @@ public class NullnessAnnotations {
6062
+ "ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|"
6163
+ "ProtoPassThroughNullness")
6264
.asMatchPredicate();
65+
private static final ImmutableSet<String> FALSE_NULLNESS_ANNOTATIONS =
66+
ImmutableSet.of(
67+
"jakarta.validation.constraints.NotNull", "javax.validation.constraints.NotNull");
6368

6469
private NullnessAnnotations() {} // static methods only
6570

@@ -84,12 +89,13 @@ public static boolean annotationsAreAmbiguous(
8489
public static ImmutableList<AnnotationMirror> annotationsRelevantToNullness(
8590
Collection<? extends AnnotationMirror> annotations) {
8691
return annotations.stream()
87-
.filter(a -> ANNOTATION_RELEVANT_TO_NULLNESS.test(simpleName(a).toString()))
92+
.filter(NullnessAnnotations::isAnnotationRelevant)
8893
.collect(toImmutableList());
8994
}
9095

9196
public static ImmutableList<AnnotationTree> annotationsRelevantToNullness(
9297
List<? extends AnnotationTree> annotations) {
98+
// Missing support for FALSE_NULLNESS_ANNOTATIONS
9399
return annotations.stream()
94100
.filter(a -> ANNOTATION_RELEVANT_TO_NULLNESS.test(simpleName(a)))
95101
.collect(toImmutableList());
@@ -216,7 +222,10 @@ public static Optional<Nullness> getUpperBound(TypeVariable typeVar) {
216222

217223
private static Optional<Nullness> fromAnnotationStream(
218224
Stream<? extends AnnotationMirror> annotations) {
219-
return fromAnnotationSimpleNames(annotations.map(a -> simpleName(a).toString()));
225+
return fromAnnotationSimpleNames(
226+
annotations
227+
.filter(NullnessAnnotations::isAnnotationRelevant)
228+
.map(a -> simpleName(a).toString()));
220229
}
221230

222231
private static Optional<Nullness> fromAnnotationSimpleNames(Stream<String> annotations) {
@@ -225,4 +234,21 @@ private static Optional<Nullness> fromAnnotationSimpleNames(Stream<String> annot
225234
.map(annot -> NULLABLE_ANNOTATION.test(annot) ? Nullness.NULLABLE : Nullness.NONNULL)
226235
.reduce(Nullness::greatestLowerBound);
227236
}
237+
238+
private static boolean isAnnotationRelevant(AnnotationMirror annotation) {
239+
// https://github.com/google/error-prone/issues/4334
240+
// Some @NotNull annotations, e.g. com.google.firebase.database.annotations.NotNull, resemble
241+
// JSpecify @NonNull semantics. However, the Jakarta Bean Validation @NotNull constraint instead
242+
// represents a runtime check to reject a value that turns out to be null, rather than a static
243+
// promise that null will not be observed. Therefore, "@j.v.c.NotNull @o.j.a.Nullable Object o"
244+
// is a sensible and unambiguous declaration.
245+
if (annotation.getAnnotationType().asElement() instanceof QualifiedNameable qn) {
246+
var fqn = qn.getQualifiedName().toString();
247+
if (FALSE_NULLNESS_ANNOTATIONS.contains(fqn)) {
248+
return false;
249+
}
250+
return ANNOTATION_RELEVANT_TO_NULLNESS.test(simpleName(annotation).toString());
251+
}
252+
return true;
253+
}
228254
}

core/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,13 @@
360360
<artifactId>javax.inject</artifactId>
361361
<version>1</version>
362362
</dependency>
363+
<dependency>
364+
<!-- Apache 2.0 -->
365+
<groupId>jakarta.validation</groupId>
366+
<artifactId>jakarta.validation-api</artifactId>
367+
<version>3.1.0</version>
368+
<scope>test</scope>
369+
</dependency>
363370
</dependencies>
364371

365372
<build>

core/src/test/java/com/google/errorprone/bugpatterns/nullness/MultipleNullnessAnnotationsTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,26 @@ class T {
166166
""")
167167
.doTest();
168168
}
169+
170+
@Test
171+
public void falseNullnessAnnotations() {
172+
testHelper
173+
.addSourceLines(
174+
"T.java",
175+
"""
176+
import static java.lang.annotation.ElementType.*;
177+
import java.lang.annotation.Target;
178+
import org.jspecify.annotations.Nullable;
179+
180+
@Target(FIELD)
181+
@interface NotNull {}
182+
183+
abstract class T {
184+
@jakarta.validation.constraints.NotNull @Nullable Object negative;
185+
// BUG: Diagnostic contains:
186+
@NotNull @Nullable Object positive;
187+
}
188+
""")
189+
.doTest();
190+
}
169191
}

core/src/test/java/com/google/errorprone/bugpatterns/nullness/RedundantNullCheckTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,4 +1034,32 @@ void foo(@NonNull String s) {
10341034
""")
10351035
.doTest();
10361036
}
1037+
1038+
@Test
1039+
public void negative_falseNullnessAnnotation_inNullMarkedScope() {
1040+
compilationHelper
1041+
.addSourceLines(
1042+
"Test.java",
1043+
"""
1044+
import org.jspecify.annotations.NullMarked;
1045+
import org.jspecify.annotations.NonNull;
1046+
import org.jspecify.annotations.Nullable;
1047+
import jakarta.validation.constraints.NotNull;
1048+
1049+
@NullMarked
1050+
class Test {
1051+
@NotNull @Nullable String negative;
1052+
@NonNull @Nullable String positive;
1053+
1054+
void foo() {
1055+
if (negative == null) {
1056+
/* This is fine */
1057+
}
1058+
// BUG: Diagnostic contains: RedundantNullCheck
1059+
if (positive == null) {}
1060+
}
1061+
}
1062+
""")
1063+
.doTest();
1064+
}
10371065
}

0 commit comments

Comments
 (0)