From b628e41173a4c1ea19ab63b9561c016243cc1631 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Wed, 14 May 2025 15:20:17 +0200 Subject: [PATCH] [java-dfa] Support type hierarchy for parameters Fixes IDEA-371133 Missing warning when passing null to parameter of non-nullable generic type GitOrigin-RevId: dff653e1e85d7633b744dc2ffbcdb140ac24fe4e --- .../codeInspection/dataFlow/DfaPsiUtil.java | 69 ++++++++++++------- .../JSpecifyCallExplicitTypeParameters.java | 21 ++++++ .../fixture/JetBrainsNotNullByDefault.java | 3 +- .../DataFlowInspection21Test.java | 6 ++ 4 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 java/java-tests/testData/inspection/dataFlow/fixture/JSpecifyCallExplicitTypeParameters.java diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaPsiUtil.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaPsiUtil.java index c98ff2e10ed6..a04a4024f17f 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaPsiUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaPsiUtil.java @@ -119,15 +119,16 @@ public final class DfaPsiUtil { if (DumbService.isDumb(owner.getProject())) return Nullability.UNKNOWN; NullabilityAnnotationInfo fromAnnotation = getNullabilityFromAnnotation(owner, ignoreParameterNullabilityInference); if (fromAnnotation != null) { - if (fromAnnotation.getNullability() != Nullability.NOT_NULL && - owner instanceof PsiMethod method) { - PsiType type = method.getReturnType(); - PsiAnnotationOwner annotationOwner = fromAnnotation.getAnnotation().getOwner(); - if (PsiUtil.resolveClassInClassTypeOnly(type) instanceof PsiTypeParameter && - annotationOwner instanceof PsiType && annotationOwner != type) { - // Nullable/Unknown from type hierarchy: should check the instantiation, as it could be more concrete - Nullability fromType = getNullabilityFromType(resultType, owner); - if (fromType != null) return fromType; + if (fromAnnotation.getNullability() != Nullability.NOT_NULL) { + PsiType type = PsiUtil.getTypeByPsiElement(owner); + if (type != null) { + PsiAnnotationOwner annotationOwner = fromAnnotation.getAnnotation().getOwner(); + if (PsiUtil.resolveClassInClassTypeOnly(type) instanceof PsiTypeParameter && + annotationOwner instanceof PsiType && annotationOwner != type) { + // Nullable/Unknown from type hierarchy: should check the instantiation, as it could be more concrete + Nullability fromType = getNullabilityFromType(resultType, owner); + if (fromType != null) return fromType; + } } } return fromAnnotation.getNullability(); @@ -260,7 +261,40 @@ public final class DfaPsiUtil { } private static @Nullable NullabilityAnnotationInfo getTypeOwnNullability(@NotNull PsiType eachType, boolean local) { - for (PsiAnnotation annotation : eachType.getAnnotations()) { + NullabilityAnnotationInfo info = getNullabilityFromAnnotations(eachType.getAnnotations()); + if (info != null) return info; + if (eachType instanceof PsiClassType classType && !local) { + PsiElement context = classType.getPsiContext(); + if (context != null) { + NullableNotNullManager manager = NullableNotNullManager.getInstance(context.getProject()); + if (context instanceof PsiJavaCodeReferenceElement ref && ref.getParent() instanceof PsiTypeElement typeElement) { + // Due to the absence of nullability type inference, the annotation that presents directly at context + // could be removed from eachType. In this case, using context annotation would be wrong, so we just return null + info = getNullabilityFromAnnotations(typeElement.getApplicableAnnotations()); + } + NullabilityAnnotationInfo typeUseNullability = manager.findDefaultTypeUseNullability(context); + if (typeUseNullability != null) { + if (info != null && info.getNullability() != typeUseNullability.getNullability()) { + return null; + } + return typeUseNullability; + } + PsiClass declaration = PsiUtil.resolveClassInClassTypeOnly(classType); + if (declaration instanceof PsiTypeParameter typeParameter && typeParameter.getExtendsList().getReferenceElements().length == 0) { + // If there's no bound, we assume an implicit `extends Object` bound, which is subject to default annotation if any. + typeUseNullability = manager.findDefaultTypeUseNullability(declaration); + if (info != null && typeUseNullability != null && info.getNullability() != typeUseNullability.getNullability()) { + return null; + } + return typeUseNullability; + } + } + } + return null; + } + + private static @Nullable NullabilityAnnotationInfo getNullabilityFromAnnotations(PsiAnnotation[] annotations) { + for (PsiAnnotation annotation : annotations) { String qualifiedName = annotation.getQualifiedName(); NullableNotNullManager nnn = NullableNotNullManager.getInstance(annotation.getProject()); Optional optionalNullability = nnn.getAnnotationNullability(qualifiedName); @@ -270,21 +304,6 @@ public final class DfaPsiUtil { return new NullabilityAnnotationInfo(annotation, nullability, false); } } - if (eachType instanceof PsiClassType classType && !local) { - PsiElement context = classType.getPsiContext(); - if (context != null) { - NullableNotNullManager manager = NullableNotNullManager.getInstance(context.getProject()); - NullabilityAnnotationInfo typeUseNullability = manager.findDefaultTypeUseNullability(context); - if (typeUseNullability != null) { - return typeUseNullability; - } - PsiClass declaration = PsiUtil.resolveClassInClassTypeOnly(classType); - if (declaration instanceof PsiTypeParameter typeParameter && typeParameter.getExtendsList().getReferenceElements().length == 0) { - // If there's no bound, we assume an implicit `extends Object` bound, which is subject to default annotation if any. - return manager.findDefaultTypeUseNullability(declaration); - } - } - } return null; } diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/JSpecifyCallExplicitTypeParameters.java b/java/java-tests/testData/inspection/dataFlow/fixture/JSpecifyCallExplicitTypeParameters.java new file mode 100644 index 000000000000..a82a92576649 --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/JSpecifyCallExplicitTypeParameters.java @@ -0,0 +1,21 @@ +import org.jspecify.annotations.NotNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class Demo { + @NullMarked + static class Test { + static void test(T value) { + } + } + + void test() { + Test.<@NotNull String>test(null); + Test.<@Nullable String>test(null); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/JetBrainsNotNullByDefault.java b/java/java-tests/testData/inspection/dataFlow/fixture/JetBrainsNotNullByDefault.java index 83539b927e3b..5690a4ee6e30 100644 --- a/java/java-tests/testData/inspection/dataFlow/fixture/JetBrainsNotNullByDefault.java +++ b/java/java-tests/testData/inspection/dataFlow/fixture/JetBrainsNotNullByDefault.java @@ -28,8 +28,7 @@ class FromDemo { public void test(@Nullable Integer t1, @Nullable Integer t2) { Integer o = new FromDemo().oneOfTwo(t1, t2); - // TODO: should not warn - if (o == null) { + if (o == null) { System.out.println("1"); } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection21Test.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection21Test.java index 78c721726a51..6aa6ac03a45b 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection21Test.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection21Test.java @@ -162,6 +162,12 @@ public class DataFlowInspection21Test extends DataFlowInspectionTestCase { doTest(); } + public void testJSpecifyCallExplicitTypeParameters() { + addJSpecifyNullMarked(myFixture); + setupTypeUseAnnotations("org.jspecify.annotations", myFixture); + doTest(); + } + public void testJSpecifyGetOrDefault() { addJSpecifyNullMarked(myFixture); setupTypeUseAnnotations("org.jspecify.annotations", myFixture);