diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/JavaClassSupersImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/JavaClassSupersImpl.java index cbe80cc602fc..083c87f64376 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/JavaClassSupersImpl.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/JavaClassSupersImpl.java @@ -129,7 +129,26 @@ public final class JavaClassSupersImpl extends JavaClassSupers { if (paramCandidate instanceof PsiTypeParameter && paramCandidate != parameter) { targetType = outer.substituteWithBoundsPromotion((PsiTypeParameter)paramCandidate); if (targetType != null && !innerType.getNullability().equals(TypeNullability.UNKNOWN)) { - targetType = targetType.withNullability(innerType.getNullability()); + TypeNullability instantiated; + if (PsiUtil.resolveClassInClassTypeOnly(targetType) == paramCandidate) { + // interface Super { void take(@NullnessUnspecified T t); } + // interface Sub extends Super {} + // + // For the `Super` substitutor of `Sub`, the outer substitutor leaves `U` as it is, and then there is + // nothing to instantiate the usage with: the bare `U` it hands back is not written anywhere in code, so it + // has no nullability of its own to report, and instantiating with it would erase the nullable bound of `U`. + instantiated = innerType.getNullability(); + } + else { + //initialized with the known type + // Bar getNonNullBar() { <- with String + // return new Bar<>(); + // } + + //simplified without taking into account the nullability of the captured type + instantiated = innerType.getNullability().instantiatedWith(targetType.getNullability()); + } + targetType = targetType.withNullability(instantiated); } } else { diff --git a/java/java-tests/testData/inspection/nullableProblems/ReturnDerivedWithNullableBound.java b/java/java-tests/testData/inspection/nullableProblems/ReturnDerivedWithNullableBound.java new file mode 100644 index 000000000000..d0eea9c26bcc --- /dev/null +++ b/java/java-tests/testData/inspection/nullableProblems/ReturnDerivedWithNullableBound.java @@ -0,0 +1,19 @@ +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +class ReturnDerivedWithNullableBound { + Bar getNonNullBar() { + return new Bar<>(); + } + + Foo getNonNullFoo() { + return getNonNullBar(); + } + + static class Foo { + } + + static class Bar extends Foo { + } +} 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 1f01da6df2b0..f90940cabb3f 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/NullableStuffInspectionTest.java @@ -455,6 +455,13 @@ public class NullableStuffInspectionTest extends LightJavaCodeInsightFixtureTest setupTypeUseAnnotations("org.jspecify.annotations", myFixture); doTest(); } + + public void testReturnDerivedWithNullableBound() { + myInspection.REPORT_NOT_NULL_TO_NULLABLE_CONFLICTS_IN_ASSIGNMENTS = true; + addJSpecifyNullMarked(myFixture); + setupTypeUseAnnotations("org.jspecify.annotations", myFixture); + doTest(); + } public void testNullableParameterOverride() { doTest();