From 5480b565a5955ba7ad972cbd414b6b2220d971e4 Mon Sep 17 00:00:00 2001 From: Georgii Ustinov Date: Thu, 2 Oct 2025 14:36:35 +0000 Subject: [PATCH] [Java. Inspections] IDEA-375132 Nullable return incompatibilities Basic implementation. MR brings detection of nullable inconsistencies inside return expressions for generic types. Now it supports - Support of arbitrary generic type (previously only collections was supported) - Recursive detection of inconsistencies - Detection of assigning not-null to nullable (via option) GitOrigin-RevId: ed37ea02ca44db58698f36f2ca82c67171733e69 --- .../messages/JavaAnalysisBundle.properties | 2 + .../dataFlow/NullabilityUtil.java | 6 +- .../nullable/NullableStuffInspectionBase.java | 21 ++- .../nullable/NullableStuffInspection.java | 4 +- .../impl/analysis/JavaGenericsUtil.java | 64 +++++++- .../src/com/intellij/psi/GenericsUtil.java | 42 +++++- .../util/JavaTypeNullabilityUtil.java | 138 ++++++++++++++++++ ...lableCollectionWhereNotNullIsExpected.java | 4 +- ...ullableMapValueWhereNotNullIsExpected.java | 2 +- .../ReturnIncompatibilitiesWithGeneric.java | 87 +++++++++++ .../JSpecifyFilteredAnnotationTest.java | 41 ++---- .../NullableStuffInspectionTest.java | 8 +- .../resources/messages/JavaBundle.properties | 1 + 13 files changed, 373 insertions(+), 47 deletions(-) create mode 100644 java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java diff --git a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties index af95396d80b6..831ed144adc9 100644 --- a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties +++ b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties @@ -128,6 +128,8 @@ annotate.overridden.methods.parameters=Annotate overriding method parameters as anonymous.ref.loc.can.be.replaced.with.0=Anonymous #ref #loc can be replaced with {0} anonymous.ref.loc.can.be.replaced.with.lambda=Anonymous #ref #loc can be replaced with lambda assigning.a.collection.of.nullable.elements=Assigning a collection of nullable elements into a collection of non-null elements +returning.a.class.with.nullable.parameters=Returning a class with nullable type parameters when a class with non-null type parameters is expected +returning.a.class.with.notnull.parameters=Returning a class with non-null type parameters when a class with nullable type parameters is expected conflicting.nullability.annotations=Conflicting nullability annotations nullable.stuff.error.overriding.nullable.with.notnull=Overriding a collection of nullable elements with a collection of non-null elements nullable.stuff.error.overriding.notnull.with.nullable=Overriding a collection of non-null elements with a collection of nullable elements diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullabilityUtil.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullabilityUtil.java index dab177215ed0..c253fbf1f59e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullabilityUtil.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullabilityUtil.java @@ -1,9 +1,7 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInspection.dataFlow; -import com.intellij.codeInsight.JavaPsiEquivalenceUtil; -import com.intellij.codeInsight.Nullability; -import com.intellij.codeInsight.NullableNotNullManager; +import com.intellij.codeInsight.*; import com.intellij.codeInsight.daemon.ImplicitUsageProvider; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java index 12fb964c4ccb..bf1b7df988ef 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInspection.nullable; import com.intellij.codeInsight.*; @@ -35,6 +35,7 @@ import com.intellij.psi.search.searches.OverridingMethodsSearch; import com.intellij.psi.util.*; import com.intellij.util.ArrayUtil; import com.intellij.util.ArrayUtilRt; +import com.intellij.util.JavaTypeNullabilityUtil; import com.intellij.util.containers.ContainerUtil; import com.siyeh.ig.psiutils.TypeUtils; import org.jdom.Element; @@ -67,6 +68,7 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection @SuppressWarnings("WeakerAccess") public boolean IGNORE_EXTERNAL_SUPER_NOTNULL; @SuppressWarnings("WeakerAccess") public boolean REPORT_NOTNULL_PARAMETERS_OVERRIDES_NOT_ANNOTATED; @SuppressWarnings("WeakerAccess") public boolean REPORT_NULLABILITY_ANNOTATION_ON_LOCALS = true; + @SuppressWarnings("WeakerAccess") public boolean REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = false; /** * @deprecated the field remains to minimize changes to users' inspection profiles. */ @@ -93,7 +95,8 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection "REPORT_NOTNULL_PARAMETERS_OVERRIDES_NOT_ANNOTATED".equals(name) && "false".equals(value) || "REQUIRE_NOTNULL_FIELDS_INITIALIZED".equals(name) && "true".equals(value) || "REPORT_NULLABILITY_ANNOTATION_ON_LOCALS".equals(name) && "true".equals(value) || - "REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER".equals(name) && "true".equals(value)) { + "REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER".equals(name) && "true".equals(value) || + "REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS".equals(name) && "false".equals(value)) { node.removeContent(child); } } @@ -474,7 +477,7 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection PsiExpression returnValue = statement.getReturnValue(); if (returnValue == null) return; - checkCollectionNullityOnAssignment(statement.getReturnValue(), PsiTypesUtil.getMethodReturnType(statement), returnValue); + checkGenericClassOnReturn(PsiTypesUtil.getMethodReturnType(statement), returnValue); } @Override @@ -505,6 +508,18 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection } } + private void checkGenericClassOnReturn(@Nullable PsiType expectedType, + @NotNull PsiExpression returnValue) { + PsiType returnType = returnValue.getType(); + JavaTypeNullabilityUtil.NullabilityConflict + conflict = JavaTypeNullabilityUtil.getNullabilityConflictInAssignment(expectedType, returnType, + REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS); + if (conflict == JavaTypeNullabilityUtil.NullabilityConflict.UNKNOWN) return; + String messageKey = conflict == JavaTypeNullabilityUtil.NullabilityConflict.NOT_NULL_TO_NULL ? + "returning.a.class.with.notnull.parameters" : "returning.a.class.with.nullable.parameters"; + reportProblem(holder, returnValue, messageKey); + } + private void checkCollectionNullityOnAssignment(@NotNull PsiElement errorElement, @Nullable PsiType expectedType, @Nullable PsiExpression assignedExpression) { diff --git a/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java b/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java index 399641b8cf72..a72052d74bf1 100644 --- a/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInspection.nullable; import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo; @@ -45,6 +45,8 @@ public class NullableStuffInspection extends NullableStuffInspectionBase { checkbox("REPORT_NOT_ANNOTATED_GETTER", JavaBundle.message("inspection.nullable.problems.not.annotated.getters.for.annotated.fields")), checkbox("REPORT_NULLS_PASSED_TO_NOT_NULL_PARAMETER", JavaBundle.message("inspection.nullable.problems.notnull.parameters.with.null.literal.option")), checkbox("REPORT_NULLABILITY_ANNOTATION_ON_LOCALS", JavaBundle.message("inspection.nullable.problems.nullability.on.locals")), + checkbox("REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS", JavaBundle.message( + "inspection.nullable.problems.notnull.to.nullable.assignment.conflicts")), JavaInspectionControls.button(JavaInspectionButtons.ButtonKind.NULLABILITY_ANNOTATIONS) ); } diff --git a/java/java-psi-api/src/com/intellij/codeInsight/daemon/impl/analysis/JavaGenericsUtil.java b/java/java-psi-api/src/com/intellij/codeInsight/daemon/impl/analysis/JavaGenericsUtil.java index 90a337cf417c..2b36907fa93a 100644 --- a/java/java-psi-api/src/com/intellij/codeInsight/daemon/impl/analysis/JavaGenericsUtil.java +++ b/java/java-psi-api/src/com/intellij/codeInsight/daemon/impl/analysis/JavaGenericsUtil.java @@ -1,20 +1,16 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInsight.daemon.impl.analysis; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; -import com.intellij.psi.util.InheritanceUtil; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.util.PsiUtil; -import com.intellij.psi.util.TypeConversionUtil; +import com.intellij.psi.util.*; +import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; +import java.util.*; import static com.intellij.codeInsight.AnnotationUtil.CHECK_EXTERNAL; @@ -325,6 +321,58 @@ public final class JavaGenericsUtil { return getCollectionItemType(expression.getType(), expression.getResolveScope()); } + + /** + * Substitutes all values for type parameters of the {@code baseType} from the {@code derivedType}. + * + * @return null if the calculation wasn't successful, list of substituted types otherwise. + * + * @see JavaGenericsUtil#getCollectionItemType(PsiExpression) + */ + public static @Nullable List<@NotNull PsiType> getParentParameterTypeListFromDerivedType(@Nullable PsiType baseType, @Nullable PsiType derivedType) { + if (derivedType instanceof PsiClassType) { + PsiClass baseClass = PsiTypesUtil.getPsiClass(baseType); + if (baseClass == null) return null; + final PsiClassType.ClassResolveResult resolveResult = getDerivedClassTypeResolveResult((PsiClassType)derivedType); + PsiClass derivedClass = resolveResult.getElement(); + if (derivedClass == null) return null; + PsiSubstitutor substitutor = resolveResult.getSubstitutor(); + PsiTypeParameter[] parameters = baseClass.getTypeParameters(); + PsiSubstitutor superClassSubstitutor = TypeConversionUtil.getClassSubstitutor(baseClass, derivedClass, substitutor); + if (superClassSubstitutor == null) return null; + return ContainerUtil.map( + parameters, typeParameter -> { + PsiType substitutedType = superClassSubstitutor.substitute(typeParameter); + return substitutedType == null ? PsiType.getJavaLangObject(derivedClass.getManager(), derivedClass.getResolveScope()) : substitutedType; + } + ); + } else if (derivedType instanceof PsiIntersectionType) { + for (PsiType conjunct : ((PsiIntersectionType)derivedType).getConjuncts()) { + List<@NotNull PsiType> candidates = getParentParameterTypeListFromDerivedType(baseType, conjunct); + if (candidates != null) return candidates; + } + } + return null; + } + + /** + * Retrieves the resolve result corresponding to the given {@code derivedType}. If the initial resolved + * result is a type parameter with an upper bound, then the upper bound is returned, otherwise the initial resolve result. + */ + private static @NotNull PsiClassType.ClassResolveResult getDerivedClassTypeResolveResult(PsiClassType derivedType) { + final PsiClassType.ClassResolveResult resolveResult = derivedType.resolveGenerics(); + PsiClass derivedClass = resolveResult.getElement(); + if (derivedClass instanceof PsiTypeParameter) { + PsiTypeParameter typeParameter = (PsiTypeParameter)derivedClass; + PsiClassType[] types = typeParameter.getExtendsListTypes(); + if (types.length > 1) return PsiClassType.ClassResolveResult.EMPTY; + else if (types.length == 1) { + return types[0].resolveGenerics(); + } + } + return resolveResult; + } + public static @Nullable PsiType getCollectionItemType(@Nullable PsiType type, @NotNull GlobalSearchScope scope) { if (type instanceof PsiArrayType) { return ((PsiArrayType)type).getComponentType(); diff --git a/java/java-psi-api/src/com/intellij/psi/GenericsUtil.java b/java/java-psi-api/src/com/intellij/psi/GenericsUtil.java index 019470bbcc8f..e4648ac440fe 100644 --- a/java/java-psi-api/src/com/intellij/psi/GenericsUtil.java +++ b/java/java-psi-api/src/com/intellij/psi/GenericsUtil.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.psi; import com.intellij.codeInsight.TypeNullability; @@ -693,4 +693,44 @@ public final class GenericsUtil { method.isConstructor() || method.hasModifierProperty(PsiModifier.PRIVATE) && PsiUtil.getLanguageLevel(method).isAtLeast(LanguageLevel.JDK_1_9); } + + /** + * Checks whether a given type is a wildcard type with explicit extends bound. + * + * @param type the type to check + * @return {@code true} if the type is a wildcard type with explicit extends bound, {@code false} otherwise + */ + @Contract("null -> false") + public static boolean isWildcardWithExtendsBound(@Nullable PsiType type) { + if (type instanceof PsiWildcardType) { + PsiWildcardType wildcardType = (PsiWildcardType)type; + return wildcardType.isExtends(); + } else if (type instanceof PsiCapturedWildcardType) { + PsiCapturedWildcardType wildcardType = (PsiCapturedWildcardType)type; + return isWildcardWithExtendsBound(wildcardType.getWildcard()); + } + return false; + } + + /** + * Calculates the type bounded to the given wildcard type. + * + * @param type wildcard type to calculate bounded type for + * @return bounded type if {@code type} is bounded, {@code null} otherwise + */ + @Contract("null -> null") + public static @Nullable PsiType getWildcardBound(@Nullable PsiType type) { + if (type instanceof PsiWildcardType) { + PsiWildcardType wildcardType = (PsiWildcardType)type; + if (wildcardType.isExtends()) return wildcardType.getExtendsBound(); + if (wildcardType.isSuper()) return wildcardType.getSuperBound(); + } else if (type instanceof PsiCapturedWildcardType) { + PsiCapturedWildcardType wildcardType = (PsiCapturedWildcardType)type; + return getWildcardBound(wildcardType.getWildcard()); + } + return null; + } } diff --git a/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java b/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java index 3dd84f53bf13..be092e147a59 100644 --- a/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java +++ b/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java @@ -2,7 +2,9 @@ package com.intellij.util; import com.intellij.codeInsight.*; +import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil; import com.intellij.psi.*; +import com.intellij.psi.util.TypeConversionUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -91,4 +93,140 @@ public final class JavaTypeNullabilityUtil { } return TypeNullability.UNKNOWN; } + + /** + * Checks whether {@code rightType} can be assigned into {@code leftType} from the point of nullability in the type parameters. + * @param leftType type to assign to + * @param rightType assigned value + * @param checkNotNullToNull whether to check for nullability conflict when assigning not-null to null value + * + * @see JavaTypeNullabilityUtil#getNullabilityConflictType(PsiType, PsiType) + */ + public static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType, + @Nullable PsiType rightType, + boolean checkNotNullToNull) { + return getNullabilityConflictInAssignment(leftType, rightType, checkNotNullToNull, false); + } + + private static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType, + @Nullable PsiType rightType, + boolean checkNotNullToNull, + boolean checkConflictInInitialType) { + if (checkConflictInInitialType) { + NullabilityConflict nullabilityConflict = getNullabilityConflictType(leftType, rightType); + if (isAllowedNullabilityConflictType(checkNotNullToNull, nullabilityConflict)) return nullabilityConflict; + } + + if (leftType == null || TypeConversionUtil.isNullType(leftType) || + rightType == null || TypeConversionUtil.isNullType(rightType) + ) return NullabilityConflict.UNKNOWN; + + if (rightType instanceof PsiIntersectionType) { + return getNullabilityConflictInTypeArguments(leftType, rightType, checkNotNullToNull); + } + + if (rightType instanceof PsiCapturedWildcardType) { + return getNullabilityConflictInAssignment(leftType, ((PsiCapturedWildcardType)rightType).getUpperBound(true), checkNotNullToNull, false); + } + if (leftType instanceof PsiCapturedWildcardType) { + return getNullabilityConflictInAssignment(((PsiCapturedWildcardType)leftType).getLowerBound(), rightType, checkNotNullToNull, false); + } + + if (leftType instanceof PsiWildcardType) { + return getNullabilityConflictInAssignment(GenericsUtil.getWildcardBound(leftType), rightType, checkNotNullToNull, false); + } + if (rightType instanceof PsiWildcardType) { + return getNullabilityConflictInAssignment(leftType, GenericsUtil.getWildcardBound(rightType), checkNotNullToNull, false); + } + + if (leftType instanceof PsiArrayType && rightType instanceof PsiArrayType) { + return getNullabilityConflictInAssignment(((PsiArrayType)leftType).getComponentType(), + ((PsiArrayType)rightType).getComponentType(), checkNotNullToNull, true); + } + + if (!(leftType instanceof PsiClassType) || !(rightType instanceof PsiClassType)) { + return NullabilityConflict.UNKNOWN; + } + + return getNullabilityConflictInTypeArguments(leftType, rightType, checkNotNullToNull); + } + + /** + * Finds first inconsistencies in nullability inside generic class type arguments. + * For example, {@code GenericClass<@NotNull A, @Nullable B, @NotNull C> = GenericClass<@Nullable A, @Nullable B, @NotNull C>} will have + * conflicts in the first and third type arguments. + *

+ * Note: this method also treats intersection type as type with arguments. + * @param checkNotNullToNull whether to check for nullability conflict when assigning not-null to null value. + * @return first inconsistency in nullability inside generic class type arguments. + */ + private static @NotNull NullabilityConflict getNullabilityConflictInTypeArguments(@NotNull PsiType leftType, + @NotNull PsiType rightType, + boolean checkNotNullToNull) { + List leftParameterTypeList = JavaGenericsUtil.getParentParameterTypeListFromDerivedType(leftType, leftType); + List rightParameterTypeList = JavaGenericsUtil.getParentParameterTypeListFromDerivedType(leftType, rightType); + if (leftParameterTypeList == null || + rightParameterTypeList == null || + leftParameterTypeList.size() != rightParameterTypeList.size()) { + return NullabilityConflict.UNKNOWN; + } + + for (int i = 0; i < leftParameterTypeList.size(); i++) { + PsiType leftParameterType = leftParameterTypeList.get(i); + PsiType rightParameterType = rightParameterTypeList.get(i); + + NullabilityConflict nullabilityConflict = getNullabilityConflictInAssignment( + leftParameterType, + rightParameterType, + checkNotNullToNull, + true + ); + if (nullabilityConflict != NullabilityConflict.UNKNOWN) return nullabilityConflict; + } + + return NullabilityConflict.UNKNOWN; + } + + private static boolean isAllowedNullabilityConflictType(boolean checkNotNullToNull, @NotNull NullabilityConflict nullabilityConflict) { + return nullabilityConflict != NullabilityConflict.UNKNOWN && + (checkNotNullToNull || nullabilityConflict != NullabilityConflict.NOT_NULL_TO_NULL); + } + + /** + * Checks whether {@code rightType} can be assigned into {@code leftType} from the point of nullability. + * @param leftType type to assign to + * @param rightType assigned value + * @see NullabilityConflict + */ + public static @NotNull NullabilityConflict getNullabilityConflictType(@Nullable PsiType leftType, @Nullable PsiType rightType) { + if (leftType == null || rightType == null) return NullabilityConflict.UNKNOWN; + TypeNullability leftTypeNullability = leftType.getNullability(); + TypeNullability rightTypeNullability = rightType.getNullability(); + Nullability leftNullability = leftTypeNullability.nullability(); + Nullability rightNullability = rightTypeNullability.nullability(); + + if (leftNullability == Nullability.NOT_NULL && rightNullability == Nullability.NULLABLE) return NullabilityConflict.NULL_TO_NOT_NULL; + // It is not possible to have NOT_NULL_TO_NULL conflict when left type is wildcard with upper bound, + // e.g., this assignment is legal {@code List = List<@NotNull String>} + else if (leftNullability == Nullability.NULLABLE && rightNullability == Nullability.NOT_NULL && !GenericsUtil.isWildcardWithExtendsBound(leftType)) return NullabilityConflict.NOT_NULL_TO_NULL; + return NullabilityConflict.UNKNOWN; + } + + /** + * Represents a conflict in nullability between 2 types + */ + public enum NullabilityConflict { + /** + * Attempt to assign null to a not-null type, example: {@code Container<@NotNull T> c = Container<@Nullable T>} + */ + NULL_TO_NOT_NULL, + /** + * Attempt to assign not-null to a null type, example: {@code Container<@Nullable> c <- Container<@NotNull T>} + */ + NOT_NULL_TO_NULL, + /** + * There is no conflict or it is unknown + */ + UNKNOWN + } } diff --git a/java/java-tests/testData/inspection/nullableProblems/PassingNullableCollectionWhereNotNullIsExpected.java b/java/java-tests/testData/inspection/nullableProblems/PassingNullableCollectionWhereNotNullIsExpected.java index 0b37349104d2..a08c8f55d33c 100644 --- a/java/java-tests/testData/inspection/nullableProblems/PassingNullableCollectionWhereNotNullIsExpected.java +++ b/java/java-tests/testData/inspection/nullableProblems/PassingNullableCollectionWhereNotNullIsExpected.java @@ -31,9 +31,9 @@ class JC { Supplier> supplier = () -> list; Supplier> supplierRef = this::getNullableList; - Supplier> supplier3 = () -> { return list;}; + Supplier> supplier3 = () -> { return list;}; - return list; + return list; } } diff --git a/java/java-tests/testData/inspection/nullableProblems/PassingNullableMapValueWhereNotNullIsExpected.java b/java/java-tests/testData/inspection/nullableProblems/PassingNullableMapValueWhereNotNullIsExpected.java index 6e3e07e6c9b2..8c84e78c925a 100644 --- a/java/java-tests/testData/inspection/nullableProblems/PassingNullableMapValueWhereNotNullIsExpected.java +++ b/java/java-tests/testData/inspection/nullableProblems/PassingNullableMapValueWhereNotNullIsExpected.java @@ -9,6 +9,6 @@ class Test { @NotNull Map<@NotNull String, @NotNull ArrayList<@Nullable String>> map = new HashMap<>(); map.put("x", arrayList); - return map; + return map; } } \ No newline at end of file diff --git a/java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java b/java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java new file mode 100644 index 000000000000..f93dbe3a80f2 --- /dev/null +++ b/java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java @@ -0,0 +1,87 @@ +import org.jspecify.annotations.NotNull; +import org.jspecify.annotations.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Collection; + +class B { + B<@NotNull String> simpleNullableToNotNull(B<@Nullable String> arg) { + return arg; + } + + B<@Nullable String> simpleNotNullToNullable(B<@NotNull String> arg) { + return arg; + } + + B> nested(B> arg) { + return arg; + } + + B extendsWildcardNullable(B<@NotNull Object> arg) { + return arg; + } + + B extendsWildcardNotNull(B<@Nullable String> arg) { + return arg; + } + + B SupperWildcard(B<@Nullable Object> arg) { + return arg; + } + + B extendsWildcardBothNotNull(B arg) { + return arg; + } + + B extendsWildcardBothNullable(B arg) { + return arg; + } + + B superWildCardBoth(B arg) { + return arg; + } + + B> nestedWithWildcard(B> arg) { + return arg; + } + + Map, List<@Nullable String>> checkIsPerformedIfSecondTypeArgumentIsTheSame(Map, List<@NotNull String>> arg) { + return arg; + } + + B<@NotNull String>[] array(B<@Nullable String>[] arg) { + return arg; + } + + Object[] @NotNull [] nullabilityInNestedArray(Object[] @Nullable [] arg) { + return arg; + } + + B<@NotNull String>[][] multiDimensionalArray(B<@Nullable String>[][] arg) { + return arg; + } + + static class C { + C secondArgument(C arg) { + return arg; + } + } + + interface I {} + + List<@Nullable String> intersectionSimple(Object arg) { + return (List<@NotNull String> & I) arg; + } + + Collection<@Nullable String> intersectionTreatsFirstMatchingUnknown(Object arg) { + return (Collection & List<@NotNull String> & I) arg; + } + + Collection<@Nullable String> intersectionTreatsFirstMatchingMismatch(Object arg) { + return (Collection<@NotNull String> & List<@NotNull String> & I) arg; + } + + List> nestedIntersection(Object arg) { + return (List> & I) arg; + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java index 4268457017ec..f6e906028703 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java @@ -173,9 +173,18 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT return false; } }, + + new SkipIndividuallyFilter( // see: IDEA-375132 + Set.of( + new Pair<>("NotNullMarkedUseOfWildcardAsTypeArgument.java", 30), + new Pair<>("SameTypeTypeVariable.java", 31), + new Pair<>("SameTypeTypeVariable.java", 51), + new Pair<>("SuperVsObject.java", 24), // see: IDEA-379303 + new Pair<>("SuperNullableForNonNullableTypeParameter.java", 27) // see: IDEA-379303 + ) + ), new CallWithParameterWithNestedGenericsFilter(), // see: IDEA-377682 - new VariableWithNestedGenericsFilter(), // see: IDEA-377683 - new ReturnWithNestedGenericsFilter() // see: IDEA-375132 + new VariableWithNestedGenericsFilter() // see: IDEA-377683 ); private static final LightProjectDescriptor PROJECT_DESCRIPTOR = new DefaultLightProjectDescriptor() { @@ -239,6 +248,7 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT var dfaInspection = new JSpecifyDataFlowInspection(actual); dfaInspection.TREAT_UNKNOWN_MEMBERS_AS_NULLABLE = true; var nullableStuffInspection = new JSpecifyNullableStuffInspection(actual); + nullableStuffInspection.REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = true; var notNullFieldNotInitializedInspection = new JSpecifyNotNullFieldNotInitializedInspection(actual); List inspections = List.of(dfaInspection, nullableStuffInspection, notNullFieldNotInitializedInspection); ReadAction.run(() -> { @@ -425,29 +435,6 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT } } - private static class ReturnWithNestedGenericsFilter implements ErrorFilter { - - @Override - public boolean filterActual(@NotNull PsiFile file, - @NotNull String strippedText, - int lineNumber, - int startLineOffset, - @NotNull String errorMessage) { - if (!errorMessage.contains("jspecify_nullness_mismatch")) return false; - PsiElement element = findElement(file, strippedText, lineNumber, startLineOffset); - PsiReturnStatement returnStatement = PsiTreeUtil.getParentOfType(element, PsiReturnStatement.class, true); - if (returnStatement == null) return false; - PsiMethod method = PsiTreeUtil.getParentOfType(returnStatement, PsiMethod.class, true); - return method.getReturnType() instanceof PsiClassType classType && classType.hasParameters(); - } - - @Override - public boolean filterExpected(@NotNull PsiElement psiElement, @NotNull String errorMessage) { - //filter only actual file - return false; - } - } - private static class SkipIndividuallyFilter implements ErrorFilter { private final Set> places; @@ -593,7 +580,9 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT "inspection.nullable.problems.at.local.variable" -> warnings.put(anchor, "jspecify_unrecognized_location"); case "inspection.nullable.problems.Nullable.method.overrides.NotNull", "inspection.nullable.problems.NotNull.parameter.overrides.Nullable", - "assigning.a.collection.of.nullable.elements" + "assigning.a.collection.of.nullable.elements", + "returning.a.class.with.nullable.parameters", + "returning.a.class.with.notnull.parameters" //, "non.null.type.argument.is.expected" //todo see IDEA-377707 -> warnings.put(anchor, "jspecify_nullness_mismatch"); case "inspection.nullable.problems.method.overrides.NotNull", "inspection.nullable.problems.parameter.overrides.NotNull" -> diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java index b7dc7792cade..1039f465094f 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.java.codeInspection; @@ -476,4 +476,10 @@ public class NullableStuffInspectionTest extends LightJavaCodeInsightFixtureTest addJSpecifyNullMarked(myFixture); doTest(); } + + public void testReturnIncompatibilitiesWithGeneric() { + myInspection.REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = true; + setupTypeUseAnnotations("org.jspecify.annotations", myFixture); + doTest(); + } } \ No newline at end of file diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 3c633ba0738d..b487cb4f994d 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -560,6 +560,7 @@ inspection.nullable.problems.not.annotated.getters.for.annotated.fields=Report n inspection.nullable.problems.notnull.overrides.option=Report @NotNull ¶meters overriding non-annotated inspection.nullable.problems.notnull.parameters.with.null.literal.option=Report @NotNull parameters with null-literal argument usages inspection.nullable.problems.nullability.on.locals=Report nullability annotations on local variables when they are non-applicable +inspection.nullable.problems.notnull.to.nullable.assignment.conflicts=Report assignment of a not null type parameter to nullable type parameter inspection.optional.get.without.is.present.message={0}.#ref() without ''isPresent()'' check inspection.optional.get.without.is.present.method.reference.message=#ref without 'isPresent()' check inspection.overflowing.loop.index.inspection.description=Loop executes zero or billions of times