Skip to content

Commit fc3a158

Browse files
ahejlsbergmprobst
authored andcommitted
Improve recursion depth checks (microsoft#46599)
* Decrease recursion depth limit to 3 + smarter check for recursion * Accept new baselines * Always set last type id * Keep indexed access recursion depth check * Less expensive and corrected check for broadest equivalent keys
1 parent 06617b2 commit fc3a158

File tree

2 files changed

+67
-54
lines changed

2 files changed

+67
-54
lines changed

src/compiler/checker.ts

+61-48
Original file line numberDiff line numberDiff line change
@@ -17804,7 +17804,7 @@ namespace ts {
1780417804
if (source.flags & TypeFlags.Singleton) return true;
1780517805
}
1780617806
if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) {
17807-
const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation));
17807+
const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false));
1780817808
if (related !== undefined) {
1780917809
return !!(related & RelationComparisonResult.Succeeded);
1781017810
}
@@ -18704,7 +18704,8 @@ namespace ts {
1870418704
if (overflow) {
1870518705
return Ternary.False;
1870618706
}
18707-
const id = getRelationKey(source, target, intersectionState | (inPropertyCheck ? IntersectionState.InPropertyCheck : 0), relation);
18707+
const keyIntersectionState = intersectionState | (inPropertyCheck ? IntersectionState.InPropertyCheck : 0);
18708+
const id = getRelationKey(source, target, keyIntersectionState, relation, /*ingnoreConstraints*/ false);
1870818709
const entry = relation.get(id);
1870918710
if (entry !== undefined) {
1871018711
if (reportErrors && entry & RelationComparisonResult.Failed && !(entry & RelationComparisonResult.Reported)) {
@@ -18731,16 +18732,13 @@ namespace ts {
1873118732
targetStack = [];
1873218733
}
1873318734
else {
18734-
// generate a key where all type parameter id positions are replaced with unconstrained type parameter ids
18735-
// this isn't perfect - nested type references passed as type arguments will muck up the indexes and thus
18736-
// prevent finding matches- but it should hit up the common cases
18737-
const broadestEquivalentId = id.split(",").map(i => i.replace(/-\d+/g, (_match, offset: number) => {
18738-
const index = length(id.slice(0, offset).match(/[-=]/g) || undefined);
18739-
return `=${index}`;
18740-
})).join(",");
18735+
// A key that starts with "*" is an indication that we have type references that reference constrained
18736+
// type parameters. For such keys we also check against the key we would have gotten if all type parameters
18737+
// were unconstrained.
18738+
const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, keyIntersectionState, relation, /*ignoreConstraints*/ true) : undefined;
1874118739
for (let i = 0; i < maybeCount; i++) {
1874218740
// If source and target are already being compared, consider them related with assumptions
18743-
if (id === maybeKeys[i] || broadestEquivalentId === maybeKeys[i]) {
18741+
if (id === maybeKeys[i] || broadestEquivalentId && broadestEquivalentId === maybeKeys[i]) {
1874418742
return Ternary.Maybe;
1874518743
}
1874618744
}
@@ -20295,47 +20293,55 @@ namespace ts {
2029520293
return isNonDeferredTypeReference(type) && some(getTypeArguments(type), t => !!(t.flags & TypeFlags.TypeParameter) || isTypeReferenceWithGenericArguments(t));
2029620294
}
2029720295

20298-
/**
20299-
* getTypeReferenceId(A<T, number, U>) returns "111=0-12=1"
20300-
* where A.id=111 and number.id=12
20301-
*/
20302-
function getTypeReferenceId(type: TypeReference, typeParameters: Type[], depth = 0) {
20303-
let result = "" + type.target.id;
20304-
for (const t of getTypeArguments(type)) {
20305-
if (isUnconstrainedTypeParameter(t)) {
20306-
let index = typeParameters.indexOf(t);
20307-
if (index < 0) {
20308-
index = typeParameters.length;
20309-
typeParameters.push(t);
20296+
function getGenericTypeReferenceRelationKey(source: TypeReference, target: TypeReference, postFix: string, ignoreConstraints: boolean) {
20297+
const typeParameters: Type[] = [];
20298+
let constraintMarker = "";
20299+
const sourceId = getTypeReferenceId(source, 0);
20300+
const targetId = getTypeReferenceId(target, 0);
20301+
return `${constraintMarker}${sourceId},${targetId}${postFix}`;
20302+
// getTypeReferenceId(A<T, number, U>) returns "111=0-12=1"
20303+
// where A.id=111 and number.id=12
20304+
function getTypeReferenceId(type: TypeReference, depth = 0) {
20305+
let result = "" + type.target.id;
20306+
for (const t of getTypeArguments(type)) {
20307+
if (t.flags & TypeFlags.TypeParameter) {
20308+
if (ignoreConstraints || isUnconstrainedTypeParameter(t)) {
20309+
let index = typeParameters.indexOf(t);
20310+
if (index < 0) {
20311+
index = typeParameters.length;
20312+
typeParameters.push(t);
20313+
}
20314+
result += "=" + index;
20315+
continue;
20316+
}
20317+
// We mark type references that reference constrained type parameters such that we know to obtain
20318+
// and look for a "broadest equivalent key" in the cache.
20319+
constraintMarker = "*";
20320+
}
20321+
else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) {
20322+
result += "<" + getTypeReferenceId(t as TypeReference, depth + 1) + ">";
20323+
continue;
2031020324
}
20311-
result += "=" + index;
20312-
}
20313-
else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) {
20314-
result += "<" + getTypeReferenceId(t as TypeReference, typeParameters, depth + 1) + ">";
20315-
}
20316-
else {
2031720325
result += "-" + t.id;
2031820326
}
20327+
return result;
2031920328
}
20320-
return result;
2032120329
}
2032220330

2032320331
/**
2032420332
* To improve caching, the relation key for two generic types uses the target's id plus ids of the type parameters.
2032520333
* For other cases, the types ids are used.
2032620334
*/
20327-
function getRelationKey(source: Type, target: Type, intersectionState: IntersectionState, relation: ESMap<string, RelationComparisonResult>) {
20335+
function getRelationKey(source: Type, target: Type, intersectionState: IntersectionState, relation: ESMap<string, RelationComparisonResult>, ignoreConstraints: boolean) {
2032820336
if (relation === identityRelation && source.id > target.id) {
2032920337
const temp = source;
2033020338
source = target;
2033120339
target = temp;
2033220340
}
2033320341
const postFix = intersectionState ? ":" + intersectionState : "";
20334-
if (isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target)) {
20335-
const typeParameters: Type[] = [];
20336-
return getTypeReferenceId(source as TypeReference, typeParameters) + "," + getTypeReferenceId(target as TypeReference, typeParameters) + postFix;
20337-
}
20338-
return source.id + "," + target.id + postFix;
20342+
return isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target) ?
20343+
getGenericTypeReferenceRelationKey(source as TypeReference, target as TypeReference, postFix, ignoreConstraints) :
20344+
`${source.id},${target.id}${postFix}`;
2033920345
}
2034020346

2034120347
// Invoke the callback for each underlying property symbol of the given symbol and return the first
@@ -20389,27 +20395,34 @@ namespace ts {
2038920395
}
2039020396

2039120397
// Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons
20392-
// for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible,
20398+
// for maxDepth or more occurrences or instantiations of the type have been recorded on the given stack. It is possible,
2039320399
// though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely
20394-
// expanding. Effectively, we will generate a false positive when two types are structurally equal to at least 5
20400+
// expanding. Effectively, we will generate a false positive when two types are structurally equal to at least maxDepth
2039520401
// levels, but unequal at some level beyond that.
20396-
// In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially
20397-
// the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives
20398-
// for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
20399-
// It also detects when a recursive type reference has expanded 5 or more times, eg, if the true branch of
20402+
// In addition, this will also detect when an indexed access has been chained off of maxDepth more times (which is
20403+
// essentially the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding
20404+
// false positives for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
20405+
// It also detects when a recursive type reference has expanded maxDepth or more times, e.g. if the true branch of
2040020406
// `type A<T> = null extends T ? [A<NonNullable<T>>] : [T]`
20401-
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`
20402-
// in such cases we need to terminate the expansion, and we do so here.
20403-
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 5): boolean {
20407+
// has expanded into `[A<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>>>>>>]`. In such cases we need
20408+
// to terminate the expansion, and we do so here.
20409+
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 3): boolean {
2040420410
if (depth >= maxDepth) {
2040520411
const identity = getRecursionIdentity(type);
2040620412
let count = 0;
20413+
let lastTypeId = 0;
2040720414
for (let i = 0; i < depth; i++) {
20408-
if (getRecursionIdentity(stack[i]) === identity) {
20409-
count++;
20410-
if (count >= maxDepth) {
20411-
return true;
20415+
const t = stack[i];
20416+
if (getRecursionIdentity(t) === identity) {
20417+
// We only count occurrences with a higher type id than the previous occurrence, since higher
20418+
// type ids are an indicator of newer instantiations caused by recursion.
20419+
if (t.id >= lastTypeId) {
20420+
count++;
20421+
if (count >= maxDepth) {
20422+
return true;
20423+
}
2041220424
}
20425+
lastTypeId = t.id;
2041320426
}
2041420427
}
2041520428
}

tests/baselines/reference/invariantGenericErrorElaboration.errors.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
tests/cases/compiler/invariantGenericErrorElaboration.ts(3,7): error TS2322: Type 'Num' is not assignable to type 'Runtype<any>'.
2-
The types of 'constraint.constraint.constraint' are incompatible between these types.
3-
Type 'Constraint<Constraint<Constraint<Num>>>' is not assignable to type 'Constraint<Constraint<Constraint<Runtype<any>>>>'.
4-
Type 'Constraint<Runtype<any>>' is not assignable to type 'Constraint<Num>'.
2+
The types of 'constraint.constraint' are incompatible between these types.
3+
Type 'Constraint<Constraint<Num>>' is not assignable to type 'Constraint<Constraint<Runtype<any>>>'.
4+
Type 'Runtype<any>' is not assignable to type 'Num'.
55
tests/cases/compiler/invariantGenericErrorElaboration.ts(4,19): error TS2322: Type 'Num' is not assignable to type 'Runtype<any>'.
66

77

@@ -11,9 +11,9 @@ tests/cases/compiler/invariantGenericErrorElaboration.ts(4,19): error TS2322: Ty
1111
const wat: Runtype<any> = Num;
1212
~~~
1313
!!! error TS2322: Type 'Num' is not assignable to type 'Runtype<any>'.
14-
!!! error TS2322: The types of 'constraint.constraint.constraint' are incompatible between these types.
15-
!!! error TS2322: Type 'Constraint<Constraint<Constraint<Num>>>' is not assignable to type 'Constraint<Constraint<Constraint<Runtype<any>>>>'.
16-
!!! error TS2322: Type 'Constraint<Runtype<any>>' is not assignable to type 'Constraint<Num>'.
14+
!!! error TS2322: The types of 'constraint.constraint' are incompatible between these types.
15+
!!! error TS2322: Type 'Constraint<Constraint<Num>>' is not assignable to type 'Constraint<Constraint<Runtype<any>>>'.
16+
!!! error TS2322: Type 'Runtype<any>' is not assignable to type 'Num'.
1717
const Foo = Obj({ foo: Num })
1818
~~~
1919
!!! error TS2322: Type 'Num' is not assignable to type 'Runtype<any>'.

0 commit comments

Comments
 (0)