diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..584c009e1417e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -2063,6 +2063,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { var evolvingArrayTypes: EvolvingArrayType[] = []; var undefinedProperties: SymbolTable = new Map(); var markerTypes = new Set(); + // Enum symbols whose member names are currently being late-bound, used to break the recursion + // that a computed enum member name referring back to the same enum would otherwise cause. + var enumsResolvingLateBoundNames = new Set(); var unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String); var resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving); @@ -13544,12 +13547,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function getDeclaredTypeOfEnum(symbol: Symbol): Type { const links = getSymbolLinks(symbol); if (!links.declaredType) { + // A member with a dynamic (computed) name can refer back to a member of the same enum + // (e.g. `enum E { [object] = 1, object = 2 }`). Determining whether that name is + // late-bindable resolves the referenced member's type, which re-enters this function; + // while that resolution is in progress we must not try to late-bind such names again, + // otherwise we recurse until the stack overflows. Computed names are not permitted on + // enum members anyway (a separate error is reported), so treating them as non-bindable + // here only affects already-invalid code. + const resolvingLateBoundNames = enumsResolvingLateBoundNames.has(symbol); + if (!resolvingLateBoundNames) { + enumsResolvingLateBoundNames.add(symbol); + } const memberTypeList: Type[] = []; if (symbol.declarations) { for (const declaration of symbol.declarations) { if (declaration.kind === SyntaxKind.EnumDeclaration) { for (const member of (declaration as EnumDeclaration).members) { - if (hasBindableName(member)) { + const bindable = resolvingLateBoundNames ? !hasDynamicName(member) : hasBindableName(member); + if (bindable) { const memberSymbol = getSymbolOfDeclaration(member); const value = getEnumMemberValue(member).value; const memberType = getFreshTypeOfLiteralType( @@ -13564,6 +13579,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } } + if (!resolvingLateBoundNames) { + enumsResolvingLateBoundNames.delete(symbol); + } const enumType = memberTypeList.length ? getUnionType(memberTypeList, UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined) : createComputedEnumType(symbol); @@ -13571,7 +13589,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { enumType.flags |= TypeFlags.EnumLiteral; enumType.symbol = symbol; } - links.declaredType = enumType; + links.declaredType ??= enumType; } return links.declaredType; } diff --git a/tests/baselines/reference/enumComputedNameSelfReferenceCrash.errors.txt b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.errors.txt new file mode 100644 index 0000000000000..137da927d822e --- /dev/null +++ b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.errors.txt @@ -0,0 +1,35 @@ +enumComputedNameSelfReferenceCrash.ts(6,5): error TS1164: Computed property names are not allowed in enums. +enumComputedNameSelfReferenceCrash.ts(13,5): error TS1164: Computed property names are not allowed in enums. +enumComputedNameSelfReferenceCrash.ts(19,5): error TS1164: Computed property names are not allowed in enums. + + +==== enumComputedNameSelfReferenceCrash.ts (3 errors) ==== + // Computed enum member names are not allowed, but a computed name that refers back to a member of + // the same enum must not send the checker into infinite recursion while resolving the enum type. + // https://github.com/microsoft/TypeScript/issues/63173 + + declare const enum E { + [object] = 1, + ~~~~~~~~ +!!! error TS1164: Computed property names are not allowed in enums. + A, + object = 10, + } + E.A.toString(); + + const enum F { + [F.A] = 1, + ~~~~~ +!!! error TS1164: Computed property names are not allowed in enums. + A = 2, + } + F.A.toString(); + + enum G { + [G.b] = 1, + ~~~~~ +!!! error TS1164: Computed property names are not allowed in enums. + b = 2, + } + G.b; + \ No newline at end of file diff --git a/tests/baselines/reference/enumComputedNameSelfReferenceCrash.js b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.js new file mode 100644 index 0000000000000..c868b7216bf6e --- /dev/null +++ b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.js @@ -0,0 +1,56 @@ +//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] //// + +//// [enumComputedNameSelfReferenceCrash.ts] +// Computed enum member names are not allowed, but a computed name that refers back to a member of +// the same enum must not send the checker into infinite recursion while resolving the enum type. +// https://github.com/microsoft/TypeScript/issues/63173 + +declare const enum E { + [object] = 1, + A, + object = 10, +} +E.A.toString(); + +const enum F { + [F.A] = 1, + A = 2, +} +F.A.toString(); + +enum G { + [G.b] = 1, + b = 2, +} +G.b; + + +//// [enumComputedNameSelfReferenceCrash.js] +"use strict"; +// Computed enum member names are not allowed, but a computed name that refers back to a member of +// the same enum must not send the checker into infinite recursion while resolving the enum type. +// https://github.com/microsoft/TypeScript/issues/63173 +2 /* E.A */.toString(); +2 /* F.A */.toString(); +var G; +(function (G) { + G[G[G.b] = 1] = G.b; + G[G["b"] = 2] = "b"; +})(G || (G = {})); +G.b; + + +//// [enumComputedNameSelfReferenceCrash.d.ts] +declare const enum E { + [object] = 1, + A = 2, + object = 10 +} +declare const enum F { + [F.A] = 1, + A = 2 +} +declare enum G { + [G.b] = 1, + b = 2 +} diff --git a/tests/baselines/reference/enumComputedNameSelfReferenceCrash.symbols b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.symbols new file mode 100644 index 0000000000000..89eeddf401669 --- /dev/null +++ b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.symbols @@ -0,0 +1,63 @@ +//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] //// + +=== enumComputedNameSelfReferenceCrash.ts === +// Computed enum member names are not allowed, but a computed name that refers back to a member of +// the same enum must not send the checker into infinite recursion while resolving the enum type. +// https://github.com/microsoft/TypeScript/issues/63173 + +declare const enum E { +>E : Symbol(E, Decl(enumComputedNameSelfReferenceCrash.ts, 0, 0)) + + [object] = 1, +>[object] : Symbol(E[object], Decl(enumComputedNameSelfReferenceCrash.ts, 4, 22)) +>object : Symbol(E.object, Decl(enumComputedNameSelfReferenceCrash.ts, 6, 6)) + + A, +>A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17)) + + object = 10, +>object : Symbol(E.object, Decl(enumComputedNameSelfReferenceCrash.ts, 6, 6)) +} +E.A.toString(); +>E.A.toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) +>E.A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17)) +>E : Symbol(E, Decl(enumComputedNameSelfReferenceCrash.ts, 0, 0)) +>A : Symbol(E.A, Decl(enumComputedNameSelfReferenceCrash.ts, 5, 17)) +>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) + +const enum F { +>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15)) + + [F.A] = 1, +>[F.A] : Symbol(F[F.A], Decl(enumComputedNameSelfReferenceCrash.ts, 11, 14)) +>F.A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14)) +>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15)) +>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14)) + + A = 2, +>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14)) +} +F.A.toString(); +>F.A.toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) +>F.A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14)) +>F : Symbol(F, Decl(enumComputedNameSelfReferenceCrash.ts, 9, 15)) +>A : Symbol(F.A, Decl(enumComputedNameSelfReferenceCrash.ts, 12, 14)) +>toString : Symbol(Number.toString, Decl(lib.es5.d.ts, --, --)) + +enum G { +>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15)) + + [G.b] = 1, +>[G.b] : Symbol(G[G.b], Decl(enumComputedNameSelfReferenceCrash.ts, 17, 8)) +>G.b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14)) +>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15)) +>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14)) + + b = 2, +>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14)) +} +G.b; +>G.b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14)) +>G : Symbol(G, Decl(enumComputedNameSelfReferenceCrash.ts, 15, 15)) +>b : Symbol(G.b, Decl(enumComputedNameSelfReferenceCrash.ts, 18, 14)) + diff --git a/tests/baselines/reference/enumComputedNameSelfReferenceCrash.types b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.types new file mode 100644 index 0000000000000..15e9fb3851405 --- /dev/null +++ b/tests/baselines/reference/enumComputedNameSelfReferenceCrash.types @@ -0,0 +1,109 @@ +//// [tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts] //// + +=== enumComputedNameSelfReferenceCrash.ts === +// Computed enum member names are not allowed, but a computed name that refers back to a member of +// the same enum must not send the checker into infinite recursion while resolving the enum type. +// https://github.com/microsoft/TypeScript/issues/63173 + +declare const enum E { +>E : E +> : ^ + + [object] = 1, +>[object] : E.__computed +> : ^^^^^^^^^^^^ +>object : E.object +> : ^^^^^^^^ +>1 : 1 +> : ^ + + A, +>A : E.A +> : ^^^ + + object = 10, +>object : E.object +> : ^^^^^^^^ +>10 : 10 +> : ^^ +} +E.A.toString(); +>E.A.toString() : string +> : ^^^^^^ +>E.A.toString : (radix?: number) => string +> : ^ ^^^ ^^^^^ +>E.A : E.A +> : ^^^ +>E : typeof E +> : ^^^^^^^^ +>A : E.A +> : ^^^ +>toString : (radix?: number) => string +> : ^ ^^^ ^^^^^ + +const enum F { +>F : F +> : ^ + + [F.A] = 1, +>[F.A] : F.__computed +> : ^^^^^^^^^^^^ +>F.A : F +> : ^ +>F : typeof F +> : ^^^^^^^^ +>A : F +> : ^ +>1 : 1 +> : ^ + + A = 2, +>A : F.A +> : ^^^ +>2 : 2 +> : ^ +} +F.A.toString(); +>F.A.toString() : string +> : ^^^^^^ +>F.A.toString : (radix?: number) => string +> : ^ ^^^ ^^^^^ +>F.A : F +> : ^ +>F : typeof F +> : ^^^^^^^^ +>A : F +> : ^ +>toString : (radix?: number) => string +> : ^ ^^^ ^^^^^ + +enum G { +>G : G +> : ^ + + [G.b] = 1, +>[G.b] : G.__computed +> : ^^^^^^^^^^^^ +>G.b : G +> : ^ +>G : typeof G +> : ^^^^^^^^ +>b : G +> : ^ +>1 : 1 +> : ^ + + b = 2, +>b : G.b +> : ^^^ +>2 : 2 +> : ^ +} +G.b; +>G.b : G +> : ^ +>G : typeof G +> : ^^^^^^^^ +>b : G +> : ^ + diff --git a/tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts b/tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts new file mode 100644 index 0000000000000..633154b99846f --- /dev/null +++ b/tests/cases/compiler/enumComputedNameSelfReferenceCrash.ts @@ -0,0 +1,25 @@ +// @target: es2015 +// @declaration: true + +// Computed enum member names are not allowed, but a computed name that refers back to a member of +// the same enum must not send the checker into infinite recursion while resolving the enum type. +// https://github.com/microsoft/TypeScript/issues/63173 + +declare const enum E { + [object] = 1, + A, + object = 10, +} +E.A.toString(); + +const enum F { + [F.A] = 1, + A = 2, +} +F.A.toString(); + +enum G { + [G.b] = 1, + b = 2, +} +G.b;