diff --git a/java/java-impl/src/com/intellij/codeInsight/generation/OverrideImplementsAnnotationsHandler.java b/java/java-impl/src/com/intellij/codeInsight/generation/OverrideImplementsAnnotationsHandler.java index ebbbc7157ace..6e71d3bff133 100644 --- a/java/java-impl/src/com/intellij/codeInsight/generation/OverrideImplementsAnnotationsHandler.java +++ b/java/java-impl/src/com/intellij/codeInsight/generation/OverrideImplementsAnnotationsHandler.java @@ -11,9 +11,14 @@ import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifierList; import com.intellij.psi.PsiModifierListOwner; import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.PsiTypeElement; +import com.intellij.psi.PsiVariable; import com.intellij.psi.codeStyle.JavaCodeStyleSettings; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ArrayUtil; @@ -59,7 +64,7 @@ public interface OverrideImplementsAnnotationsHandler extends OverrideImplements if (moduleScope != null && facade.findClass(annotation, moduleScope) == null) continue; int flags = CHECK_EXTERNAL | CHECK_TYPE; - if (AnnotationUtil.isAnnotated(source, annotation, flags) && !AnnotationUtil.isAnnotated(target, annotation, flags)) { + if (AnnotationUtil.isAnnotated(source, annotation, flags) && !hasAnnotation(target, annotation)) { each.transferToTarget(annotation, source, target); } } @@ -70,6 +75,78 @@ public interface OverrideImplementsAnnotationsHandler extends OverrideImplements } } + private static boolean hasAnnotation(@NotNull PsiModifierListOwner target, @NotNull String annotation) { + if (AnnotationUtil.isAnnotated(target, annotation, CHECK_EXTERNAL | CHECK_TYPE)) return true; + // Handle case when the annotation present in the source modifier list + // has been 'hidden' after the package qualifier when generating target. + // For example for: + // source = @NotNull Map.Entry param + // target = java.util.@org.jetbrains.annotations.NotNull Map.Entry + // AnnotationUtil.hasAnnotation(.., ..@NotNull) returns true for source but false for the target + PsiTypeElement typeElement = getTypeElement(target); + if (typeElement == null) return false; + return hasAnnotationOnFirstNonPackageSubElement(typeElement, annotation); + } + + private static @Nullable PsiTypeElement getTypeElement(@NotNull PsiModifierListOwner target) { + if (target instanceof PsiMethod) return ((PsiMethod)target).getReturnTypeElement(); + if (target instanceof PsiVariable) return ((PsiVariable)target).getTypeElement(); + return null; + } + + /** + * Checks if a specified annotation is present at the innermost component reference element of the given type element + * after skipping the initial package qualifier. + * + *

Examples: + *

{@code
+   * Returns true for annotation "org.jetbrains.annotations.NotNull"
+   * when typeElement represents: java.lang.@NotNull String
+   *
+   * Returns true for annotation "org.jetbrains.annotations.NotNull"
+   * when typeElement represents: @NotNull String
+   *
+   * Returns false for annotation "org.jetbrains.annotations.NotNull"
+   * when typeElement represents: @NotNull java.lang.String
+   *
+   * Returns true for annotation "org.jetbrains.annotations.Nullable"
+   * when typeElement represents: java.util.@Nullable List
+   * }
+ * + * @param typeElement The {@link PsiTypeElement} to inspect. This represents a type in Java source code, + * such as a method return type or a parameter type. + * @param annotation The fully qualified name of the annotation to search for. + * @return true if the annotation is found after the package qualifier in the type reference; + * false otherwise, or if the type element has no innermost component reference. + */ + private static boolean hasAnnotationOnFirstNonPackageSubElement(PsiTypeElement typeElement, @NotNull String annotation) { + PsiJavaCodeReferenceElement reference = typeElement.getInnermostComponentReferenceElement(); + if (reference == null) return false; + var leftMostResolvedQualifier = getLeftmostNonPackageElement(reference); + if (leftMostResolvedQualifier == null) return false; + return hasDirectAnnotation(leftMostResolvedQualifier, annotation); + } + + private static @Nullable PsiElement getLeftmostNonPackageElement(@NotNull PsiJavaCodeReferenceElement reference) { + PsiJavaCodeReferenceElement qualifier = + reference.getQualifier() instanceof PsiJavaCodeReferenceElement referenceElement ? referenceElement : null; + if (qualifier != null) { + PsiElement result = getLeftmostNonPackageElement(qualifier); + if (result != null) return result; + } + PsiElement resolved = reference.resolve(); + return resolved == null || resolved instanceof PsiPackage ? null : reference; + } + + private static boolean hasDirectAnnotation(@NotNull PsiElement element, @NotNull String annotation) { + for (PsiElement child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof PsiAnnotation psiAnnotation && psiAnnotation.hasQualifiedName(annotation)) { + return true; + } + } + return false; + } + default void transferToTarget(String annotation, PsiModifierListOwner source, PsiModifierListOwner target) { PsiModifierList modifierList = target.getModifierList(); assert modifierList != null : target; diff --git a/java/java-tests/testData/codeInsight/overrideImplement/nullability/afterNestedTypeParameter.java b/java/java-tests/testData/codeInsight/overrideImplement/nullability/afterNestedTypeParameter.java new file mode 100644 index 000000000000..c5df1d2f0825 --- /dev/null +++ b/java/java-tests/testData/codeInsight/overrideImplement/nullability/afterNestedTypeParameter.java @@ -0,0 +1,21 @@ +package mypackage; + +import org.jetbrains.annotations.NotNull; + +class Outer { + static class Middle { + static class Nested { + } + } +} + +interface Parent { + void test(@NotNull Outer.Middle.Nested nested); +} + +class Child implements Parent { + @Override + public void test(@NotNull Outer.Middle.Nested nested) { + + } +} diff --git a/java/java-tests/testData/codeInsight/overrideImplement/nullability/beforeNestedTypeParameter.java b/java/java-tests/testData/codeInsight/overrideImplement/nullability/beforeNestedTypeParameter.java new file mode 100644 index 000000000000..c8f685d4bf51 --- /dev/null +++ b/java/java-tests/testData/codeInsight/overrideImplement/nullability/beforeNestedTypeParameter.java @@ -0,0 +1,18 @@ +package mypackage; + +import org.jetbrains.annotations.NotNull; + +class Outer { + static class Middle { + static class Nested { + } + } +} + +interface Parent { + void test(@NotNull Outer.Middle.Nested nested); +} + +class Child implements Parent { + +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/OverrideImplementNullabilityTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/OverrideImplementNullabilityTest.java index 9f4d47fa5999..aa90ef1ff95f 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/OverrideImplementNullabilityTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/OverrideImplementNullabilityTest.java @@ -43,5 +43,9 @@ public class OverrideImplementNullabilityTest extends OverrideImplementBaseTest doTest(); } + public void testNestedTypeParameter() { + doTest(); + } + protected void doTest() { doTest(false, true); } } \ No newline at end of file