Improve checks for infinitely expanding recursive conditional types (#46326)

* Improve checks for infinitely expanding recursive conditional types

* Accept new baselines

* Add regression tests

* Remove 'export' modifier

* Accept new baselines
This commit is contained in:
Anders Hejlsberg
2021-10-13 07:08:01 -07:00
committed by GitHub
parent 315b807489
commit 0d2aeb7c65
8 changed files with 307 additions and 47 deletions

View File

@@ -19024,6 +19024,12 @@ namespace ts {
}
}
else if (target.flags & TypeFlags.Conditional) {
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
// conditional type and bail out with a Ternary.Maybe result.
if (isDeeplyNestedType(target, targetStack, targetDepth, 10)) {
resetErrorInfo(saveErrorInfo);
return Ternary.Maybe;
}
const c = target as ConditionalType;
// Check if the conditional is always true or always false but still deferred for distribution purposes
const skipTrue = !isTypeAssignableTo(getPermissiveInstantiation(c.checkType), getPermissiveInstantiation(c.extendsType));
@@ -19123,33 +19129,34 @@ namespace ts {
}
}
else if (source.flags & TypeFlags.Conditional) {
// If we reach 10 levels of nesting for the same conditional type, assume it is an infinitely expanding recursive
// conditional type and bail out with a Ternary.Maybe result.
if (isDeeplyNestedType(source, sourceStack, sourceDepth, 10)) {
resetErrorInfo(saveErrorInfo);
return Ternary.Maybe;
}
if (target.flags & TypeFlags.Conditional) {
// If one of the conditionals under comparison seems to be infinitely expanding, stop comparing it - back out, try
// the constraint, and failing that, give up trying to relate the two. This is the only way we can handle recursive conditional
// types, which might expand forever.
if (!isDeeplyNestedType(source, sourceStack, sourceDepth) && !isDeeplyNestedType(target, targetStack, targetDepth)) {
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
let sourceExtends = (source as ConditionalType).extendsType;
let mapper: TypeMapper | undefined;
if (sourceParams) {
// If the source has infer type parameters, we instantiate them in the context of the target
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
mapper = ctx.mapper;
// Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.
const sourceParams = (source as ConditionalType).root.inferTypeParameters;
let sourceExtends = (source as ConditionalType).extendsType;
let mapper: TypeMapper | undefined;
if (sourceParams) {
// If the source has infer type parameters, we instantiate them in the context of the target
const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedToWorker);
inferTypes(ctx.inferences, (target as ConditionalType).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
sourceExtends = instantiateType(sourceExtends, ctx.mapper);
mapper = ctx.mapper;
}
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
}
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) &&
(isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both) || isRelatedTo((target as ConditionalType).checkType, (source as ConditionalType).checkType, RecursionFlags.Both))) {
if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source as ConditionalType), mapper), getTrueTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors)) {
result &= isRelatedTo(getFalseTypeFromConditionalType(source as ConditionalType), getFalseTypeFromConditionalType(target as ConditionalType), RecursionFlags.Both, reportErrors);
}
if (result) {
resetErrorInfo(saveErrorInfo);
return result;
}
if (result) {
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
@@ -19165,18 +19172,13 @@ namespace ts {
}
}
// We'll repeatedly decompose source side conditionals if they're recursive - check if we've already recured on the constraint a lot and, if so, bail
// on the comparison.
if (!isDeeplyNestedType(source, sourceStack, sourceDepth)) {
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
}
// conditionals _can_ be related to one another via normal constraint, as, eg, `A extends B ? O : never` should be assignable to `O`
// when `O` is a conditional (`never` is trivially assignable to `O`, as is `O`!).
const defaultConstraint = getDefaultConstraintOfConditionalType(source as ConditionalType);
if (defaultConstraint) {
if (result = isRelatedTo(defaultConstraint, target, RecursionFlags.Source, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
@@ -20322,14 +20324,14 @@ namespace ts {
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
// in such cases we need to terminate the expansion, and we do so here.
function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean {
if (depth >= 5) {
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 5): boolean {
if (depth >= maxDepth) {
const identity = getRecursionIdentity(type);
let count = 0;
for (let i = 0; i < depth; i++) {
if (getRecursionIdentity(stack[i]) === identity) {
count++;
if (count >= 5) {
if (count >= maxDepth) {
return true;
}
}