From 146a7bcd01113366b5168763932dfc0d43b8d175 Mon Sep 17 00:00:00 2001 From: andre-fajar-n Date: Mon, 12 Oct 2026 14:42:58 +0700 Subject: [PATCH 1/3] fix: Fix MemberList panic on enum element access with merged namespace exports --- tsc/internal/checker/checker.go | 4 ++- ...zerNamespaceExportElementAccess.errors.txt | 16 +++++++++++ ...InitializerNamespaceExportElementAccess.js | 27 +++++++++++++++++++ ...alizerNamespaceExportElementAccess.symbols | 21 +++++++++++++++ ...tializerNamespaceExportElementAccess.types | 22 +++++++++++++++ ...InitializerNamespaceExportElementAccess.ts | 9 +++++++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt create mode 100644 tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js create mode 100644 tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types create mode 100644 tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 345ea00ce1437..370f05963e5dd 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -24600,7 +24600,9 @@ func (c *Checker) evaluateEntity(expr *ast.Node, location *ast.Node) evaluator.R if rootSymbol != nil && rootSymbol.Flags()&ast.SymbolFlagsEnum != 0 { name := expr.AsElementAccessExpression().ArgumentExpression.Text() member := rootSymbol.Exports()[name] - if member != nil { + // Merged namespace+enum symbols can export non-enum members (e.g. a class). + // Only treat EnumMember exports as constant enum values — mirror the identifier path. + if member != nil && member.Flags()&ast.SymbolFlagsEnumMember != 0 { if location != nil { return c.evaluateEnumMember(expr, member, location) } diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt new file mode 100644 index 0000000000000..69bf95c4d7248 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt @@ -0,0 +1,16 @@ +enumInitializerNamespaceExportElementAccess.ts(8,9): error TS18033: Type 'typeof C' is not assignable to type 'number' as required for computed enum member values. + + +==== enumInitializerNamespaceExportElementAccess.ts (1 errors) ==== + // Namespace export merged with enum must not be treated as an enum member via E["C"]. + // Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock + + namespace E { + export class C {} + } + enum E { + D = E["C"], + ~~~~~~ +!!! error TS18033: Type 'typeof C' is not assignable to type 'number' as required for computed enum member values. + } + \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js new file mode 100644 index 0000000000000..559adf2a32a78 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js @@ -0,0 +1,27 @@ +//// [tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts] //// + +//// [enumInitializerNamespaceExportElementAccess.ts] +// Namespace export merged with enum must not be treated as an enum member via E["C"]. +// Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock + +namespace E { + export class C {} +} +enum E { + D = E["C"], +} + + +//// [enumInitializerNamespaceExportElementAccess.js] +"use strict"; +// Namespace export merged with enum must not be treated as an enum member via E["C"]. +// Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock +var E; +(function (E) { + class C { + } + E.C = C; +})(E || (E = {})); +(function (E) { + E[E["D"] = E["C"]] = "D"; +})(E || (E = {})); diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols new file mode 100644 index 0000000000000..63f06800aca92 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols @@ -0,0 +1,21 @@ +//// [tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts] //// + +=== enumInitializerNamespaceExportElementAccess.ts === +// Namespace export merged with enum must not be treated as an enum member via E["C"]. +// Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock + +namespace E { +>E : Symbol(E, Decl(enumInitializerNamespaceExportElementAccess.ts, 0, 0), Decl(enumInitializerNamespaceExportElementAccess.ts, 5, 1)) + + export class C {} +>C : Symbol(C, Decl(enumInitializerNamespaceExportElementAccess.ts, 3, 13)) +} +enum E { +>E : Symbol(E, Decl(enumInitializerNamespaceExportElementAccess.ts, 0, 0), Decl(enumInitializerNamespaceExportElementAccess.ts, 5, 1)) + + D = E["C"], +>D : Symbol(E.D, Decl(enumInitializerNamespaceExportElementAccess.ts, 6, 8)) +>E : Symbol(E, Decl(enumInitializerNamespaceExportElementAccess.ts, 0, 0), Decl(enumInitializerNamespaceExportElementAccess.ts, 5, 1)) +>"C" : Symbol(E.C, Decl(enumInitializerNamespaceExportElementAccess.ts, 3, 13)) +} + diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types new file mode 100644 index 0000000000000..9cd29af7731e9 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types @@ -0,0 +1,22 @@ +//// [tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts] //// + +=== enumInitializerNamespaceExportElementAccess.ts === +// Namespace export merged with enum must not be treated as an enum member via E["C"]. +// Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock + +namespace E { +>E : typeof E + + export class C {} +>C : C +} +enum E { +>E : E + + D = E["C"], +>D : E.D +>E["C"] : typeof E.C +>E : typeof E +>"C" : "C" +} + diff --git a/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts b/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts new file mode 100644 index 0000000000000..e36ee8c24e47f --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts @@ -0,0 +1,9 @@ +// Namespace export merged with enum must not be treated as an enum member via E["C"]. +// Previously panicked: Unhandled case in Node.MemberList: KindModuleBlock + +namespace E { + export class C {} +} +enum E { + D = E["C"], +} From c22763173b575b8575f01e216d7fff744c48cf73 Mon Sep 17 00:00:00 2001 From: andre-fajar-n Date: Mon, 12 Oct 2026 15:20:42 +0700 Subject: [PATCH 2/3] fix: Resolve aliases before EnumMember check in enum element access --- tsc/internal/checker/checker.go | 18 ++++++++---- ...zerNamespaceExportElementAccess.errors.txt | 12 ++++++++ ...InitializerNamespaceExportElementAccess.js | 25 ++++++++++++++++ ...alizerNamespaceExportElementAccess.symbols | 27 +++++++++++++++++ ...tializerNamespaceExportElementAccess.types | 29 +++++++++++++++++++ ...InitializerNamespaceExportElementAccess.ts | 12 ++++++++ 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 370f05963e5dd..6e57ed8d7deeb 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -24600,13 +24600,19 @@ func (c *Checker) evaluateEntity(expr *ast.Node, location *ast.Node) evaluator.R if rootSymbol != nil && rootSymbol.Flags()&ast.SymbolFlagsEnum != 0 { name := expr.AsElementAccessExpression().ArgumentExpression.Text() member := rootSymbol.Exports()[name] - // Merged namespace+enum symbols can export non-enum members (e.g. a class). - // Only treat EnumMember exports as constant enum values — mirror the identifier path. - if member != nil && member.Flags()&ast.SymbolFlagsEnumMember != 0 { - if location != nil { - return c.evaluateEnumMember(expr, member, location) + if member != nil { + // Follow aliases like resolveEntityName (property-access path), e.g. export import C = F.A. + for member.Flags()&ast.SymbolFlagsValue == 0 && member.Flags()&ast.SymbolFlagsAlias != 0 { + member = c.resolveAlias(member) + } + // Merged namespace+enum symbols can export non-enum members (e.g. a class). + // Only treat EnumMember exports as constant enum values — mirror the identifier path. + if member.Flags()&ast.SymbolFlagsEnumMember != 0 { + if location != nil { + return c.evaluateEnumMember(expr, member, location) + } + return c.getEnumMemberValue(member.ValueDeclaration()) } - return c.getEnumMemberValue(member.ValueDeclaration()) } } } diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt index 69bf95c4d7248..7818ffce72b99 100644 --- a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.errors.txt @@ -13,4 +13,16 @@ enumInitializerNamespaceExportElementAccess.ts(8,9): error TS18033: Type 'typeof ~~~~~~ !!! error TS18033: Type 'typeof C' is not assignable to type 'number' as required for computed enum member values. } + + // Alias exports that resolve to enum members must still constant-evaluate (like E.C). + enum F { + A = 1, + } + namespace E2 { + export import C = F.A; + } + enum E2 { + D = E2["C"], + Next, + } \ No newline at end of file diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js index 559adf2a32a78..e5312dbec4852 100644 --- a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.js @@ -10,6 +10,18 @@ namespace E { enum E { D = E["C"], } + +// Alias exports that resolve to enum members must still constant-evaluate (like E.C). +enum F { + A = 1, +} +namespace E2 { + export import C = F.A; +} +enum E2 { + D = E2["C"], + Next, +} //// [enumInitializerNamespaceExportElementAccess.js] @@ -25,3 +37,16 @@ var E; (function (E) { E[E["D"] = E["C"]] = "D"; })(E || (E = {})); +// Alias exports that resolve to enum members must still constant-evaluate (like E.C). +var F; +(function (F) { + F[F["A"] = 1] = "A"; +})(F || (F = {})); +var E2; +(function (E2) { + E2.C = F.A; +})(E2 || (E2 = {})); +(function (E2) { + E2[E2["D"] = 1] = "D"; + E2[E2["Next"] = 2] = "Next"; +})(E2 || (E2 = {})); diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols index 63f06800aca92..f5e6db5dfc5fe 100644 --- a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.symbols @@ -19,3 +19,30 @@ enum E { >"C" : Symbol(E.C, Decl(enumInitializerNamespaceExportElementAccess.ts, 3, 13)) } +// Alias exports that resolve to enum members must still constant-evaluate (like E.C). +enum F { +>F : Symbol(F, Decl(enumInitializerNamespaceExportElementAccess.ts, 8, 1)) + + A = 1, +>A : Symbol(F.A, Decl(enumInitializerNamespaceExportElementAccess.ts, 11, 8)) +} +namespace E2 { +>E2 : Symbol(E2, Decl(enumInitializerNamespaceExportElementAccess.ts, 13, 1), Decl(enumInitializerNamespaceExportElementAccess.ts, 16, 1)) + + export import C = F.A; +>C : Symbol(C, Decl(enumInitializerNamespaceExportElementAccess.ts, 14, 14)) +>F : Symbol(F, Decl(enumInitializerNamespaceExportElementAccess.ts, 8, 1)) +>A : Symbol(C, Decl(enumInitializerNamespaceExportElementAccess.ts, 11, 8)) +} +enum E2 { +>E2 : Symbol(E2, Decl(enumInitializerNamespaceExportElementAccess.ts, 13, 1), Decl(enumInitializerNamespaceExportElementAccess.ts, 16, 1)) + + D = E2["C"], +>D : Symbol(E2.D, Decl(enumInitializerNamespaceExportElementAccess.ts, 17, 9)) +>E2 : Symbol(E2, Decl(enumInitializerNamespaceExportElementAccess.ts, 13, 1), Decl(enumInitializerNamespaceExportElementAccess.ts, 16, 1)) +>"C" : Symbol(E2.C, Decl(enumInitializerNamespaceExportElementAccess.ts, 14, 14)) + + Next, +>Next : Symbol(E2.Next, Decl(enumInitializerNamespaceExportElementAccess.ts, 18, 16)) +} + diff --git a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types index 9cd29af7731e9..ade9d221407d6 100644 --- a/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types +++ b/tsc/testdata/baselines/reference/compiler/enumInitializerNamespaceExportElementAccess.types @@ -20,3 +20,32 @@ enum E { >"C" : "C" } +// Alias exports that resolve to enum members must still constant-evaluate (like E.C). +enum F { +>F : F + + A = 1, +>A : F.A +>1 : 1 +} +namespace E2 { +>E2 : typeof E2 + + export import C = F.A; +>C : F.A +>F : F +>A : F.A +} +enum E2 { +>E2 : E2 + + D = E2["C"], +>D : E2.D +>E2["C"] : F +>E2 : typeof E2 +>"C" : "C" + + Next, +>Next : E2.Next +} + diff --git a/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts b/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts index e36ee8c24e47f..9a40c89e1baf4 100644 --- a/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts +++ b/tsc/testdata/tests/cases/compiler/enumInitializerNamespaceExportElementAccess.ts @@ -7,3 +7,15 @@ namespace E { enum E { D = E["C"], } + +// Alias exports that resolve to enum members must still constant-evaluate (like E.C). +enum F { + A = 1, +} +namespace E2 { + export import C = F.A; +} +enum E2 { + D = E2["C"], + Next, +} From bbbce585555dab94b70efb6e3bccacd3c4df7041 Mon Sep 17 00:00:00 2001 From: andre-fajar-n Date: Thu, 29 Oct 2026 09:23:10 +0700 Subject: [PATCH 3/3] refactor: Share alias and enum-member helpers in evaluateEntity --- tsc/internal/checker/checker.go | 49 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 6d77b6d3d0d83..94ed055ec31ce 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -16240,9 +16240,7 @@ func (c *Checker) resolveEntityName(name *ast.Node, meaning ast.SymbolFlags, ign c.markSymbolOfAliasDeclarationIfTypeOnly(getAliasDeclarationFromName(name), nil) } // We know a symbol with the given meaning exists along the alias chain, so resolve until we find it. - for symbol.Flags()&meaning == 0 && !dontResolveAlias && symbol.Flags()&ast.SymbolFlagsAlias != 0 { - symbol = c.resolveAlias(symbol) - } + symbol = c.resolveAliasRecursively(symbol, meaning, dontResolveAlias) } return symbol } @@ -16683,6 +16681,16 @@ func (c *Checker) ResolveAlias(symbol *ast.Symbol) (*ast.Symbol, bool) { return resolved, resolved != c.unknownSymbol } +// resolveAliasRecursively follows alias symbols until one with the requested meaning is found +// (or dontResolveAlias is set). Used by resolveEntityName and constant enum ElementAccess evaluation — +// the only places that fully walk the alias chain for a meaning. +func (c *Checker) resolveAliasRecursively(symbol *ast.Symbol, meaning ast.SymbolFlags, dontResolveAlias bool) *ast.Symbol { + for symbol.Flags()&meaning == 0 && !dontResolveAlias && symbol.Flags()&ast.SymbolFlagsAlias != 0 { + symbol = c.resolveAlias(symbol) + } + return symbol +} + // Resolve an alias symbol to the first target symbol in the resolution chain that includes some other // meaning. Pure aliases are eagerly resolved and any type-only markers are back-propagated to the original // symbol. The function panics if the argument is not a symbol with an alias meaning. @@ -24754,11 +24762,8 @@ func (c *Checker) evaluateEntity(expr *ast.Node, location *ast.Node) evaluator.R return evaluator.NewResult(jsnum.FromString(expr.Text()), false, false, false) } } - if symbol.Flags()&ast.SymbolFlagsEnumMember != 0 { - if location != nil { - return c.evaluateEnumMember(expr, symbol, location) - } - return c.getEnumMemberValue(symbol.ValueDeclaration()) + if result, ok := c.tryEvaluateEnumMemberSymbol(expr, symbol, location); ok { + return result } if c.isConstantVariable(symbol) { declaration := symbol.ValueDeclaration() @@ -24780,17 +24785,10 @@ func (c *Checker) evaluateEntity(expr *ast.Node, location *ast.Node) evaluator.R name := expr.AsElementAccessExpression().ArgumentExpression.Text() member := rootSymbol.Exports().Get(name) if member != nil { - // Follow aliases like resolveEntityName (property-access path), e.g. export import C = F.A. - for member.Flags()&ast.SymbolFlagsValue == 0 && member.Flags()&ast.SymbolFlagsAlias != 0 { - member = c.resolveAlias(member) - } - // Merged namespace+enum symbols can export non-enum members (e.g. a class). - // Only treat EnumMember exports as constant enum values — mirror the identifier path. - if member.Flags()&ast.SymbolFlagsEnumMember != 0 { - if location != nil { - return c.evaluateEnumMember(expr, member, location) - } - return c.getEnumMemberValue(member.ValueDeclaration()) + // e.g. export import C = F.A — walk aliases until a Value meaning appears. + member = c.resolveAliasRecursively(member, ast.SymbolFlagsValue, false /*dontResolveAlias*/) + if result, ok := c.tryEvaluateEnumMemberSymbol(expr, member, location); ok { + return result } } } @@ -24800,6 +24798,19 @@ func (c *Checker) evaluateEntity(expr *ast.Node, location *ast.Node) evaluator.R panic("Unhandled case in evaluateEntity") } +// tryEvaluateEnumMemberSymbol evaluates a symbol as a constant enum member when applicable. +// Shared by Identifier/PropertyAccess and ElementAccess paths in evaluateEntity. Merged +// namespace+enum symbols can export non-enum members (e.g. a class); those return ok=false. +func (c *Checker) tryEvaluateEnumMemberSymbol(expr *ast.Node, symbol *ast.Symbol, location *ast.Node) (evaluator.Result, bool) { + if symbol.Flags()&ast.SymbolFlagsEnumMember == 0 { + return evaluator.NewResult(nil, false, false, false), false + } + if location != nil { + return c.evaluateEnumMember(expr, symbol, location), true + } + return c.getEnumMemberValue(symbol.ValueDeclaration()), true +} + func (c *Checker) evaluateEnumMember(expr *ast.Node, symbol *ast.Symbol, location *ast.Node) evaluator.Result { declaration := symbol.ValueDeclaration() if declaration == nil || declaration == location {