[Java] IDEA-375132 IDEA-381374 Limit recursion depth when checking nullability conflict.

Previous implementation for nullability conflict detection didn't perform correct substitution for bounded type parameters. New approach does that, but it may face with endless recursion when facing with recursive type bounds. This commit limits for the type parameters

GitOrigin-RevId: 226e2d6f536cdcc943461632a42fc76a1fcc8e01
This commit is contained in:
Georgii Ustinov
2025-10-30 10:21:52 +00:00
committed by intellij-monorepo-bot
parent 7003aeac2a
commit 9f6615f6ed
2 changed files with 33 additions and 8 deletions
@@ -21,6 +21,13 @@ import java.util.Set;
*/
@ApiStatus.Internal
public final class JavaTypeNullabilityUtil {
/**
* Java allows recursive type bounds, e.g. {@code class A<T extends A<T>> {}}.
* During the nullability conflict it may turn into the endless recursion.
* This field limits the depth of the recursive type parameters.
*/
private static final int MAX_NULLABILITY_CONFLICT_RECURSION_PARAMETER_DEPTH = 100;
/**
* Computes the class type nullability
*
@@ -107,13 +114,17 @@ public final class JavaTypeNullabilityUtil {
public static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType,
@Nullable PsiType rightType,
boolean checkNotNullToNull) {
return getNullabilityConflictInAssignment(leftType, rightType, checkNotNullToNull, false);
return getNullabilityConflictInAssignment(leftType, rightType, 0, checkNotNullToNull, false);
}
private static @NotNull NullabilityConflict getNullabilityConflictInAssignment(@Nullable PsiType leftType,
@Nullable PsiType rightType,
int recursionDepth,
boolean checkNotNullToNull,
boolean checkConflictInInitialType) {
if (recursionDepth >= MAX_NULLABILITY_CONFLICT_RECURSION_PARAMETER_DEPTH) {
return NullabilityConflict.UNKNOWN;
}
if (checkConflictInInitialType) {
NullabilityConflict nullabilityConflict = getNullabilityConflictType(leftType, rightType);
if (isAllowedNullabilityConflictType(checkNotNullToNull, nullabilityConflict)) return nullabilityConflict;
@@ -126,34 +137,34 @@ public final class JavaTypeNullabilityUtil {
}
if (rightType instanceof PsiIntersectionType) {
return getNullabilityConflictInTypeArguments(leftType, rightType, checkNotNullToNull);
return getNullabilityConflictInTypeArguments(leftType, rightType, recursionDepth, checkNotNullToNull);
}
if (rightType instanceof PsiCapturedWildcardType) {
return getNullabilityConflictInAssignment(leftType, ((PsiCapturedWildcardType)rightType).getUpperBound(true), checkNotNullToNull,
return getNullabilityConflictInAssignment(leftType, ((PsiCapturedWildcardType)rightType).getUpperBound(true), recursionDepth + 1, checkNotNullToNull,
false);
}
if (leftType instanceof PsiCapturedWildcardType) {
return getNullabilityConflictInAssignment(((PsiCapturedWildcardType)leftType).getLowerBound(), rightType, checkNotNullToNull, false);
return getNullabilityConflictInAssignment(((PsiCapturedWildcardType)leftType).getLowerBound(), rightType, recursionDepth + 1, checkNotNullToNull, false);
}
if (leftType instanceof PsiWildcardType) {
return getNullabilityConflictInAssignment(GenericsUtil.getWildcardBound(leftType), rightType, checkNotNullToNull, false);
return getNullabilityConflictInAssignment(GenericsUtil.getWildcardBound(leftType), rightType, recursionDepth + 1, checkNotNullToNull, false);
}
if (rightType instanceof PsiWildcardType) {
return getNullabilityConflictInAssignment(leftType, GenericsUtil.getWildcardBound(rightType), checkNotNullToNull, false);
return getNullabilityConflictInAssignment(leftType, GenericsUtil.getWildcardBound(rightType), recursionDepth + 1, checkNotNullToNull, false);
}
if (leftType instanceof PsiArrayType && rightType instanceof PsiArrayType) {
return getNullabilityConflictInAssignment(((PsiArrayType)leftType).getComponentType(),
((PsiArrayType)rightType).getComponentType(), checkNotNullToNull, true);
((PsiArrayType)rightType).getComponentType(), recursionDepth + 1, checkNotNullToNull, true);
}
if (!(leftType instanceof PsiClassType) || !(rightType instanceof PsiClassType)) {
return NullabilityConflict.UNKNOWN;
}
return getNullabilityConflictInTypeArguments(leftType, rightType, checkNotNullToNull);
return getNullabilityConflictInTypeArguments(leftType, rightType, recursionDepth, checkNotNullToNull);
}
/**
@@ -167,6 +178,7 @@ public final class JavaTypeNullabilityUtil {
*/
private static @NotNull NullabilityConflict getNullabilityConflictInTypeArguments(@NotNull PsiType leftType,
@NotNull PsiType rightType,
int recursionDepth,
boolean checkNotNullToNull) {
PsiClass leftClass = PsiTypesUtil.getPsiClass(leftType);
if (leftClass == null) return NullabilityConflict.UNKNOWN;
@@ -185,6 +197,7 @@ public final class JavaTypeNullabilityUtil {
NullabilityConflict nullabilityConflict = getNullabilityConflictInAssignment(
leftParameterType,
rightParameterType,
recursionDepth + 1,
checkNotNullToNull,
true
);
@@ -84,4 +84,16 @@ class B<T> {
List<List<@Nullable String>> nestedIntersection(Object arg) {
return <warning descr="Returning a class with non-null type parameters when a class with nullable type parameters is expected">(List<List<@NotNull String>> & I) arg</warning>;
}
static class NoStackOverflow {
public abstract static class Parent
<S extends Number, B extends Parent<S, B>>{}
public static class Child extends
Parent<Number, Child> {}
public Parent rawType(Child p) {
return p;
}
}
}