From 6a33175cfe8799b1cd99563243054dbb82e1475c Mon Sep 17 00:00:00 2001 From: Georgii Ustinov Date: Mon, 27 Oct 2025 18:11:39 +0100 Subject: [PATCH] [Java. Inspections] IDEA-381448 Show exact place for nullability conflict in return statements GitOrigin-RevId: fe03e0582080b4717f7ee85f68b581780518cec4 --- .../messages/JavaAnalysisBundle.properties | 11 +- .../nullable/NullableStuffInspectionBase.java | 31 ++- .../nullable/NullableStuffInspectionUtil.java | 200 ++++++++++++++++++ .../resources/META-INF/JavaPlugin.xml | 1 + .../nullable/NullableStuffInspection.java | 11 + .../util/JavaTypeNullabilityUtil.java | 113 +++++++--- .../ReturnIncompatibilitiesWithGeneric.java | 36 ++-- .../JSpecifyConformanceAnnotationTest.java | 11 +- .../JSpecifyFilteredAnnotationTest.java | 11 +- .../NullableStuffInspectionTest.java | 1 + platform/analysis-api/api-dump.txt | 4 + .../codeInspection/InspectionManager.java | 8 + .../codeInspection/ProblemsHolder.java | 20 +- platform/analysis-impl/api-dump.txt | 2 + .../codeInspection/InspectionManagerBase.java | 12 ++ 15 files changed, 405 insertions(+), 67 deletions(-) create mode 100644 java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionUtil.java diff --git a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties index f12e26a2bc25..99f30a38feaa 100644 --- a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties +++ b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties @@ -132,8 +132,15 @@ 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.arguments=Returning a class with nullable type arguments when a class with not-null type arguments is expected -returning.a.class.with.notnull.arguments=Returning a class with not-null type arguments when a class with nullable type arguments is expected +returning.a.class.with.nullable.arguments=\ + \ + Returning a class with nullable type arguments when a class with not-null type arguments is expected{0}\ + +returning.a.class.with.notnull.arguments=\ + \ + Returning a class with not-null type arguments when a class with nullable type arguments is expected{0}\ + +returning.a.type.nullability.conflict.message=Return type: 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/nullable/NullableStuffInspectionBase.java b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionBase.java index 24660d075f09..acf86cbaadc8 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 @@ -533,13 +533,16 @@ 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 ? + JavaTypeNullabilityUtil.NullabilityConflictContext + context = JavaTypeNullabilityUtil.getNullabilityConflictInAssignment(expectedType, returnType, + REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS); + if (context.nullabilityConflict == JavaTypeNullabilityUtil.NullabilityConflict.UNKNOWN) return; + String messageKey = context.nullabilityConflict == JavaTypeNullabilityUtil.NullabilityConflict.NOT_NULL_TO_NULL ? "returning.a.class.with.notnull.arguments" : "returning.a.class.with.nullable.arguments"; - reportProblem(holder, returnValue, messageKey); + + reportProblem(holder, returnValue, LocalQuickFix.EMPTY_ARRAY, + messageKey, new Object[]{""}, + messageKey, new Object[]{NullableStuffInspectionUtil.getTypePresentationInNullabilityConflict(context)}); } private void checkCollectionNullityOnAssignment(@NotNull PsiElement errorElement, @@ -587,9 +590,19 @@ public class NullableStuffInspectionBase extends AbstractBaseJavaLocalInspection protected void reportProblem(@NotNull ProblemsHolder holder, @NotNull PsiElement anchor, @NotNull LocalQuickFix @NotNull [] fixes, @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String messageKey, Object... args) { - holder.registerProblem(anchor, - JavaAnalysisBundle.message(messageKey, args), - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, fixes); + reportProblem(holder, anchor, fixes, messageKey, args, messageKey, args); + } + + protected void reportProblem(@NotNull ProblemsHolder holder, @NotNull PsiElement anchor, @NotNull LocalQuickFix @NotNull [] fixes, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String descriptionKey, @NotNull Object @NotNull[] descriptionArgs, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String tooltipKey, @NotNull Object @NotNull[] tooltipArgs) { + ProblemsHolder.ProblemBuilder builder = holder.problem(anchor, JavaAnalysisBundle.message(descriptionKey, descriptionArgs)) + .tooltip(JavaAnalysisBundle.message(tooltipKey, tooltipArgs)) + .highlight(ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + for (LocalQuickFix quickFix : fixes) { + builder.fix(quickFix); + } + builder.register(); } private static boolean isNullableNotNullCollectionConflict(@Nullable PsiType expectedType, diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionUtil.java b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionUtil.java new file mode 100644 index 000000000000..c3062c9d0aea --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspectionUtil.java @@ -0,0 +1,200 @@ +// 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.java.analysis.JavaAnalysisBundle; +import com.intellij.openapi.util.NlsSafe; +import com.intellij.openapi.util.text.HtmlChunk; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.JavaTypeNullabilityUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class NullableStuffInspectionUtil { + static @NotNull @NlsSafe String getTypePresentationInNullabilityConflict(@NotNull JavaTypeNullabilityUtil.NullabilityConflictContext context) { + PsiElement place = context.getPlace(); + if (place == null) return ""; + int dimensionsInArray = (context.type instanceof PsiArrayType type) ? type.getArrayDimensions() : 0; + + PsiTypeElement topmostType = PsiTreeUtil.getTopmostParentOfType(place, PsiTypeElement.class); + if (topmostType == null) return ""; + + PsiTypeElement target = PsiTreeUtil.getParentOfType(place, PsiTypeElement.class, false); + if (target == null) return ""; + + PsiType outerType = topmostType.getType(); + if (target == topmostType) { + // Presentation is not handling with the whole type + return ""; + } + + List path = computeTypeArgumentPath(topmostType, target, dimensionsInArray); + if (path == null) return ""; + + Context presentationContext = getPresentationContext(outerType, path, outerType instanceof PsiArrayType); + + String typeText = presentationContext.sb.toString(); + String annotationText = getAnnotationText(context); + if (presentationContext.position == null || annotationText == null) return ""; + HtmlChunk result = generateHtmlChunk(typeText, "@" + annotationText, presentationContext.position); + + return result.toString(); + } + + private static @Nullable String getAnnotationText(@NotNull JavaTypeNullabilityUtil.NullabilityConflictContext context) { + PsiAnnotation annotation = context.getAnnotation(); + if (annotation == null) return null; + PsiJavaCodeReferenceElement ref = annotation.getNameReferenceElement(); + if (ref == null) return null; + return ref.getReferenceName(); + } + + private static @NotNull HtmlChunk generateHtmlChunk(@NlsSafe String text, + @NlsSafe String annotationText, + int position) { + HtmlChunk result = HtmlChunk.p().child( + HtmlChunk.fragment( + HtmlChunk.text(JavaAnalysisBundle.message("returning.a.type.nullability.conflict.message")), + HtmlChunk.text(" "), + HtmlChunk.text(text.substring(0, position)), + HtmlChunk.tag("b").children(HtmlChunk.text(annotationText)), + HtmlChunk.text(" "), + HtmlChunk.text(text.substring(position)) + ) + ); + return result; + } + + private static @Nullable List<@NotNull Integer> computeTypeArgumentPath(@NotNull PsiTypeElement top, @NotNull PsiTypeElement target, int firstArrayDepth) { + List indices = new ArrayList<>(); + PsiTypeElement current = target; + boolean isFirstArrayOccurence = true; + while (true) { + PsiTypeElement parentTypeElement = PsiTreeUtil.getParentOfType(current, PsiTypeElement.class, true); + if (parentTypeElement == null) return null; + PsiType parentType = parentTypeElement.getType(); + if (parentType instanceof PsiClassType) { + PsiJavaCodeReferenceElement ref = parentTypeElement.getInnermostComponentReferenceElement(); + if (ref == null) return null; + PsiReferenceParameterList params = ref.getParameterList(); + if (params == null) return null; + PsiTypeElement[] args = params.getTypeParameterElements(); + int found = -1; + for (int i = 0; i < args.length; i++) { + if (args[i] == current) { + found = i; + break; + } + } + if (found < 0) return null; + indices.add(found); + } + else if (parentType instanceof PsiWildcardType || + parentType instanceof PsiCapturedWildcardType) { + indices.add(0); + } else if (parentType instanceof PsiArrayType arrayType) { + int candidateArrayDepth = arrayType.getArrayDimensions(); + int numberToAdd; + if (isFirstArrayOccurence && candidateArrayDepth == firstArrayDepth) { + numberToAdd = 0; + } else if (isFirstArrayOccurence && firstArrayDepth > 0) { + numberToAdd = firstArrayDepth; + } else { + numberToAdd = candidateArrayDepth; + } + for (int i = 0; i < numberToAdd; i++) { + indices.add(0); + } + } + if (parentTypeElement == top) break; + current = parentTypeElement; + isFirstArrayOccurence = false; + } + Collections.reverse(indices); + return indices; + } + + private static @NotNull Context getPresentationContext(@NotNull PsiType type, List path, boolean isInsideArray) { + Context context = new Context(); + buildTextRepresentation(type, context, path, 0, isInsideArray); + return context; + } + + /** + * Constructs the simplified presentable view of the type and detects the place in which the nullability annotation should be inserted. + * The result of this method is stored in the {@link Context#sb} member. + * @param isInsideArray - Corresponds to the state whether the method is called within the array dimension. It is used to distinguish + * cases {@code @Nullable String[]} and {@code String @Nullable [][]}. + */ + private static void buildTextRepresentation(@NotNull PsiType type, + @NotNull Context context, + @NotNull List<@NotNull Integer> path, + int depth, + boolean isInsideArray) { + if (depth == path.size()) { + if (type instanceof PsiArrayType arrayType && isInsideArray) { + context.sb.append(arrayType.getPresentableText(false)); + context.sb.append(" "); + context.position = context.sb.length(); + } + else if (type instanceof PsiArrayType arrayType) { + context.sb.append(arrayType.getDeepComponentType().getPresentableText(false)); + context.sb.append(" "); + context.position = context.sb.length(); + context.sb.repeat("[]", Math.max(0, arrayType.getArrayDimensions())); + } + else { + String text = type.getPresentableText(false); + context.position = context.sb.length(); + context.sb.append(text); + } + return; + } + if (type instanceof PsiArrayType arrayType) { + PsiType component = arrayType.getComponentType(); + buildTextRepresentation(component, context, path, depth + 1, true); + context.sb.append("[]"); + return; + } + if (type instanceof PsiWildcardType wc) { + String prefix = wc.isExtends() ? "? extends " : wc.isSuper() ? "? super " : "?"; + context.sb.append(prefix); + PsiType bound = wc.getBound(); + if (bound != null) { + buildTextRepresentation(bound, context, path, depth + 1, false); + } + return; + } + if (type instanceof PsiClassType cls) { + String name = cls.getClassName(); + if (name == null) name = cls.getPresentableText(false); + context.sb.append(name); + PsiType[] params = cls.getParameters(); + if (params.length > 0) { + context.sb.append('<'); + for (int i = 0; i < params.length; i++) { + if (i > 0) context.sb.append(", "); + if (i == path.get(depth)) { + buildTextRepresentation(params[i], context, path, depth + 1, false); + } + else { + context.sb.append(params[i].getPresentableText(false)); + } + } + context.sb.append('>'); + } + return; + } + + context.sb.append(type.getPresentableText(false)); + } + + private static final class Context { + private @Nullable Integer position = null; + private final @NotNull StringBuilder sb = new StringBuilder(); + } +} diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index ecf95a529137..522df70ffe67 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -2043,6 +2043,7 @@ implementationClass="com.intellij.codeInspection.wrongPackageStatement.WrongPackageStatementInspection"/> 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 b95e44f8e430..095ce25e4018 100644 --- a/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/nullable/NullableStuffInspection.java @@ -11,6 +11,7 @@ import com.intellij.codeInspection.options.OptPane; import com.intellij.codeInspection.options.OptionController; import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; import com.intellij.java.JavaBundle; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; @@ -25,6 +26,7 @@ import com.intellij.util.ArrayUtil; import com.intellij.util.Processor; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static com.intellij.codeInspection.options.OptPane.checkbox; import static com.intellij.codeInspection.options.OptPane.pane; @@ -54,6 +56,15 @@ public class NullableStuffInspection extends NullableStuffInspectionBase { ); } + @Override + public @Nullable @Nls String getStaticDescription() { + if (ApplicationManager.getApplication().isUnitTestMode()) { + return ""; + } else { + return null; + } + } + @Override public @NotNull OptionController getOptionController() { return super.getOptionController() 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 8c0b58ed79fa..9eb6eabfe956 100644 --- a/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java +++ b/java/java-psi-api/src/com/intellij/util/JavaTypeNullabilityUtil.java @@ -11,10 +11,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; /** * Helper methods to compute nullability of Java types. @@ -23,14 +20,14 @@ import java.util.Set; public final class JavaTypeNullabilityUtil { /** * Computes the class type nullability - * + * * @param type type to compute nullability for * @return type nullability */ public static @NotNull TypeNullability getTypeNullability(@NotNull PsiClassType type) { return getTypeNullability(type, null, !isLocal(type)); } - + private static @NotNull TypeNullability getTypeNullability(@NotNull PsiClassType type, @Nullable Set visited, boolean checkContainer) { @@ -77,7 +74,7 @@ public final class JavaTypeNullabilityUtil { } return TypeNullability.UNKNOWN; } - + private static boolean isLocal(PsiClassType classType) { PsiElement context = classType.getPsiContext(); return context instanceof PsiJavaCodeReferenceElement && @@ -115,27 +112,27 @@ public final class JavaTypeNullabilityUtil { * @param rightType assigned value * @param checkNotNullToNull whether to check for nullability conflict when assigning not-null to null value * - * @see JavaTypeNullabilityUtil#getNullabilityConflictType(PsiType, PsiType) + * @see JavaTypeNullabilityUtil#getNullabilityConflictTypeContext(PsiType, PsiType) */ - public static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType, + public static @NotNull NullabilityConflictContext getNullabilityConflictInAssignment(@Nullable PsiType leftType, @Nullable PsiType rightType, boolean checkNotNullToNull) { return getNullabilityConflictInAssignment(leftType, rightType, checkNotNullToNull, false); } - private static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType, + private static @NotNull NullabilityConflictContext getNullabilityConflictInAssignment(@Nullable PsiType leftType, @Nullable PsiType rightType, boolean checkNotNullToNull, boolean checkConflictInInitialType) { if (checkConflictInInitialType) { - NullabilityConflict nullabilityConflict = getNullabilityConflictType(leftType, rightType); - if (isAllowedNullabilityConflictType(checkNotNullToNull, nullabilityConflict)) return nullabilityConflict; + NullabilityConflictContext context = getNullabilityConflictTypeContext(leftType, rightType); + if (isAllowedNullabilityConflictType(checkNotNullToNull, context)) return context; } if (leftType == null || TypeConversionUtil.isNullType(leftType) || rightType == null || TypeConversionUtil.isNullType(rightType) ) { - return NullabilityConflict.UNKNOWN; + return NullabilityConflictContext.UNKNOWN; } if (rightType instanceof PsiIntersectionType) { @@ -163,7 +160,7 @@ public final class JavaTypeNullabilityUtil { } if (!(leftType instanceof PsiClassType) || !(rightType instanceof PsiClassType)) { - return NullabilityConflict.UNKNOWN; + return NullabilityConflictContext.UNKNOWN; } return getNullabilityConflictInTypeArguments(leftType, rightType, checkNotNullToNull); @@ -178,35 +175,34 @@ public final class JavaTypeNullabilityUtil { * @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, + private static @NotNull NullabilityConflictContext getNullabilityConflictInTypeArguments(@NotNull PsiType leftType, @NotNull PsiType rightType, boolean checkNotNullToNull) { - if (isRawType(leftType) || isRawType(rightType)) return NullabilityConflict.UNKNOWN; + if (isRawType(leftType) || isRawType(rightType)) return NullabilityConflictContext.UNKNOWN; PsiClass leftClass = PsiTypesUtil.getPsiClass(leftType); - if (leftClass == null) return NullabilityConflict.UNKNOWN; - + if (leftClass == null) return NullabilityConflictContext.UNKNOWN; List leftParameterTypeList = getParentParameterTypeListFromDerivedType(leftType, leftClass); List rightParameterTypeList = getParentParameterTypeListFromDerivedType(rightType, leftClass); if (leftParameterTypeList == null || rightParameterTypeList == null || leftParameterTypeList.size() != rightParameterTypeList.size()) { - return NullabilityConflict.UNKNOWN; + return NullabilityConflictContext.UNKNOWN; } for (int i = 0; i < leftParameterTypeList.size(); i++) { PsiType leftParameterType = leftParameterTypeList.get(i); PsiType rightParameterType = rightParameterTypeList.get(i); - NullabilityConflict nullabilityConflict = getNullabilityConflictInAssignment( + NullabilityConflictContext context = getNullabilityConflictInAssignment( leftParameterType, rightParameterType, checkNotNullToNull, true ); - if (nullabilityConflict != NullabilityConflict.UNKNOWN) return nullabilityConflict; + if (context.nullabilityConflict != NullabilityConflict.UNKNOWN) return context; } - return NullabilityConflict.UNKNOWN; + return NullabilityConflictContext.UNKNOWN; } private static boolean isRawType(@NotNull PsiType type) { @@ -225,9 +221,9 @@ public final class JavaTypeNullabilityUtil { return PsiUtil.substituteTypeParameters(derivedType, superClass, true); } - private static boolean isAllowedNullabilityConflictType(boolean checkNotNullToNull, @NotNull NullabilityConflict nullabilityConflict) { - return nullabilityConflict != NullabilityConflict.UNKNOWN && - (checkNotNullToNull || nullabilityConflict != NullabilityConflict.NOT_NULL_TO_NULL); + private static boolean isAllowedNullabilityConflictType(boolean checkNotNullToNull, @NotNull NullabilityConflictContext context) { + return context.nullabilityConflict != NullabilityConflict.UNKNOWN && + (checkNotNullToNull || context.nullabilityConflict != NullabilityConflict.NOT_NULL_TO_NULL); } /** @@ -236,18 +232,73 @@ public final class JavaTypeNullabilityUtil { * @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; + public static @NotNull NullabilityConflictContext getNullabilityConflictTypeContext(@Nullable PsiType leftType, @Nullable PsiType rightType) { + if (leftType == null || rightType == null) return NullabilityConflictContext.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; + if (leftNullability == Nullability.NOT_NULL && rightNullability == Nullability.NULLABLE) { + return new NullabilityConflictContext(NullabilityConflict.NULL_TO_NOT_NULL, rightType); + } // 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; + else if (leftNullability == Nullability.NULLABLE && rightNullability == Nullability.NOT_NULL && !GenericsUtil.isWildcardWithExtendsBound(leftType)) { + return new NullabilityConflictContext(NullabilityConflict.NOT_NULL_TO_NULL, rightType); + } + return NullabilityConflictContext.UNKNOWN; + } + + + /** + * Holds information about the nullability conflict that might be used to provide more descriptive error messages. + */ + public static class NullabilityConflictContext { + public final @NotNull NullabilityConflict nullabilityConflict; + public final @Nullable PsiType type; + + public static final NullabilityConflictContext UNKNOWN = new NullabilityConflictContext(NullabilityConflict.UNKNOWN, null); + + public NullabilityConflictContext(@NotNull NullabilityConflict nullabilityConflict, @Nullable PsiType type) { + this.type = type; + this.nullabilityConflict = nullabilityConflict; + } + + /** + * @return type argument or array type in which the conflict is occurred. + */ + public @Nullable PsiElement getPlace() { + return getPlace(type); + } + + + /** + * @return nullability annotation that produces the conflict. + */ + public @Nullable PsiAnnotation getAnnotation() { + if (type == null) return null; + TypeNullability nullability = type.getNullability(); + NullabilityAnnotationInfo info = nullability.toNullabilityAnnotationInfo(); + if (info == null) return null; + return info.getAnnotation(); + } + + private static @Nullable PsiElement getPlace(PsiType placeHolder) { + if (placeHolder instanceof PsiClassType) { + return ((PsiClassType)placeHolder).getPsiContext(); + } + else if (placeHolder instanceof PsiCapturedWildcardType) { + return getPlace(((PsiCapturedWildcardType) placeHolder).getWildcard()); + } + else if (placeHolder instanceof PsiWildcardType) { + return getPlace(((PsiWildcardType)placeHolder).getBound()); + } + else if (placeHolder instanceof PsiArrayType) { + return getPlace(placeHolder.getDeepComponentType()); + } + return null; + } } /** @@ -267,4 +318,4 @@ public final class JavaTypeNullabilityUtil { */ UNKNOWN } -} +} \ 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 index eb5500ba3eb8..d94919ecab6e 100644 --- a/java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java +++ b/java/java-tests/testData/inspection/nullableProblems/ReturnIncompatibilitiesWithGeneric.java @@ -1,20 +1,21 @@ import org.jspecify.annotations.NotNull; import org.jspecify.annotations.Nullable; +import org.jspecify.annotations.NullMarked; import java.util.List; import java.util.Map; import java.util.Collection; class B { B<@NotNull String> simpleNullableToNotNull(B<@Nullable String> arg) { - return arg; + return arg; } B<@Nullable String> simpleNotNullToNullable(B<@NotNull String> arg) { - return arg; + return arg; } B> nested(B> arg) { - return arg; + return arg; } B extendsWildcardNullable(B<@NotNull Object> arg) { @@ -22,7 +23,7 @@ class B { } B extendsWildcardNotNull(B<@Nullable String> arg) { - return arg; + return arg; } B SupperWildcard(B<@Nullable Object> arg) { @@ -30,7 +31,7 @@ class B { } B extendsWildcardBothNotNull(B arg) { - return arg; + return arg; } B extendsWildcardBothNullable(B arg) { @@ -42,35 +43,35 @@ class B { } B> nestedWithWildcard(B> arg) { - return arg; + return arg; } Map, List<@Nullable String>> checkIsPerformedIfSecondTypeArgumentIsTheSame(Map, List<@NotNull String>> arg) { - return arg; + return arg; } B<@NotNull String>[] array(B<@Nullable String>[] arg) { - return arg; + return arg; } Object[] @NotNull [] nullabilityInNestedArray(Object[] @Nullable [] arg) { - return arg; + return arg; } B<@NotNull String>[][] multiDimensionalArray(B<@Nullable String>[][] arg) { - return arg; + return arg; } static class C { C secondArgument(C arg) { - return arg; + return arg; } } interface I {} List<@Nullable String> intersectionSimple(Object arg) { - return (List<@NotNull String> & I) arg; + return (List<@NotNull String> & I) arg; } Collection<@Nullable String> intersectionTreatsFirstMatchingUnknown(Object arg) { @@ -78,11 +79,11 @@ class B { } Collection<@Nullable String> intersectionTreatsFirstMatchingMismatch(Object arg) { - return (Collection<@NotNull String> & List<@NotNull String> & I) arg; + return (Collection<@NotNull String> & List<@NotNull String> & I) arg; } List> nestedIntersection(Object arg) { - return (List> & I) arg; + return (List> & I) arg; } static class NoStackOverflow { @@ -96,4 +97,11 @@ class B { return p; } } + + @NullMarked + static class ReturnWithNullMarked { + static List<@Nullable String> f(List arg) { + return arg; + } + } } \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyConformanceAnnotationTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyConformanceAnnotationTest.java index 74871b0d787d..50d2cf38c3de 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyConformanceAnnotationTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyConformanceAnnotationTest.java @@ -7,6 +7,7 @@ import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.codeInspection.ex.InspectionManagerEx; import com.intellij.codeInspection.nullable.NullableStuffInspection; +import com.intellij.java.analysis.JavaAnalysisBundle; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; @@ -31,6 +32,7 @@ import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import one.util.streamex.EntryStream; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -221,11 +223,10 @@ public class JSpecifyConformanceAnnotationTest extends LightJavaCodeInsightFixtu } @Override - protected void reportProblem(@NotNull ProblemsHolder holder, - @NotNull PsiElement anchor, - LocalQuickFix @NotNull [] fixes, - @NotNull String messageKey, Object... args) { - switch (messageKey) { + protected void reportProblem(@NotNull ProblemsHolder holder, @NotNull PsiElement anchor, @NotNull LocalQuickFix @NotNull [] fixes, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String descriptionKey, @NotNull Object @NotNull[] descriptionArgs, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String tooltipKey, @NotNull Object @NotNull[] tooltipArgs) { + switch (descriptionKey) { case "inspection.nullable.problems.primitive.type.annotation", "inspection.nullable.problems.at.throws", "inspection.nullable.problems.at.type.parameter", 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 1da1b38e3e9c..411cb324e6cf 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/JSpecifyFilteredAnnotationTest.java @@ -14,6 +14,7 @@ import com.intellij.codeInspection.dataFlow.NullabilityProblemKind; import com.intellij.codeInspection.ex.InspectionManagerEx; import com.intellij.codeInspection.nullable.NotNullFieldNotInitializedInspection; import com.intellij.codeInspection.nullable.NullableStuffInspection; +import com.intellij.java.analysis.JavaAnalysisBundle; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.module.Module; @@ -41,6 +42,7 @@ import one.util.streamex.EntryStream; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.PropertyKey; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; @@ -561,11 +563,10 @@ public class JSpecifyFilteredAnnotationTest extends LightJavaCodeInsightFixtureT } @Override - protected void reportProblem(@NotNull ProblemsHolder holder, - @NotNull PsiElement anchor, - LocalQuickFix @NotNull [] fixes, - @NotNull String messageKey, Object... args) { - switch (messageKey) { + protected void reportProblem(@NotNull ProblemsHolder holder, @NotNull PsiElement anchor, @NotNull LocalQuickFix @NotNull [] fixes, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String descriptionKey, @NotNull Object @NotNull[] descriptionArgs, + @NotNull @PropertyKey(resourceBundle = JavaAnalysisBundle.BUNDLE) String tooltipKey, @NotNull Object @NotNull[] tooltipArgs) { + switch (descriptionKey) { case "inspection.nullable.problems.primitive.type.annotation", "inspection.nullable.problems.receiver.annotation", "inspection.nullable.problems.outer.type", "inspection.nullable.problems.at.reference.list", "inspection.nullable.problems.at.constructor", "inspection.nullable.problems.at.enum.constant" -> 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 7e372396f436..563efccecc5a 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java @@ -511,6 +511,7 @@ public class NullableStuffInspectionTest extends LightJavaCodeInsightFixtureTest public void testReturnIncompatibilitiesWithGeneric() { myInspection.REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = true; + addJSpecifyNullMarked(myFixture); setupTypeUseAnnotations("org.jspecify.annotations", myFixture); doTest(); } diff --git a/platform/analysis-api/api-dump.txt b/platform/analysis-api/api-dump.txt index 86c2448c60bb..1ee712609859 100644 --- a/platform/analysis-api/api-dump.txt +++ b/platform/analysis-api/api-dump.txt @@ -287,6 +287,8 @@ com.intellij.codeInspection.HintAction - com.intellij.codeInsight.intention.IntentionAction - fixSilently(com.intellij.openapi.editor.Editor):Z - a:showHint(com.intellij.openapi.editor.Editor):Z +a:com.intellij.codeInspection.InspectionManager +- a:createProblemDescriptor(com.intellij.psi.PsiElement,com.intellij.openapi.util.TextRange,java.lang.String,com.intellij.codeInspection.ProblemHighlightType,java.lang.String,Z,com.intellij.codeInspection.LocalQuickFix[]):com.intellij.codeInspection.ProblemDescriptor com.intellij.codeInspection.InspectionProfile - java.lang.Comparable - sf:DEFAULT_PROFILE_NAME:java.lang.String @@ -347,6 +349,8 @@ com.intellij.codeInspection.ProblemDescriptor - a:setProblemGroup(com.intellij.lang.annotation.ProblemGroup):V - a:setTextAttributes(com.intellij.openapi.editor.colors.TextAttributesKey):V - a:showTooltip():Z +f:com.intellij.codeInspection.ProblemsHolder$ProblemBuilder +- tooltip(java.lang.String):com.intellij.codeInspection.ProblemsHolder$ProblemBuilder com.intellij.codeInspection.QuickFix - com.intellij.openapi.application.WriteActionAware - sf:EMPTY_ARRAY:com.intellij.codeInspection.QuickFix[] diff --git a/platform/analysis-api/src/com/intellij/codeInspection/InspectionManager.java b/platform/analysis-api/src/com/intellij/codeInspection/InspectionManager.java index ef1abca44dc2..4474b6e211d1 100644 --- a/platform/analysis-api/src/com/intellij/codeInspection/InspectionManager.java +++ b/platform/analysis-api/src/com/intellij/codeInspection/InspectionManager.java @@ -81,6 +81,14 @@ public abstract class InspectionManager { boolean onTheFly, @NotNull LocalQuickFix @Nullable ... fixes); + public abstract @NotNull ProblemDescriptor createProblemDescriptor(@NotNull PsiElement psiElement, + @Nullable("null means the text range of the element") TextRange rangeInElement, + @NotNull @InspectionMessage String descriptionTemplate, + @NotNull ProblemHighlightType highlightType, + @NotNull String tooltipMessage, + boolean onTheFly, + @NotNull LocalQuickFix @Nullable ... fixes); + @Contract(pure = true) public abstract @NotNull ProblemDescriptor createProblemDescriptor(@NotNull PsiElement psiElement, @NotNull @InspectionMessage String descriptionTemplate, diff --git a/platform/analysis-api/src/com/intellij/codeInspection/ProblemsHolder.java b/platform/analysis-api/src/com/intellij/codeInspection/ProblemsHolder.java index 43055fa91c36..36a8a2a02abd 100644 --- a/platform/analysis-api/src/com/intellij/codeInspection/ProblemsHolder.java +++ b/platform/analysis-api/src/com/intellij/codeInspection/ProblemsHolder.java @@ -244,10 +244,12 @@ public class ProblemsHolder { private @NotNull ProblemHighlightType myHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING; private @Nullable TextRange myRange; private final @NotNull List myFixes = new ArrayList<>(); + private @NotNull String myTooltip; private ProblemBuilder(@NotNull PsiElement element, @InspectionMessage @NotNull String template) { myPsiElement = element; myDescriptionTemplate = template; + myTooltip = myDescriptionTemplate; } /** @@ -314,8 +316,24 @@ public class ProblemsHolder { return this; } + /** + * @param tooltip a message that is visible in the editor. It might contain HTML markup. + * @return this builder + */ + @Contract(value = "_ -> this", mutates = "this") + public ProblemBuilder tooltip(@NotNull String tooltip) { + myTooltip = tooltip; + return this; + } + public void register() { - registerProblem(myPsiElement, myDescriptionTemplate, myHighlightType, myRange, myFixes.toArray(LocalQuickFix.EMPTY_ARRAY)); + ProblemDescriptor descriptor = getManager() + .createProblemDescriptor(myPsiElement, + myRange, + myDescriptionTemplate, + myHighlightType, myTooltip, myOnTheFly, + myFixes.toArray(LocalQuickFix.EMPTY_ARRAY)); + registerProblem(descriptor); } } } \ No newline at end of file diff --git a/platform/analysis-impl/api-dump.txt b/platform/analysis-impl/api-dump.txt index 83404835a7e6..4985875ffbb9 100644 --- a/platform/analysis-impl/api-dump.txt +++ b/platform/analysis-impl/api-dump.txt @@ -246,6 +246,8 @@ com.intellij.codeInsight.template.impl.TemplateOptionalProcessor com.intellij.codeInsight.template.impl.TemplatePreprocessor - sf:EP_NAME:com.intellij.openapi.extensions.ExtensionPointName - a:preprocessTemplate(com.intellij.openapi.editor.Editor,com.intellij.psi.PsiFile,I,java.lang.String,java.lang.String):V +a:com.intellij.codeInspection.InspectionManagerBase +- createProblemDescriptor(com.intellij.psi.PsiElement,com.intellij.openapi.util.TextRange,java.lang.String,com.intellij.codeInspection.ProblemHighlightType,java.lang.String,Z,com.intellij.codeInspection.LocalQuickFix[]):com.intellij.codeInspection.ProblemDescriptor com.intellij.codeInspection.InspectionToolResultExporter - com.intellij.codeInspection.ProblemDescriptionsProcessor - a:addProblemElement(com.intellij.codeInspection.reference.RefEntity,Z,com.intellij.codeInspection.CommonProblemDescriptor[]):V diff --git a/platform/analysis-impl/src/com/intellij/codeInspection/InspectionManagerBase.java b/platform/analysis-impl/src/com/intellij/codeInspection/InspectionManagerBase.java index a75ca4cfa8ae..8381d1324717 100644 --- a/platform/analysis-impl/src/com/intellij/codeInspection/InspectionManagerBase.java +++ b/platform/analysis-impl/src/com/intellij/codeInspection/InspectionManagerBase.java @@ -98,6 +98,18 @@ public abstract class InspectionManagerBase extends InspectionManager { return new ProblemDescriptorBase(psiElement, psiElement, descriptionTemplate, fixes, highlightType, false, rangeInElement, tooltip, onTheFly); } + @Override + public @NotNull ProblemDescriptor createProblemDescriptor(@NotNull PsiElement psiElement, + @Nullable("null means the text range of the element") TextRange rangeInElement, + @NotNull String descriptionTemplate, + @NotNull ProblemHighlightType highlightType, + @Nullable String tooltipMessage, + boolean onTheFly, + @NotNull LocalQuickFix @Nullable ... fixes) { + boolean showTooltip = highlightType != ProblemHighlightType.INFORMATION; + return new ProblemDescriptorBase(psiElement, psiElement, descriptionTemplate, fixes, highlightType, false, rangeInElement, showTooltip, onTheFly, tooltipMessage); + } + @Override public @NotNull ProblemDescriptor createProblemDescriptor(@NotNull PsiElement psiElement, @NotNull @InspectionMessage String descriptionTemplate,