From 992ae423e361e8ba7a9240d803d5b63c484b7334 Mon Sep 17 00:00:00 2001 From: Daniel Theophanes Date: Tue, 27 Jan 2026 15:07:31 -0600 Subject: [PATCH] internal/transformers/tstransformers: fix unqualified references across merged namespaces/enum Within a single file, if multiple namespace blocks of the same namespace are present (for instance if the files have been manually joined together) symbols found in the current namespace, but previous blocks, must be qualified. container.Contains(location) incorrectly excluded references in other declaration blocks of the same namespace or enum. It is up to the caller to ensure files are joined together in the correct order. --- .../tstransforms/runtimesyntax.go | 15 +++++-- .../tstransforms/runtimesyntax_test.go | 41 ++++++++++++++++++- .../mergedNamespaceExportReference.js | 39 ++++++++++++++++++ .../mergedNamespaceExportReference.symbols | 34 +++++++++++++++ .../mergedNamespaceExportReference.types | 37 +++++++++++++++++ .../mergedNamespaceExportReference.ts | 17 ++++++++ 6 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 testdata/baselines/reference/compiler/mergedNamespaceExportReference.js create mode 100644 testdata/baselines/reference/compiler/mergedNamespaceExportReference.symbols create mode 100644 testdata/baselines/reference/compiler/mergedNamespaceExportReference.types create mode 100644 testdata/tests/cases/compiler/mergedNamespaceExportReference.ts diff --git a/internal/transformers/tstransforms/runtimesyntax.go b/internal/transformers/tstransforms/runtimesyntax.go index a7ed95f5200..3745c985882 100644 --- a/internal/transformers/tstransforms/runtimesyntax.go +++ b/internal/transformers/tstransforms/runtimesyntax.go @@ -1,7 +1,5 @@ package tstransforms -// !!! Unqualified enum member references across merged enum declarations are not currently supported (e.g `enum E {A}; enum E {B=A}`) -// !!! Unqualified namespace member references across merged namespace declarations are not currently supported (e.g `namespace N { export var x = 1; }; namespace N { x; }`). // !!! SourceMaps and Comments need to be validated import ( @@ -1048,7 +1046,18 @@ func (tx *RuntimeSyntaxTransformer) visitExpressionIdentifier(node *ast.Identifi tx.resolver = binder.NewReferenceResolver(tx.compilerOptions, binder.ReferenceResolverHooks{}) } container := tx.resolver.GetReferencedExportContainer(location, false /*prefixLocals*/) - if container != nil && (ast.IsEnumDeclaration(container) || ast.IsModuleDeclaration(container)) && container.Contains(location) { + // Get symbols from the original nodes (before transformation) since transformed nodes may not have symbols + var currentNamespaceSymbol *ast.Symbol + var currentEnumSymbol *ast.Symbol + if tx.currentNamespace != nil { + currentNamespaceSymbol = tx.EmitContext().MostOriginal(tx.currentNamespace).Symbol() + } + if tx.currentEnum != nil { + currentEnumSymbol = tx.EmitContext().MostOriginal(tx.currentEnum).Symbol() + } + if container != nil && + ((ast.IsModuleDeclaration(container) && currentNamespaceSymbol != nil && container.Symbol() == currentNamespaceSymbol) || + (ast.IsEnumDeclaration(container) && currentEnumSymbol != nil && container.Symbol() == currentEnumSymbol)) { containerName := tx.getNamespaceContainerName(container) memberName := node.Clone(tx.Factory()) diff --git a/internal/transformers/tstransforms/runtimesyntax_test.go b/internal/transformers/tstransforms/runtimesyntax_test.go index deb8704666f..7be92119e5b 100644 --- a/internal/transformers/tstransforms/runtimesyntax_test.go +++ b/internal/transformers/tstransforms/runtimesyntax_test.go @@ -207,7 +207,7 @@ var E; E[E["A"] = 0] = "A"; })(E || (E = {})); (function (E) { - E["B"] = A; + E["B"] = E.A; if (typeof E.B !== "string") E[E.B] = "B"; })(E || (E = {}));`}, @@ -268,7 +268,7 @@ func TestNamespaceTransformer(t *testing.T) { N.x = 1; })(N || (N = {})); (function (N) { - x; + N.x; })(N || (N = {}));`}, {title: "exported array binding pattern", input: "namespace N { export var [x] = [1]; }", output: `var N; @@ -342,6 +342,15 @@ func TestNamespaceTransformer(t *testing.T) { N.f = f; })(N || (N = {}));`}, + {title: "exported function call across namespaces", input: "namespace N { export function Foo() {} } namespace N { Foo(); }", output: `var N; +(function (N) { + function Foo() { } + N.Foo = Foo; +})(N || (N = {})); +(function (N) { + N.Foo(); +})(N || (N = {}));`}, + {title: "export class", input: "namespace N { export class C {} }", output: `var N; (function (N) { class C { @@ -349,6 +358,34 @@ func TestNamespaceTransformer(t *testing.T) { N.C = C; })(N || (N = {}));`}, + {title: "class extends across namespaces", input: "namespace A { export class TypeA {} } namespace A { export class TypeB extends TypeA {} }", output: `var A; +(function (A) { + class TypeA { + } + A.TypeA = TypeA; +})(A || (A = {})); +(function (A) { + class TypeB extends A.TypeA { + } + A.TypeB = TypeB; +})(A || (A = {}));`}, + + {title: "three namespace blocks with class inheritance", input: "namespace N { export class A {} } namespace N { export class B extends A {} } namespace N { class C extends B {} }", output: `var N; +(function (N) { + class A { + } + N.A = A; +})(N || (N = {})); +(function (N) { + class B extends N.A { + } + N.B = B; +})(N || (N = {})); +(function (N) { + class C extends N.B { + } +})(N || (N = {}));`}, + {title: "export enum", input: "namespace N { export enum E {A} }", output: `var N; (function (N) { let E; diff --git a/testdata/baselines/reference/compiler/mergedNamespaceExportReference.js b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.js new file mode 100644 index 00000000000..027b744d33f --- /dev/null +++ b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.js @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/mergedNamespaceExportReference.ts] //// + +//// [mergedNamespaceExportReference.ts] +// Test that references to exported namespace members across merged namespace +// declarations are correctly qualified in the emitted JavaScript. + +namespace N { + export function foo() { return 1; } + export var x = 1; + export class C {} +} + +namespace N { + // These should emit as N.foo(), N.x, and N.C + foo(); + x; + class D extends C {} +} + + +//// [mergedNamespaceExportReference.js] +// Test that references to exported namespace members across merged namespace +// declarations are correctly qualified in the emitted JavaScript. +var N; +(function (N) { + function foo() { return 1; } + N.foo = foo; + N.x = 1; + class C { + } + N.C = C; +})(N || (N = {})); +(function (N) { + // These should emit as N.foo(), N.x, and N.C + N.foo(); + N.x; + class D extends N.C { + } +})(N || (N = {})); diff --git a/testdata/baselines/reference/compiler/mergedNamespaceExportReference.symbols b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.symbols new file mode 100644 index 00000000000..9b8a604e0c3 --- /dev/null +++ b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.symbols @@ -0,0 +1,34 @@ +//// [tests/cases/compiler/mergedNamespaceExportReference.ts] //// + +=== mergedNamespaceExportReference.ts === +// Test that references to exported namespace members across merged namespace +// declarations are correctly qualified in the emitted JavaScript. + +namespace N { +>N : Symbol(N, Decl(mergedNamespaceExportReference.ts, 0, 0), Decl(mergedNamespaceExportReference.ts, 7, 1)) + + export function foo() { return 1; } +>foo : Symbol(foo, Decl(mergedNamespaceExportReference.ts, 3, 13)) + + export var x = 1; +>x : Symbol(x, Decl(mergedNamespaceExportReference.ts, 5, 14)) + + export class C {} +>C : Symbol(C, Decl(mergedNamespaceExportReference.ts, 5, 21)) +} + +namespace N { +>N : Symbol(N, Decl(mergedNamespaceExportReference.ts, 0, 0), Decl(mergedNamespaceExportReference.ts, 7, 1)) + + // These should emit as N.foo(), N.x, and N.C + foo(); +>foo : Symbol(foo, Decl(mergedNamespaceExportReference.ts, 3, 13)) + + x; +>x : Symbol(x, Decl(mergedNamespaceExportReference.ts, 5, 14)) + + class D extends C {} +>D : Symbol(D, Decl(mergedNamespaceExportReference.ts, 12, 6)) +>C : Symbol(C, Decl(mergedNamespaceExportReference.ts, 5, 21)) +} + diff --git a/testdata/baselines/reference/compiler/mergedNamespaceExportReference.types b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.types new file mode 100644 index 00000000000..9b61e84d9c0 --- /dev/null +++ b/testdata/baselines/reference/compiler/mergedNamespaceExportReference.types @@ -0,0 +1,37 @@ +//// [tests/cases/compiler/mergedNamespaceExportReference.ts] //// + +=== mergedNamespaceExportReference.ts === +// Test that references to exported namespace members across merged namespace +// declarations are correctly qualified in the emitted JavaScript. + +namespace N { +>N : typeof N + + export function foo() { return 1; } +>foo : () => number +>1 : 1 + + export var x = 1; +>x : number +>1 : 1 + + export class C {} +>C : C +} + +namespace N { +>N : typeof N + + // These should emit as N.foo(), N.x, and N.C + foo(); +>foo() : number +>foo : () => number + + x; +>x : number + + class D extends C {} +>D : D +>C : C +} + diff --git a/testdata/tests/cases/compiler/mergedNamespaceExportReference.ts b/testdata/tests/cases/compiler/mergedNamespaceExportReference.ts new file mode 100644 index 00000000000..51fe698b972 --- /dev/null +++ b/testdata/tests/cases/compiler/mergedNamespaceExportReference.ts @@ -0,0 +1,17 @@ +// @target: esnext + +// Test that references to exported namespace members across merged namespace +// declarations are correctly qualified in the emitted JavaScript. + +namespace N { + export function foo() { return 1; } + export var x = 1; + export class C {} +} + +namespace N { + // These should emit as N.foo(), N.x, and N.C + foo(); + x; + class D extends C {} +}