Skip to content

Commit c9bceb8

Browse files
committed
Detect comparisons between large unions or intersections
If their multiplied size is greater than 1E7 (chosen based on the repro in microsoft#41517), then we'll expend a large amount of time and memory comparing them, so alert the user instead. Fixes microsoft#41517
1 parent d156bb8 commit c9bceb8

12 files changed

+30681
-3
lines changed

src/compiler/checker.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16889,10 +16889,9 @@ namespace ts {
1688916889
if (source === target) return Ternary.True;
1689016890

1689116891
if (relation === identityRelation) {
16892-
return isIdenticalTo(source, target);
16892+
return isIdenticalTo(source, target, reportErrors);
1689316893
}
1689416894

16895-
1689616895
// We fastpath comparing a type parameter to exactly its constraint, as this is _super_ common,
1689716896
// and otherwise, for type parameters in large unions, causes us to need to compare the union to itself,
1689816897
// as we break down the _target_ union first, _then_ get the source constraint - so for every
@@ -16953,6 +16952,10 @@ namespace ts {
1695316952
return Ternary.False;
1695416953
}
1695516954

16955+
if (areUnionsOrIntersectionsTooLarge(source, target, reportErrors)) {
16956+
return Ternary.False;
16957+
}
16958+
1695616959
let result = Ternary.False;
1695716960
const saveErrorInfo = captureErrorCalculationState();
1695816961

@@ -17088,11 +17091,30 @@ namespace ts {
1708817091
}
1708917092
}
1709017093

17091-
function isIdenticalTo(source: Type, target: Type): Ternary {
17094+
function areUnionsOrIntersectionsTooLarge(source: Type, target: Type, reportErrors: boolean) {
17095+
if ((source.flags & TypeFlags.UnionOrIntersection) && (target.flags & TypeFlags.UnionOrIntersection)) {
17096+
const sourceSize = (source as UnionOrIntersectionType).types.length;
17097+
const targetSize = (target as UnionOrIntersectionType).types.length;
17098+
if (sourceSize * targetSize > 1E7) {
17099+
if (reportErrors) {
17100+
tracing.instant(tracing.Phase.CheckTypes, "areUnionsOrIntersectionsTooLarge_DepthLimit", { sourceId: source.id, sourceSize, targetId: target.id, targetSize });
17101+
reportError(Diagnostics.Expression_requires_comparison_of_excessively_large_unions_or_intersections);
17102+
}
17103+
return true;
17104+
}
17105+
}
17106+
17107+
return false;
17108+
}
17109+
17110+
function isIdenticalTo(source: Type, target: Type, reportErrors: boolean): Ternary {
1709217111
const flags = source.flags & target.flags;
1709317112
if (!(flags & TypeFlags.Substructure)) {
1709417113
return Ternary.False;
1709517114
}
17115+
if (areUnionsOrIntersectionsTooLarge(source, target, reportErrors)) {
17116+
return Ternary.False;
17117+
}
1709617118
if (flags & TypeFlags.UnionOrIntersection) {
1709717119
let result = eachTypeRelatedToSomeType(<UnionOrIntersectionType>source, <UnionOrIntersectionType>target);
1709817120
if (result) {

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3211,6 +3211,10 @@
32113211
"category": "Error",
32123212
"code": 2796
32133213
},
3214+
"Expression requires comparison of excessively large unions or intersections.": {
3215+
"category": "Error",
3216+
"code": 2797
3217+
},
32143218

32153219
"Import declaration '{0}' is using private name '{1}'.": {
32163220
"category": "Error",

tests/baselines/reference/largeUnionIntersectionComparisons.errors.txt

Lines changed: 354 additions & 0 deletions
Large diffs are not rendered by default.

tests/baselines/reference/largeUnionIntersectionComparisons.js

Lines changed: 350 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/baselines/reference/largeUnionIntersectionComparisons.symbols

Lines changed: 7095 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)