Skip to content

Error on excessive relation complexity #55851

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20778,6 +20778,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let skipParentCounter = 0; // How many errors should be skipped 'above' in the elaboration pyramid
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: DiagnosticAndArguments[] | undefined;
// In Node.js, the maximum number of elements in a map is 2^24. We limit the number of entries an invocation
// of checkTypeRelatedTo can add to a relation to 1/8th of its remaining capacity.
let relationCount = (16_000_000 - relation.size) >> 3;

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

Expand All @@ -20786,8 +20789,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
reportIncompatibleStack();
}
if (overflow) {
// Record this relation as having failed such that we don't attempt the overflowing operation again.
const id = getRelationKey(source, target, /*intersectionState*/ IntersectionState.None, relation, /*ignoreConstraints*/ false);
relation.set(id, RelationComparisonResult.Reported | RelationComparisonResult.Failed);
tracing?.instant(tracing.Phase.CheckTypes, "checkTypeRelatedTo_DepthLimit", { sourceId: source.id, targetId: target.id, depth: sourceDepth, targetDepth });
const diag = error(errorNode || currentNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target));
const message = relationCount <= 0 ?
Diagnostics.Excessive_complexity_comparing_types_0_and_1 :
Diagnostics.Excessive_stack_depth_comparing_types_0_and_1;
const diag = error(errorNode || currentNode, message, typeToString(source), typeToString(target));
if (errorOutputContainer) {
(errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag);
}
Expand Down Expand Up @@ -21420,6 +21429,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// we need to deconstruct unions before intersections (because unions are always at the top),
// and we need to handle "each" relations before "some" relations for the same kind of type.
if (source.flags & TypeFlags.Union) {
if (target.flags & TypeFlags.Union) {
// Intersections of union types are normalized into unions of intersection types, and such normalized
// unions can get very large and expensive to relate. The following fast path checks if the source union
// originated in an intersection. If so, and if that intersection contains the target type, then we know
// the result to be true (for any two types A and B, A & B is related to both A and B).
const sourceOrigin = (source as UnionType).origin;
if (sourceOrigin && sourceOrigin.flags & TypeFlags.Intersection && target.aliasSymbol && contains((sourceOrigin as IntersectionType).types, target)) {
return Ternary.True;
}
// Similarly, in unions of unions the we preserve the original list of unions. This original list is often
// much shorter than the normalized result, so we scan it in the following fast path.
const targetOrigin = (target as UnionType).origin;
if (targetOrigin && targetOrigin.flags & TypeFlags.Union && source.aliasSymbol && contains((targetOrigin as UnionType).types, source)) {
return Ternary.True;
}
}
return relation === comparableRelation ?
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) :
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState);
Expand Down Expand Up @@ -21671,6 +21696,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return entry & RelationComparisonResult.Succeeded ? Ternary.True : Ternary.False;
}
}
if (relationCount <= 0) {
overflow = true;
return Ternary.False;
}
if (!maybeKeys) {
maybeKeys = [];
maybeKeysSet = new Set();
Expand Down Expand Up @@ -21768,6 +21797,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// A false result goes straight into global cache (when something is false under
// assumptions it will also be false without assumptions)
relation.set(id, (reportErrors ? RelationComparisonResult.Reported : 0) | RelationComparisonResult.Failed | propagatingVarianceFlags);
relationCount--;
resetMaybeStack(/*markAllAsSucceeded*/ false);
}
return result;
Expand All @@ -21777,6 +21807,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
maybeKeysSet.delete(maybeKeys[i]);
if (markAllAsSucceeded) {
relation.set(maybeKeys[i], RelationComparisonResult.Succeeded | propagatingVarianceFlags);
relationCount--;
}
}
maybeCount = maybeStart;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,10 @@
"category": "Error",
"code": 2858
},
"Excessive complexity comparing types '{0}' and '{1}'.": {
"category": "Error",
"code": 2859
},

"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
Expand Down
23 changes: 23 additions & 0 deletions tests/baselines/reference/relationComplexityError.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
relationComplexityError.ts(12,5): error TS2322: Type 'T1 & T2' is not assignable to type 'T1 | null'.
relationComplexityError.ts(12,5): error TS2859: Excessive complexity comparing types 'T1 & T2' and 'T1 | null'.


==== relationComplexityError.ts (2 errors) ====
// Repro from #55630

type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
type T2 = { a: string } | { b: number };

function f1(x: T1, y: T1 & T2) {
x = y;
}

function f2(x: T1 | null, y: T1 & T2) {
x = y; // Complexity error
~
!!! error TS2322: Type 'T1 & T2' is not assignable to type 'T1 | null'.
~~~~~
!!! error TS2859: Excessive complexity comparing types 'T1 & T2' and 'T1 | null'.
}

46 changes: 46 additions & 0 deletions tests/baselines/reference/relationComplexityError.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//// [tests/cases/compiler/relationComplexityError.ts] ////

=== relationComplexityError.ts ===
// Repro from #55630

type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))

type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))
>Digits : Symbol(Digits, Decl(relationComplexityError.ts, 0, 0))

type T2 = { a: string } | { b: number };
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))
>a : Symbol(a, Decl(relationComplexityError.ts, 4, 11))
>b : Symbol(b, Decl(relationComplexityError.ts, 4, 27))

function f1(x: T1, y: T1 & T2) {
>f1 : Symbol(f1, Decl(relationComplexityError.ts, 4, 40))
>x : Symbol(x, Decl(relationComplexityError.ts, 6, 12))
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
>y : Symbol(y, Decl(relationComplexityError.ts, 6, 18))
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))

x = y;
>x : Symbol(x, Decl(relationComplexityError.ts, 6, 12))
>y : Symbol(y, Decl(relationComplexityError.ts, 6, 18))
}

function f2(x: T1 | null, y: T1 & T2) {
>f2 : Symbol(f2, Decl(relationComplexityError.ts, 8, 1))
>x : Symbol(x, Decl(relationComplexityError.ts, 10, 12))
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
>y : Symbol(y, Decl(relationComplexityError.ts, 10, 25))
>T1 : Symbol(T1, Decl(relationComplexityError.ts, 2, 72))
>T2 : Symbol(T2, Decl(relationComplexityError.ts, 3, 61))

x = y; // Complexity error
>x : Symbol(x, Decl(relationComplexityError.ts, 10, 12))
>y : Symbol(y, Decl(relationComplexityError.ts, 10, 25))
}

38 changes: 38 additions & 0 deletions tests/baselines/reference/relationComplexityError.types

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions tests/cases/compiler/relationComplexityError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @strict: true
// @noEmit: true

// Repro from #55630

type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type T1 = `${Digits}${Digits}${Digits}${Digits}` | undefined;
type T2 = { a: string } | { b: number };

function f1(x: T1, y: T1 & T2) {
x = y;
}

function f2(x: T1 | null, y: T1 & T2) {
x = y; // Complexity error
}