[java-psi] Disable nullity processing during upper bound capturing

We rely on PsiCapturedWildcardType identity when creating recursive types (the upper bound of a PsiCapturedWildcardType may refer to the same type). However, when we are under NullMarked annotation, the PsiCapturedWildcardType instance could be recreated, which prevents us from creating a proper PsiType object.
Fixes IDEA-378142 Regress issue with resolve type of returned generics value


(cherry picked from commit 4fb3c306e302715ab8de93f606397cd2a3dcb864)

IJ-CR-174927

GitOrigin-RevId: 37ff78949e58209010a5a493be537b6265052e34
This commit is contained in:
Tagir Valeev
2025-09-08 18:55:20 +02:00
committed by intellij-monorepo-bot
parent a34483fe06
commit 50bbd0efa5
4 changed files with 62 additions and 10 deletions

View File

@@ -53,7 +53,7 @@ public final class PsiCapturedWildcardType extends PsiType.Stub {
PsiType originalBound = !wildcardType.isSuper() ? wildcardType.getBound() : null;
PsiType glb = originalBound;
for (PsiType boundType : boundTypes) {
final PsiType substitutedBoundType = captureSubstitutor.substitute(boundType);
final PsiType substitutedBoundType = captureSubstitutor.substituteIgnoringNullability(boundType);
//glb for array types is not specified yet
if (originalBound instanceof PsiArrayType &&
substitutedBoundType instanceof PsiArrayType &&

View File

@@ -51,6 +51,19 @@ public interface PsiSubstitutor {
@Contract(pure = true, value = "null -> null")
PsiType substitute(@Nullable PsiType type);
/**
* Substitutes type parameters occurring in {@code type} with their values.
* If value for type parameter is {@code null}, appropriate erasure is returned.
* The nullability of the original type
*
* @param type the type to substitute the type parameters for.siT
* @return the result of the substitution.
*/
@Contract(pure = true, value = "null -> null")
default PsiType substituteIgnoringNullability(@Nullable PsiType type) {
return substitute(type);
}
/**
* @return true if this substitutor has at least one raw substitution
*/

View File

@@ -46,7 +46,6 @@ public final class PsiSubstitutorImpl implements PsiSubstitutor {
private static final UnmodifiableHashMap<PsiTypeParameter, PsiType> EMPTY_MAP = UnmodifiableHashMap.empty(PSI_EQUIVALENCE);
private final @NotNull UnmodifiableHashMap<PsiTypeParameter, PsiType> mySubstitutionMap;
private final SubstitutionVisitor mySimpleSubstitutionVisitor = new SubstitutionVisitor();
PsiSubstitutorImpl(@NotNull Map<? extends PsiTypeParameter, ? extends PsiType> map) {
mySubstitutionMap = UnmodifiableHashMap.fromMap(PSI_EQUIVALENCE, map);
@@ -78,7 +77,7 @@ public final class PsiSubstitutorImpl implements PsiSubstitutor {
}
/**
* @return type mapped to type parameter; null if type parameter is mapped to null; or PsiType.VOID if no mapping exists
* @return type mapped to type parameter; null if the type parameter is mapped to null; or PsiType.VOID if no mapping exists
*/
private PsiType getFromMap(@NotNull PsiTypeParameter typeParameter) {
if (typeParameter instanceof LightTypeParameter && ((LightTypeParameter)typeParameter).useDelegateToSubstitute()) {
@@ -88,12 +87,19 @@ public final class PsiSubstitutorImpl implements PsiSubstitutor {
}
@Override
public PsiType substitute(PsiType type) {
if (type == null) {
return null;
}
public PsiType substitute(@Nullable PsiType type) {
return doSubstitute(type, false);
}
@Override
public PsiType substituteIgnoringNullability(@Nullable PsiType type) {
return doSubstitute(type, true);
}
private @Nullable PsiType doSubstitute(@Nullable PsiType type, boolean ignoreNullity) {
if (type == null) return null;
PsiUtil.ensureValidType(type);
PsiType substituted = type.accept(mySimpleSubstitutionVisitor);
PsiType substituted = type.accept(new SubstitutionVisitor(ignoreNullity));
return correctExternalSubstitution(substituted, type);
}
@@ -151,6 +157,11 @@ public final class PsiSubstitutorImpl implements PsiSubstitutor {
}
private class SubstitutionVisitor extends PsiTypeMapper {
private final boolean ignoreNullity;
private SubstitutionVisitor(boolean ignoreNullity) {
this.ignoreNullity = ignoreNullity;
}
@Override
public PsiType visitType(@NotNull PsiType type) {
@@ -210,8 +221,10 @@ public final class PsiSubstitutorImpl implements PsiSubstitutor {
// TODO: remove once nullability works better than annotations
result = result.annotate(getMergedProvider(classType, result));
}
TypeNullability origNullability = classType.getNullability();
result = origNullability.equals(TypeNullability.UNKNOWN) ? result : result.withNullability(origNullability.instantiatedWith(result.getNullability()));
if (!ignoreNullity) {
TypeNullability origNullability = classType.getNullability();
result = origNullability.equals(TypeNullability.UNKNOWN) ? result : result.withNullability(origNullability.instantiatedWith(result.getNullability()));
}
}
return result;
}

View File

@@ -377,4 +377,30 @@ public final class PsiTypeNullabilityTest extends LightJavaCodeInsightFixtureTes
TypeNullability typeNullability = info.toTypeNullability();
assertEquals("NOT_NULL (@NotNullByDefault on package foo)", typeNullability.toString());
}
public void testFBoundResolveUnderNotNull() {
myFixture.configureByText("Test.java", """
import org.jetbrains.annotations.NotNullByDefault;
@NotNullByDefault
interface RestClient2 {
default void test(RequestHeadersUriSpec<?> spec2) {
spec2.uri().attributes().retrieve();
}
interface UriSpec<S extends RequestHeadersSpec<?>> {
S uri();
}
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
S attributes();
Object retrieve();
}
interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S>, RequestHeadersSpec<S> {
}
}
""");
myFixture.checkHighlighting();
}
}