Skip to content

Commit 62b54c4

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 0cb1ec5 commit 62b54c4

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.sun.source.tree.AnnotationTree;
2425
import com.sun.source.tree.IdentifierTree;
2526
import com.sun.source.tree.MemberSelectTree;
@@ -37,6 +38,7 @@
3738
import javax.lang.model.element.ExecutableElement;
3839
import javax.lang.model.element.Name;
3940
import javax.lang.model.element.Parameterizable;
41+
import javax.lang.model.element.QualifiedNameable;
4042
import javax.lang.model.element.TypeParameterElement;
4143
import javax.lang.model.type.IntersectionType;
4244
import javax.lang.model.type.TypeMirror;
@@ -59,6 +61,9 @@ public class NullnessAnnotations {
5961
+ "ProtoMethodMayReturnNull|ProtoMethodAcceptsNullParameter|"
6062
+ "ProtoPassThroughNullness")
6163
.asMatchPredicate();
64+
private static final ImmutableSet<String> FALSE_NULLNESS_ANNOTATIONS =
65+
ImmutableSet.of(
66+
"jakarta.validation.constraints.NotNull", "javax.validation.constraints.NotNull");
6267

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

@@ -83,12 +88,13 @@ public static boolean annotationsAreAmbiguous(
8388
public static ImmutableList<AnnotationMirror> annotationsRelevantToNullness(
8489
Collection<? extends AnnotationMirror> annotations) {
8590
return annotations.stream()
86-
.filter(a -> ANNOTATION_RELEVANT_TO_NULLNESS.test(simpleName(a).toString()))
91+
.filter(NullnessAnnotations::isAnnotationRelevant)
8792
.collect(toImmutableList());
8893
}
8994

9095
public static ImmutableList<AnnotationTree> annotationsRelevantToNullness(
9196
List<? extends AnnotationTree> annotations) {
97+
// Missing support for FALSE_NULLNESS_ANNOTATIONS
9298
return annotations.stream()
9399
.filter(a -> ANNOTATION_RELEVANT_TO_NULLNESS.test(simpleName(a)))
94100
.collect(toImmutableList());
@@ -209,7 +215,10 @@ public static Optional<Nullness> getUpperBound(TypeVariable typeVar) {
209215

210216
private static Optional<Nullness> fromAnnotationStream(
211217
Stream<? extends AnnotationMirror> annotations) {
212-
return fromAnnotationSimpleNames(annotations.map(a -> simpleName(a).toString()));
218+
return fromAnnotationSimpleNames(
219+
annotations
220+
.filter(NullnessAnnotations::isAnnotationRelevant)
221+
.map(a -> simpleName(a).toString()));
213222
}
214223

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

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)