From 896e2014ab75cf9b2b4bb72b1a74c18f7260192e Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Wed, 5 Aug 2026 13:08:22 +0200 Subject: [PATCH] [java-inspection] IDEA-392353 JSpecify type parameter nullability is not inherited correctly (also fix IDEA-392423) (cherry picked from commit ed6427f21243d69e90b671a38cb34fe8511dce61) GitOrigin-RevId: 3e4b277c9be6bdb0954663e0c3a679907e13c5b5 --- .../psi/impl/JavaClassSupersImpl.java | 21 ++++++++++++++++++- .../ReturnDerivedWithNullableBound.java | 19 +++++++++++++++++ .../NullableStuffInspectionTest.java | 7 +++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 java/java-tests/testData/inspection/nullableProblems/ReturnDerivedWithNullableBound.java 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();