Skip to content

Commit e2798ca

Browse files
committed
fix(checker): avoid stack overflow for self-referential computed enum member names
A computed enum member name that refers back to a member of the same enum (e.g. `enum E { [object] = 1, object = 2 }`) caused the checker to recurse until the stack overflowed: getDeclaredTypeOfEnum determines whether each member name is late-bindable, which resolves the referenced member's type, which re-enters getDeclaredTypeOfEnum for the same enum. Track the enums whose member names are currently being late-bound and skip late-binding of computed names while that resolution is in progress. Computed names are not permitted on enum members anyway (TS1164 is still reported), so this only affects already-invalid code, and the non-recursive paths are unchanged (no baseline updates outside the new regression test). Fixes #63173
1 parent 7964e22 commit e2798ca

6 files changed

Lines changed: 308 additions & 2 deletions

src/compiler/checker.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
20632063
var evolvingArrayTypes: EvolvingArrayType[] = [];
20642064
var undefinedProperties: SymbolTable = new Map();
20652065
var markerTypes = new Set<number>();
2066+
// Enum symbols whose member names are currently being late-bound, used to break the recursion
2067+
// that a computed enum member name referring back to the same enum would otherwise cause.
2068+
var enumsResolvingLateBoundNames = new Set<Symbol>();
20662069

20672070
var unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String);
20682071
var resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving);
@@ -13544,12 +13547,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1354413547
function getDeclaredTypeOfEnum(symbol: Symbol): Type {
1354513548
const links = getSymbolLinks(symbol);
1354613549
if (!links.declaredType) {
13550+
// A member with a dynamic (computed) name can refer back to a member of the same enum
13551+
// (e.g. `enum E { [object] = 1, object = 2 }`). Determining whether that name is
13552+
// late-bindable resolves the referenced member's type, which re-enters this function;
13553+
// while that resolution is in progress we must not try to late-bind such names again,
13554+
// otherwise we recurse until the stack overflows. Computed names are not permitted on
13555+
// enum members anyway (a separate error is reported), so treating them as non-bindable
13556+
// here only affects already-invalid code.
13557+
const resolvingLateBoundNames = enumsResolvingLateBoundNames.has(symbol);
13558+
if (!resolvingLateBoundNames) {
13559+
enumsResolvingLateBoundNames.add(symbol);
13560+
}
1354713561
const memberTypeList: Type[] = [];
1354813562
if (symbol.declarations) {
1354913563
for (const declaration of symbol.declarations) {
1355013564
if (declaration.kind === SyntaxKind.EnumDeclaration) {
1355113565
for (const member of (declaration as EnumDeclaration).members) {
13552-
if (hasBindableName(member)) {
13566+
const bindable = resolvingLateBoundNames ? !hasDynamicName(member) : hasBindableName(member);
13567+
if (bindable) {
1355313568
const memberSymbol = getSymbolOfDeclaration(member);
1355413569
const value = getEnumMemberValue(member).value;
1355513570
const memberType = getFreshTypeOfLiteralType(
@@ -13564,14 +13579,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1356413579
}
1356513580
}
1356613581
}
13582+
if (!resolvingLateBoundNames) {
13583+
enumsResolvingLateBoundNames.delete(symbol);
13584+
}
1356713585
const enumType = memberTypeList.length ?
1356813586
getUnionType(memberTypeList, UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined) :
1356913587
createComputedEnumType(symbol);
1357013588
if (enumType.flags & TypeFlags.Union) {
1357113589
enumType.flags |= TypeFlags.EnumLiteral;
1357213590
enumType.symbol = symbol;
1357313591
}
13574-
links.declaredType = enumType;
13592+
links.declaredType ??= enumType;
1357513593
}
1357613594
return links.declaredType;
1357713595
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
enumComputedNameSelfReferenceCrash.ts(6,5): error TS1164: Computed property names are not allowed in enums.
2+
enumComputedNameSelfReferenceCrash.ts(13,5): error TS1164: Computed property names are not allowed in enums.
3+
enumComputedNameSelfReferenceCrash.ts(19,5): error TS1164: Computed property names are not allowed in enums.
4+
5+
6+
==== enumComputedNameSelfReferenceCrash.ts (3 errors) ====
7+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
8+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
9+
// https://github.com/microsoft/TypeScript/issues/63173
10+
11+
declare const enum E {
12+
[object] = 1,
13+
~~~~~~~~
14+
!!! error TS1164: Computed property names are not allowed in enums.
15+
A,
16+
object = 10,
17+
}
18+
E.A.toString();
19+
20+
const enum F {
21+
[F.A] = 1,
22+
~~~~~
23+
!!! error TS1164: Computed property names are not allowed in enums.
24+
A = 2,
25+
}
26+
F.A.toString();
27+
28+
enum G {
29+
[G.b] = 1,
30+
~~~~~
31+
!!! error TS1164: Computed property names are not allowed in enums.
32+
b = 2,
33+
}
34+
G.b;
35+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] ////
2+
3+
//// [enumComputedNameSelfReferenceCrash.ts]
4+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
5+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
6+
// https://github.com/microsoft/TypeScript/issues/63173
7+
8+
declare const enum E {
9+
[object] = 1,
10+
A,
11+
object = 10,
12+
}
13+
E.A.toString();
14+
15+
const enum F {
16+
[F.A] = 1,
17+
A = 2,
18+
}
19+
F.A.toString();
20+
21+
enum G {
22+
[G.b] = 1,
23+
b = 2,
24+
}
25+
G.b;
26+
27+
28+
//// [enumComputedNameSelfReferenceCrash.js]
29+
"use strict";
30+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
31+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
32+
// https://github.com/microsoft/TypeScript/issues/63173
33+
2 /* E.A */.toString();
34+
2 /* F.A */.toString();
35+
var G;
36+
(function (G) {
37+
G[G[G.b] = 1] = G.b;
38+
G[G["b"] = 2] = "b";
39+
})(G || (G = {}));
40+
G.b;
41+
42+
43+
//// [enumComputedNameSelfReferenceCrash.d.ts]
44+
declare const enum E {
45+
[object] = 1,
46+
A = 2,
47+
object = 10
48+
}
49+
declare const enum F {
50+
[F.A] = 1,
51+
A = 2
52+
}
53+
declare enum G {
54+
[G.b] = 1,
55+
b = 2
56+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] ////
2+
3+
=== enumComputedNameSelfReferenceCrash.ts ===
4+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
5+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
6+
// https://github.com/microsoft/TypeScript/issues/63173
7+
8+
declare const enum E {
9+
>E : Symbol(E, Decl(enumComputedNameSelfReferenceCrash.ts, 0, 0))
10+
11+
[object] = 1,
12+
>[object] : Symbol(E[object], Decl(enumComputedNameSelfReferenceCrash.ts, 4, 22))
13+
>object : Symbol(E.object, Decl(enumComputedNameSelfReferenceCrash.ts, 6, 6))
14+
15+
A,
16+
>A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17))
17+
18+
object = 10,
19+
>object : Symbol(E.object, Decl(enumComputedNameSelfReferenceCrash.ts, 6, 6))
20+
}
21+
E.A.toString();
22+
>E.A.toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --))
23+
>E.A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17))
24+
>E : Symbol(E, Decl(enumComputedNameSelfReferenceCrash.ts, 0, 0))
25+
>A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17))
26+
>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --))
27+
28+
const enum F {
29+
>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15))
30+
31+
[F.A] = 1,
32+
>[F.A] : Symbol(F[F.A], Decl(enumComputedNameSelfReferenceCrash.ts, 11, 14))
33+
>F.A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14))
34+
>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15))
35+
>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14))
36+
37+
A = 2,
38+
>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14))
39+
}
40+
F.A.toString();
41+
>F.A.toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --))
42+
>F.A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14))
43+
>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15))
44+
>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14))
45+
>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --))
46+
47+
enum G {
48+
>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15))
49+
50+
[G.b] = 1,
51+
>[G.b] : Symbol(G[G.b], Decl(enumComputedNameSelfReferenceCrash.ts, 17, 8))
52+
>G.b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14))
53+
>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15))
54+
>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14))
55+
56+
b = 2,
57+
>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14))
58+
}
59+
G.b;
60+
>G.b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14))
61+
>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15))
62+
>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14))
63+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] ////
2+
3+
=== enumComputedNameSelfReferenceCrash.ts ===
4+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
5+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
6+
// https://github.com/microsoft/TypeScript/issues/63173
7+
8+
declare const enum E {
9+
>E : E
10+
> : ^
11+
12+
[object] = 1,
13+
>[object] : E.__computed
14+
> : ^^^^^^^^^^^^
15+
>object : E.object
16+
> : ^^^^^^^^
17+
>1 : 1
18+
> : ^
19+
20+
A,
21+
>A : E.A
22+
> : ^^^
23+
24+
object = 10,
25+
>object : E.object
26+
> : ^^^^^^^^
27+
>10 : 10
28+
> : ^^
29+
}
30+
E.A.toString();
31+
>E.A.toString() : string
32+
> : ^^^^^^
33+
>E.A.toString : (radix?: number) => string
34+
> : ^ ^^^ ^^^^^
35+
>E.A : E.A
36+
> : ^^^
37+
>E : typeof E
38+
> : ^^^^^^^^
39+
>A : E.A
40+
> : ^^^
41+
>toString : (radix?: number) => string
42+
> : ^ ^^^ ^^^^^
43+
44+
const enum F {
45+
>F : F
46+
> : ^
47+
48+
[F.A] = 1,
49+
>[F.A] : F.__computed
50+
> : ^^^^^^^^^^^^
51+
>F.A : F
52+
> : ^
53+
>F : typeof F
54+
> : ^^^^^^^^
55+
>A : F
56+
> : ^
57+
>1 : 1
58+
> : ^
59+
60+
A = 2,
61+
>A : F.A
62+
> : ^^^
63+
>2 : 2
64+
> : ^
65+
}
66+
F.A.toString();
67+
>F.A.toString() : string
68+
> : ^^^^^^
69+
>F.A.toString : (radix?: number) => string
70+
> : ^ ^^^ ^^^^^
71+
>F.A : F
72+
> : ^
73+
>F : typeof F
74+
> : ^^^^^^^^
75+
>A : F
76+
> : ^
77+
>toString : (radix?: number) => string
78+
> : ^ ^^^ ^^^^^
79+
80+
enum G {
81+
>G : G
82+
> : ^
83+
84+
[G.b] = 1,
85+
>[G.b] : G.__computed
86+
> : ^^^^^^^^^^^^
87+
>G.b : G
88+
> : ^
89+
>G : typeof G
90+
> : ^^^^^^^^
91+
>b : G
92+
> : ^
93+
>1 : 1
94+
> : ^
95+
96+
b = 2,
97+
>b : G.b
98+
> : ^^^
99+
>2 : 2
100+
> : ^
101+
}
102+
G.b;
103+
>G.b : G
104+
> : ^
105+
>G : typeof G
106+
> : ^^^^^^^^
107+
>b : G
108+
> : ^
109+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @target: es2015
2+
// @declaration: true
3+
4+
// Computed enum member names are not allowed, but a computed name that refers back to a member of
5+
// the same enum must not send the checker into infinite recursion while resolving the enum type.
6+
// https://github.com/microsoft/TypeScript/issues/63173
7+
8+
declare const enum E {
9+
[object] = 1,
10+
A,
11+
object = 10,
12+
}
13+
E.A.toString();
14+
15+
const enum F {
16+
[F.A] = 1,
17+
A = 2,
18+
}
19+
F.A.toString();
20+
21+
enum G {
22+
[G.b] = 1,
23+
b = 2,
24+
}
25+
G.b;

0 commit comments

Comments
 (0)