Skip to content

Commit c052bc7

Browse files
authored
Error on excessive relation complexity (#55851)
1 parent fbe426e commit c052bc7

File tree

6 files changed

+159
-1
lines changed

6 files changed

+159
-1
lines changed

src/compiler/checker.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -20778,6 +20778,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2077820778
let skipParentCounter = 0; // How many errors should be skipped 'above' in the elaboration pyramid
2077920779
let lastSkippedInfo: [Type, Type] | undefined;
2078020780
let incompatibleStack: DiagnosticAndArguments[] | undefined;
20781+
// In Node.js, the maximum number of elements in a map is 2^24. We limit the number of entries an invocation
20782+
// of checkTypeRelatedTo can add to a relation to 1/8th of its remaining capacity.
20783+
let relationCount = (16_000_000 - relation.size) >> 3;
2078120784

2078220785
Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");
2078320786

@@ -20786,8 +20789,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2078620789
reportIncompatibleStack();
2078720790
}
2078820791
if (overflow) {
20792+
// Record this relation as having failed such that we don't attempt the overflowing operation again.
20793+
const id = getRelationKey(source, target, /*intersectionState*/ IntersectionState.None, relation, /*ignoreConstraints*/ false);
20794+
relation.set(id, RelationComparisonResult.Reported | RelationComparisonResult.Failed);
2078920795
tracing?.instant(tracing.Phase.CheckTypes, "checkTypeRelatedTo_DepthLimit", { sourceId: source.id, targetId: target.id, depth: sourceDepth, targetDepth });
20790-
const diag = error(errorNode || currentNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target));
20796+
const message = relationCount <= 0 ?
20797+
Diagnostics.Excessive_complexity_comparing_types_0_and_1 :
20798+
Diagnostics.Excessive_stack_depth_comparing_types_0_and_1;
20799+
const diag = error(errorNode || currentNode, message, typeToString(source), typeToString(target));
2079120800
if (errorOutputContainer) {
2079220801
(errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag);
2079320802
}
@@ -21420,6 +21429,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2142021429
// we need to deconstruct unions before intersections (because unions are always at the top),
2142121430
// and we need to handle "each" relations before "some" relations for the same kind of type.
2142221431
if (source.flags & TypeFlags.Union) {
21432+
if (target.flags & TypeFlags.Union) {
21433+
// Intersections of union types are normalized into unions of intersection types, and such normalized
21434+
// unions can get very large and expensive to relate. The following fast path checks if the source union
21435+
// originated in an intersection. If so, and if that intersection contains the target type, then we know
21436+
// the result to be true (for any two types A and B, A & B is related to both A and B).
21437+
const sourceOrigin = (source as UnionType).origin;
21438+
if (sourceOrigin && sourceOrigin.flags & TypeFlags.Intersection && target.aliasSymbol && contains((sourceOrigin as IntersectionType).types, target)) {
21439+
return Ternary.True;
21440+
}
21441+
// Similarly, in unions of unions the we preserve the original list of unions. This original list is often
21442+
// much shorter than the normalized result, so we scan it in the following fast path.
21443+
const targetOrigin = (target as UnionType).origin;
21444+
if (targetOrigin && targetOrigin.flags & TypeFlags.Union && source.aliasSymbol && contains((targetOrigin as UnionType).types, source)) {
21445+
return Ternary.True;
21446+
}
21447+
}
2142321448
return relation === comparableRelation ?
2142421449
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) :
2142521450
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState);
@@ -21671,6 +21696,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2167121696
return entry & RelationComparisonResult.Succeeded ? Ternary.True : Ternary.False;
2167221697
}
2167321698
}
21699+
if (relationCount <= 0) {
21700+
overflow = true;
21701+
return Ternary.False;
21702+
}
2167421703
if (!maybeKeys) {
2167521704
maybeKeys = [];
2167621705
maybeKeysSet = new Set();
@@ -21768,6 +21797,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2176821797
// A false result goes straight into global cache (when something is false under
2176921798
// assumptions it will also be false without assumptions)
2177021799
relation.set(id, (reportErrors ? RelationComparisonResult.Reported : 0) | RelationComparisonResult.Failed | propagatingVarianceFlags);
21800+
relationCount--;
2177121801
resetMaybeStack(/*markAllAsSucceeded*/ false);
2177221802
}
2177321803
return result;
@@ -21777,6 +21807,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2177721807
maybeKeysSet.delete(maybeKeys[i]);
2177821808
if (markAllAsSucceeded) {
2177921809
relation.set(maybeKeys[i], RelationComparisonResult.Succeeded | propagatingVarianceFlags);
21810+
relationCount--;
2178021811
}
2178121812
}
2178221813
maybeCount = maybeStart;

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -3687,6 +3687,10 @@
36873687
"category": "Error",
36883688
"code": 2858
36893689
},
3690+
"Excessive complexity comparing types '{0}' and '{1}'.": {
3691+
"category": "Error",
3692+
"code": 2859
3693+
},
36903694

36913695
"Import declaration '{0}' is using private name '{1}'.": {
36923696
"category": "Error",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
relationComplexityError.ts(12,5): error TS2322: Type 'T1 & T2' is not assignable to type 'T1 | null'.
2+
relationComplexityError.ts(12,5): error TS2859: Excessive complexity comparing types 'T1 & T2' and 'T1 | null'.
3+
4+
5+
==== relationComplexityError.ts (2 errors) ====
6+
// Repro from #55630
7+
8+
type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
9+
type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
10+
type T2 = { a: string } | { b: number };
11+
12+
function f1(x: T1, y: T1 & T2) {
13+
x = y;
14+
}
15+
16+
function f2(x: T1 | null, y: T1 & T2) {
17+
x = y; // Complexity error
18+
~
19+
!!! error TS2322: Type 'T1 & T2' is not assignable to type 'T1 | null'.
20+
~~~~~
21+
!!! error TS2859: Excessive complexity comparing types 'T1 & T2' and 'T1 | null'.
22+
}
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//// [tests/cases/compiler/relationComplexityError.ts] ////
2+
3+
=== relationComplexityError.ts ===
4+
// Repro from #55630
5+
6+
type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
7+
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
8+
9+
type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
10+
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
11+
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
12+
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
13+
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
14+
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
15+
16+
type T2 = { a: string } | { b: number };
17+
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))
18+
>a : Symbol(a, Decl(relationComplexityError.ts, 4, 11))
19+
>b : Symbol(b, Decl(relationComplexityError.ts, 4, 27))
20+
21+
function f1(x: T1, y: T1 & T2) {
22+
>f1 : Symbol(f1, Decl(relationComplexityError.ts, 4, 40))
23+
>x : Symbol(x, Decl(relationComplexityError.ts, 6, 12))
24+
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
25+
>y : Symbol(y, Decl(relationComplexityError.ts, 6, 18))
26+
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
27+
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))
28+
29+
x = y;
30+
>x : Symbol(x, Decl(relationComplexityError.ts, 6, 12))
31+
>y : Symbol(y, Decl(relationComplexityError.ts, 6, 18))
32+
}
33+
34+
function f2(x: T1 | null, y: T1 & T2) {
35+
>f2 : Symbol(f2, Decl(relationComplexityError.ts, 8, 1))
36+
>x : Symbol(x, Decl(relationComplexityError.ts, 10, 12))
37+
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
38+
>y : Symbol(y, Decl(relationComplexityError.ts, 10, 25))
39+
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
40+
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))
41+
42+
x = y; // Complexity error
43+
>x : Symbol(x, Decl(relationComplexityError.ts, 10, 12))
44+
>y : Symbol(y, Decl(relationComplexityError.ts, 10, 25))
45+
}
46+

tests/baselines/reference/relationComplexityError.types

+38
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @strict: true
2+
// @noEmit: true
3+
4+
// Repro from #55630
5+
6+
type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
7+
type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
8+
type T2 = { a: string } | { b: number };
9+
10+
function f1(x: T1, y: T1 & T2) {
11+
x = y;
12+
}
13+
14+
function f2(x: T1 | null, y: T1 & T2) {
15+
x = y; // Complexity error
16+
}

0 commit comments

Comments
 (0)