diff --git a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js index a8b65e98e13a..9d862a22aec7 100644 --- a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js +++ b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.1.normal.js @@ -1,41 +1,21 @@ //// [TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.ts] -import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check"; -(function(A) { - var Point = function Point() { - "use strict"; - _class_call_check(this, Point); - }; - A.Point = Point; -})(A || (A = {})); -(function(A) { - var Point = function Point() { - "use strict"; - _class_call_check(this, Point); - }; - // expected error - A.Point = Point; -})(A || (A = {})); -(function(X) { - (function(Y) { - (function(Z) { - var Line = function Line() { - "use strict"; - _class_call_check(this, Line); - }; - Z.Line = Line; - })(Y.Z || (Y.Z = {})); - })(X.Y || (X.Y = {})); -})(X || (X = {})); -(function(X) { - (function(Y) { - (function(Z) { - var Line = function Line() { - "use strict"; - _class_call_check(this, Line); - }; - // expected error - Z.Line = Line; - })(Y.Z || (Y.Z = {})); - })(X.Y || (X.Y = {})); -})(X || (X = {})); -var A, X; +//! x the name `Point` is defined multiple times +//! ,-[2:1] +//! 1 | module A { +//! 2 | export class Point { +//! : ^^|^^ +//! : `-- previous definition of `Point` here +//! 3 | x: number; +//! 4 | y: number; +//! 5 | } +//! 6 | } +//! 7 | +//! 8 | module A{ +//! 9 | // expected error +//! 10 | export class Point { +//! : ^^|^^ +//! : `-- `Point` redefined here +//! 11 | origin: number; +//! 12 | angle: number; +//! 13 | } +//! `---- diff --git a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js index 026505170e33..9d862a22aec7 100644 --- a/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js +++ b/crates/swc/tests/tsc-references/TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.2.minified.js @@ -1,12 +1,21 @@ //// [TwoInternalModulesThatMergeEachWithExportedClassesOfTheSameName.ts] -var A, X, X1, Y, X2, Y1; -import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check"; -(A || (A = {})).Point = function Point() { - _class_call_check(this, Point); -}, (A || (A = {})).Point = function Point() { - _class_call_check(this, Point); -}, ((Y = (X1 = X || (X = {})).Y || (X1.Y = {})).Z || (Y.Z = {})).Line = function Line() { - _class_call_check(this, Line); -}, ((Y1 = (X2 = X || (X = {})).Y || (X2.Y = {})).Z || (Y1.Z = {})).Line = function Line() { - _class_call_check(this, Line); -}; +//! x the name `Point` is defined multiple times +//! ,-[2:1] +//! 1 | module A { +//! 2 | export class Point { +//! : ^^|^^ +//! : `-- previous definition of `Point` here +//! 3 | x: number; +//! 4 | y: number; +//! 5 | } +//! 6 | } +//! 7 | +//! 8 | module A{ +//! 9 | // expected error +//! 10 | export class Point { +//! : ^^|^^ +//! : `-- `Point` redefined here +//! 11 | origin: number; +//! 12 | angle: number; +//! 13 | } +//! `---- diff --git a/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js b/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js index 5fc005f359f4..a46c920fe089 100644 --- a/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js +++ b/crates/swc/tests/tsc-references/enumMergingErrors.1.normal.js @@ -14,24 +14,24 @@ (function(M) { (function(E1) { E1[E1["B"] = 'foo'.length] = "B"; - })(M.E1 || (M.E1 = {})); + })(M.E1); (function(E2) { E2[E2["B"] = 'foo'.length] = "B"; - })(M.E2 || (M.E2 = {})); + })(M.E2); (function(E3) { E3[E3["C"] = 0] = "C"; - })(M.E3 || (M.E3 = {})); + })(M.E3); })(M || (M = {})); (function(M) { (function(E1) { E1[E1["C"] = 0] = "C"; - })(M.E1 || (M.E1 = {})); + })(M.E1); (function(E2) { E2[E2["A"] = 0] = "A"; - })(M.E2 || (M.E2 = {})); + })(M.E2); (function(E3) { E3[E3["B"] = 'foo'.length] = "B"; - })(M.E3 || (M.E3 = {})); + })(M.E3); })(M || (M = {})); // Enum with no initializer in either declaration with constant members with the same root module (function(M1) { @@ -42,12 +42,12 @@ (function(M1) { (function(E1) { E1[E1["B"] = 0] = "B"; - })(M1.E1 || (M1.E1 = {})); + })(M1.E1); })(M1 || (M1 = {})); (function(M1) { (function(E1) { E1[E1["C"] = 0] = "C"; - })(M1.E1 || (M1.E1 = {})); + })(M1.E1); })(M1 || (M1 = {})); // Enum with initializer in only one of three declarations with constant members with the same root module (function(M2) { @@ -58,11 +58,11 @@ (function(M2) { (function(E1) { E1[E1["B"] = 0] = "B"; - })(M2.E1 || (M2.E1 = {})); + })(M2.E1); })(M2 || (M2 = {})); (function(M2) { (function(E1) { E1[E1["C"] = 0] = "C"; - })(M2.E1 || (M2.E1 = {})); + })(M2.E1); })(M2 || (M2 = {})); var M, M1, M2; diff --git a/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js b/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js index 46ddc84c87fb..995ebdea3ae8 100644 --- a/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js +++ b/crates/swc/tests/tsc-references/enumMergingErrors.2.minified.js @@ -1,3 +1,3 @@ //// [enumMergingErrors.ts] -var M, M1, M2, M3, E1, E2, E3, M4, E11, E21, E31, M5, E12, E22, E32, M11, E13, M12, E14, M13, E15, M21, E16, M22, E17, M23, E18; -(E1 = (M3 = M || (M = {})).E1 || (M3.E1 = {}))[E1.A = 0] = "A", (E2 = M3.E2 || (M3.E2 = {}))[E2.C = 0] = "C", (E3 = M3.E3 || (M3.E3 = {}))[E3.A = 0] = "A", (E11 = (M4 = M || (M = {})).E1 || (M4.E1 = {}))[E11.B = 3] = "B", (E21 = M4.E2 || (M4.E2 = {}))[E21.B = 3] = "B", (E31 = M4.E3 || (M4.E3 = {}))[E31.C = 0] = "C", (E12 = (M5 = M || (M = {})).E1 || (M5.E1 = {}))[E12.C = 0] = "C", (E22 = M5.E2 || (M5.E2 = {}))[E22.A = 0] = "A", (E32 = M5.E3 || (M5.E3 = {}))[E32.B = 3] = "B", (E13 = (M11 = M1 || (M1 = {})).E1 || (M11.E1 = {}))[E13.A = 0] = "A", (E14 = (M12 = M1 || (M1 = {})).E1 || (M12.E1 = {}))[E14.B = 0] = "B", (E15 = (M13 = M1 || (M1 = {})).E1 || (M13.E1 = {}))[E15.C = 0] = "C", (E16 = (M21 = M2 || (M2 = {})).E1 || (M21.E1 = {}))[E16.A = 0] = "A", (E17 = (M22 = M2 || (M2 = {})).E1 || (M22.E1 = {}))[E17.B = 0] = "B", (E18 = (M23 = M2 || (M2 = {})).E1 || (M23.E1 = {}))[E18.C = 0] = "C"; +var M, M1, M2, M3, E1, E2, E3, M4, E11, E21, E31, M5, E12, E22, E32, M11, E13, E14, E15, M21, E16, E17, E18; +(E1 = (M3 = M || (M = {})).E1 || (M3.E1 = {}))[E1.A = 0] = "A", (E2 = M3.E2 || (M3.E2 = {}))[E2.C = 0] = "C", (E3 = M3.E3 || (M3.E3 = {}))[E3.A = 0] = "A", (E11 = (M4 = M || (M = {})).E1)[E11.B = 3] = "B", (E21 = M4.E2)[E21.B = 3] = "B", (E31 = M4.E3)[E31.C = 0] = "C", (E12 = (M5 = M || (M = {})).E1)[E12.C = 0] = "C", (E22 = M5.E2)[E22.A = 0] = "A", (E32 = M5.E3)[E32.B = 3] = "B", (E13 = (M11 = M1 || (M1 = {})).E1 || (M11.E1 = {}))[E13.A = 0] = "A", (E14 = (M1 || (M1 = {})).E1)[E14.B = 0] = "B", (E15 = (M1 || (M1 = {})).E1)[E15.C = 0] = "C", (E16 = (M21 = M2 || (M2 = {})).E1 || (M21.E1 = {}))[E16.A = 0] = "A", (E17 = (M2 || (M2 = {})).E1)[E17.B = 0] = "B", (E18 = (M2 || (M2 = {})).E1)[E18.C = 0] = "C"; diff --git a/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js b/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js index 4b4ad3fc8413..f50fc0b48d5b 100644 --- a/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js +++ b/crates/swc/tests/tsc-references/neverReturningFunctions1.1.normal.js @@ -35,6 +35,7 @@ function f13(x, fail) { fail(); x; // Unreachable } +(function(Debug) {})(Debug || (Debug = {})); function f21(x) { if (x === undefined) Debug.fail("undefined argument"); x.length; // string @@ -214,4 +215,5 @@ function foo(services, s) { return s; } } +var Debug; export { }; diff --git a/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js b/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js index 5261e8d42e1c..196fcb55d1ff 100644 --- a/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js +++ b/crates/swc/tests/tsc-references/neverReturningFunctions1.2.minified.js @@ -1,10 +1,11 @@ //// [neverReturningFunctions1.ts] +var Debug; import "@swc/helpers/_/_call_super"; import "@swc/helpers/_/_class_call_check"; import "@swc/helpers/_/_get"; import "@swc/helpers/_/_get_prototype_of"; import "@swc/helpers/_/_inherits"; -registerComponent('test-component', { +Debug || (Debug = {}), registerComponent('test-component', { schema: { myProperty: { default: [], diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js index eeaad3ee1594..af0b69361a93 100644 --- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js +++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.1.normal.js @@ -2,10 +2,10 @@ // module export (function(m) {})(m || (m = {})); (function(m) { - var z = x; + var z = m.x; var y = { - a: x, - x: x + a: m.x, + x: m.x }; })(m || (m = {})); var m; diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js index 1074b036fe54..7e8fdf1e5c7e 100644 --- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js +++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModule.2.minified.js @@ -1,3 +1,3 @@ //// [objectLiteralShorthandPropertiesWithModule.ts] -var m; -m || (m = {}), m || (m = {}), x, x, x; +var m, m1; +m || (m = {}), (m1 = m || (m = {})).x, m1.x, m1.x; diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js index 1c5649994451..e5cb573b0326 100644 --- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js +++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.1.normal.js @@ -1,10 +1,10 @@ //// [objectLiteralShorthandPropertiesWithModuleES6.ts] (function(m) {})(m || (m = {})); (function(m) { - var z = x; + var z = m.x; var y = { - a: x, - x + a: m.x, + x: m.x }; })(m || (m = {})); var m; diff --git a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js index e3252d596ba2..7c1736f374a1 100644 --- a/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js +++ b/crates/swc/tests/tsc-references/objectLiteralShorthandPropertiesWithModuleES6.2.minified.js @@ -1,3 +1,3 @@ //// [objectLiteralShorthandPropertiesWithModuleES6.ts] -var m; -m || (m = {}), m || (m = {}), x, x, x; +var m, m1; +m || (m = {}), (m1 = m || (m = {})).x, m1.x, m1.x; diff --git a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js index 116b738821e0..d8b1cbd9fca5 100644 --- a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js +++ b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.1.normal.js @@ -1 +1,3 @@ //// [parserFunctionDeclaration7.ts] +(function(M) {})(M || (M = {})); +var M; diff --git a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js index 116b738821e0..7945d5169d57 100644 --- a/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js +++ b/crates/swc/tests/tsc-references/parserFunctionDeclaration7.2.minified.js @@ -1 +1,3 @@ //// [parserFunctionDeclaration7.ts] +var M; +M || (M = {}); diff --git a/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js b/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js index 441653975504..12ea9c29c2ce 100644 --- a/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js +++ b/crates/swc/tests/tsc-references/tsxEmit3.1.normal.js @@ -1,38 +1,38 @@ //// [file.tsx] import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check"; (function(M) { - var Foo1 = function Foo1() { + var Foo = function Foo() { "use strict"; - _class_call_check(this, Foo1); + _class_call_check(this, Foo); }; - M.Foo = Foo1; - (function(S1) { - var Bar1 = function Bar1() { + M.Foo = Foo; + (function(S) { + var Bar = function Bar() { "use strict"; - _class_call_check(this, Bar1); + _class_call_check(this, Bar); }; - S1.Bar = Bar1; + S.Bar = Bar; // Emit Foo // Foo, ; })(M.S || (M.S = {})); })(M || (M = {})); (function(M) { // Emit M.Foo - Foo, /*#__PURE__*/ React.createElement(Foo, null); - (function(S1) { + M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null); + (function(S) { // Emit M.Foo - Foo, /*#__PURE__*/ React.createElement(Foo, null); + M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null); // Emit S.Bar - Bar, /*#__PURE__*/ React.createElement(Bar, null); + S.Bar, /*#__PURE__*/ React.createElement(S.Bar, null); })(M.S || (M.S = {})); })(M || (M = {})); (function(M) { // Emit M.S.Bar - S.Bar, /*#__PURE__*/ React.createElement(S.Bar, null); + M.S.Bar, /*#__PURE__*/ React.createElement(M.S.Bar, null); })(M || (M = {})); (function(M) { var M1 = 100; // Emit M_1.Foo - Foo, /*#__PURE__*/ React.createElement(Foo, null); + M.Foo, /*#__PURE__*/ React.createElement(M.Foo, null); })(M || (M = {})); var M; diff --git a/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js b/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js index b90f0911964c..772e8a8bf417 100644 --- a/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js +++ b/crates/swc/tests/tsc-references/tsxEmit3.2.minified.js @@ -1,8 +1,8 @@ //// [file.tsx] -var M, M1, M2; +var M, M1, M2, S, M3, M4; import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check"; -(M1 = M || (M = {})).Foo = function Foo1() { - _class_call_check(this, Foo1); -}, (M1.S || (M1.S = {})).Bar = function Bar1() { - _class_call_check(this, Bar1); -}, Foo, Foo, (M2 = M || (M = {})).S || (M2.S = {}), Foo, Foo, Bar, Bar, M || (M = {}), S.Bar, S.Bar, M || (M = {}), Foo, Foo; +(M1 = M || (M = {})).Foo = function Foo() { + _class_call_check(this, Foo); +}, (M1.S || (M1.S = {})).Bar = function Bar() { + _class_call_check(this, Bar); +}, (M2 = M || (M = {})).Foo, M2.Foo, S = M2.S || (M2.S = {}), M2.Foo, M2.Foo, S.Bar, S.Bar, (M3 = M || (M = {})).S.Bar, M3.S.Bar, (M4 = M || (M = {})).Foo, M4.Foo; diff --git a/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js b/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js index 502bf2cedba6..60ee915578b1 100644 --- a/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js +++ b/crates/swc/tests/tsc-references/tsxPreserveEmit1.1.normal.js @@ -23,7 +23,7 @@ define([ (function(M) {})(M || (M = {})); (function(M) { // Should emit 'M.X' in both opening and closing tags - var y = /*#__PURE__*/ React.createElement(X, null); + var y = /*#__PURE__*/ React.createElement(M.X, null); })(M || (M = {})); var M; }); diff --git a/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js b/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js index 1ba744cf168b..4232266e885f 100644 --- a/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js +++ b/crates/swc/tests/tsc-references/tsxPreserveEmit1.2.minified.js @@ -13,5 +13,5 @@ define([ "react-router" ], function(require, React, ReactRouter) { var M; - ReactRouter.Route, M || (M = {}), M || (M = {}), X; + ReactRouter.Route, M || (M = {}), (M || (M = {})).X; }); diff --git a/crates/swc_ecma_transforms_base/src/lib.rs b/crates/swc_ecma_transforms_base/src/lib.rs index 627a72304cdf..ecfc230ae5c2 100644 --- a/crates/swc_ecma_transforms_base/src/lib.rs +++ b/crates/swc_ecma_transforms_base/src/lib.rs @@ -2,7 +2,7 @@ #![allow(clippy::mutable_key_type)] #![cfg_attr(not(test), allow(unused))] -pub use self::resolver::resolver; +pub use self::resolver::{resolver, ts_module_is_instantiated}; #[doc(hidden)] pub mod ext; diff --git a/crates/swc_ecma_transforms_base/src/resolver/mod.rs b/crates/swc_ecma_transforms_base/src/resolver/mod.rs index f23df821f3c4..58f552504920 100644 --- a/crates/swc_ecma_transforms_base/src/resolver/mod.rs +++ b/crates/swc_ecma_transforms_base/src/resolver/mod.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use rustc_hash::{FxHashMap, FxHashSet}; use swc_atoms::Atom; use swc_common::{Mark, SyntaxContext}; @@ -10,6 +12,318 @@ use tracing::{debug, span, Level}; use crate::scope::{DeclKind, IdentType, ScopeKind}; +/// Merged visibility table for one TypeScript namespace *identity*, shared +/// across every re-open of that namespace regardless of syntactic form +/// (block body, dotted body, or `export namespace` inside another body). +/// Each entry stores the *declaring* re-open's binding [Mark], so a +/// reference from a sibling body resolves to the exact context the +/// declaration was rendered with. Non-exported declarations stay local to +/// each re-open's own body scope and never enter these maps. `id` is a +/// fresh, never-applied [Mark] used purely as a stable identity handle for +/// keying nested namespaces (never-applied marks intern no [SyntaxContext] +/// numbers, so allocating one is rendering-safe). Interior mutability lets +/// sibling re-opens accumulate into the same storage even though the scope +/// tree is stack-allocated. +#[derive(Debug)] +struct NamespaceMergedTable { + id: Mark, + symbols: FxHashMap, + types: FxHashMap, + /// Names whose current `symbols` entry was written by a namespace + /// declaration id ([Resolver::bind_namespace_id]). A later re-open's + /// namespace id may replace such an entry, but an entry claimed by a + /// real declaration (class, function, enum, ...) must not be + /// clobbered: TypeScript merges the namespace *into* that declaration, + /// so references bind to the declaration's mark. + namespace_owned_symbols: FxHashSet, + /// Type-space counterpart of `namespace_owned_symbols`. + namespace_owned_types: FxHashSet, +} + +type NamespaceMergedTableRef = Rc>; + +type NamespaceTableCache = Rc>>; + +/// Per-re-open-instance binding marks, keyed by `(enclosing scope instance +/// mark, namespace name)`. Two re-opens of one namespace nested under +/// *different* parent bodies get distinct binding marks (their exported +/// members render with per-body contexts), while re-opens under the same +/// enclosing instance share one. +type NamespaceInstanceCache = Rc>>; + +/// How an exported binding of the current namespace body picks its mark. +#[derive(Debug, Clone, Copy)] +enum NamespaceBindMark { + /// Block-form bodies bind exported members with the namespace + /// instance's mark. + Instance(Mark), + /// Dotted-form bodies resolve in the enclosing scope, so exported + /// members bind with the enclosing scope's own mark. + EnclosingScope, +} + +/// The merge target for exported bindings of the current namespace body: +/// the identity's merged table plus the mark to bind new exports with. +#[derive(Debug, Clone)] +struct NamespaceExportTarget { + table: NamespaceMergedTableRef, + bind_mark: NamespaceBindMark, +} + +/// Cache keys for one namespace declaration, derived identically by the +/// hoisting pre-pass and the phase-2 visitors (via +/// [Resolver::namespace_keys]) so the two can never diverge. +#[derive(Debug)] +struct NamespaceKeys { + /// Identity of the enclosing merge parent: the enclosing namespace's + /// table id when this namespace is exported from a namespace body, + /// otherwise the enclosing scope's own mark. Keys both the merged + /// table and the instance binding mark, so every re-open of one merged + /// namespace shares one instance mark: the TypeScript transform can + /// then qualify a cross-body member reference with the alias of the + /// body the reference appears in. Non-exported namespaces key by the + /// enclosing scope's own mark, which isolates them per body. + parent_identity: Mark, + name: Atom, +} + +/// Names that are exported by *this* re-open of a TS namespace, collected +/// in a single pre-pass over the body before hoisting. The hoister uses +/// these to route same-body re-declarations into the shared export scope +/// regardless of the order in which `export ...` and its peer declarations +/// appear (so `var a; export var a = 1;` merges the same way as +/// `export var a = 1; var a;`). This also means non-exported re-opens of +/// a sibling-exported name (e.g. `export enum E {}` in body 1 and +/// `enum E {}` in body 2) stay isolated, since pre-scan looks at the +/// *current* body only. +#[derive(Debug, Default)] +struct NamespaceExportNames { + values: FxHashMap, + /// Type-space exports of non-namespace declarations (interfaces, type + /// aliases, classes, enums, import aliases). Namespace declarations + /// are deliberately absent: their type-space visibility is registered + /// by [Resolver::bind_namespace_id] with a body-local mark. + types: FxHashSet, + /// Subset of `values` that are namespace declarations. Namespace ids + /// bind body-locally (via [Resolver::bind_namespace_id]) rather than + /// through the export routing in [Resolver::modify], and the hoisting + /// pre-pass must not seed them (their declaring mark does not exist + /// until the body is visited). + namespace_ids: FxHashSet, + /// Subset of `namespace_ids` whose name has no value meaning in this + /// body: every value-side occurrence of the name is a namespace + /// declaration that is not *instantiated* (transitively type-only, + /// per [ts_module_is_instantiated]). Such a name is fully erased and + /// must not claim a value-space slot in the merged table: a value + /// reference to the name resolves past it to an outer binding, as in + /// tsc. One value-producing declaration of the name keeps the value + /// meaning of the whole merged symbol, in either declaration order, + /// so a namespace that merges with a function (or an instantiated + /// re-declaration of itself) stays out of this set. Exportedness + /// judgement and type-space handling are unaffected. + erased_namespace_ids: FxHashSet, + /// Names with at least one value-producing declaration in this body: + /// a variable, function, class, enum, exported import alias, or an + /// instantiated namespace occurrence. Guards `erased_namespace_ids` + /// against a non-instantiated namespace occurrence erasing a name + /// that another occurrence gives value meaning. + value_producing_ids: FxHashSet, +} + +impl NamespaceExportNames { + /// Records a value-producing occurrence of `sym`: the merged name has + /// value meaning in this body, so the value seed must keep it even + /// when another occurrence is a non-instantiated namespace + /// declaration (before or after this one). + fn add_value_producing(&mut self, sym: Atom) { + self.erased_namespace_ids.remove(&sym); + self.value_producing_ids.insert(sym); + } + + /// Records a non-instantiated namespace occurrence of `sym`: erased + /// from the value seed unless another occurrence in this body + /// produces a value. + fn add_erased_namespace(&mut self, sym: Atom) { + if !self.value_producing_ids.contains(&sym) { + self.erased_namespace_ids.insert(sym); + } + } +} + +type NamespaceExportNamesRef = Rc; + +/// Collect the names that the given namespace body exports. Only the +/// top-level `export ...` declarations of the body are inspected; nested +/// scopes are handled by their own pre-scan when they are visited. +/// +/// `export import A = ...` (parsed as `TsImportEqualsDecl { is_export: true, +/// .. }`) is also recognised so that the alias name is routed to the merged +/// export scope alongside `export var`, `export class`, etc. Without this, +/// the alias would be treated as body-local and references from sibling +/// re-opens of the same namespace would fall through to outer/unresolved +/// bindings, violating TypeScript's namespace-merge semantics. +fn pre_scan_namespace_exports(body: &TsNamespaceBody) -> NamespaceExportNames { + let mut scan = NamespaceExportNames::default(); + if let TsNamespaceBody::TsModuleBlock(block) = body { + block.body.iter().for_each(|item| match item { + ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export)) => { + add_decl_export_names(&export.decl, &mut scan); + } + ModuleItem::ModuleDecl(ModuleDecl::TsImportEquals(import)) if import.is_export => { + // An exported import alias behaves like an exported namespace + // for merge purposes: visible in value-space (and in + // type-space when not a type-only alias) from sibling + // re-opens of the enclosing namespace. + if !import.is_type_only { + scan.values.insert(import.id.sym.clone(), DeclKind::Lexical); + scan.add_value_producing(import.id.sym.clone()); + } + scan.types.insert(import.id.sym.clone()); + } + _ => {} + }); + } else if let TsNamespaceBody::TsNamespaceDecl(inner) = body { + // `namespace A.B { ... }` means `namespace A { export namespace B + // { ... } }`, so the dotted child is an implicit export of `A`. + scan.values.insert(inner.id.sym.clone(), DeclKind::Lexical); + scan.namespace_ids.insert(inner.id.sym.clone()); + if ts_namespace_body_is_instantiated(&inner.body) { + scan.add_value_producing(inner.id.sym.clone()); + } else { + scan.add_erased_namespace(inner.id.sym.clone()); + } + } + scan +} + +/// Whether a namespace declaration is *instantiated* in the TypeScript +/// sense. A namespace whose members are (transitively) only interfaces, +/// type aliases, type-only import aliases, and other non-instantiated +/// namespaces is erased entirely and has no value meaning; TypeScript +/// resolves a value reference to its name past it, to an outer binding +/// (using the erased name itself as a value is a checker error). The +/// `retain` module in swc_ecma_transforms_typescript consumes this +/// predicate for its namespace emit decision, so the resolver and the +/// emitter classify namespaces identically by construction. +pub fn ts_module_is_instantiated(module: &TsModuleDecl) -> bool { + module + .body + .as_ref() + .map(ts_namespace_body_is_instantiated) + .unwrap_or_default() +} + +fn ts_namespace_body_is_instantiated(body: &TsNamespaceBody) -> bool { + body.as_ts_module_block() + .map(|block| block.body.iter().any(module_item_is_instantiating)) + .or_else(|| { + body.as_ts_namespace_decl() + .map(|inner| ts_namespace_body_is_instantiated(&inner.body)) + }) + .unwrap_or(true) +} + +fn module_item_is_instantiating(item: &ModuleItem) -> bool { + item.as_module_decl() + .map(module_decl_is_instantiating) + .or_else(|| item.as_stmt().map(stmt_is_instantiating)) + .unwrap_or(true) +} + +/// Type-only import/export forms and `export as namespace` are erased; +/// every other module-level form conservatively instantiates (most are +/// not valid inside a namespace body anyway). +fn module_decl_is_instantiating(module_decl: &ModuleDecl) -> bool { + module_decl + .as_export_decl() + .map(|export| decl_is_instantiating(&export.decl)) + .unwrap_or_else(|| { + let erased = module_decl + .as_ts_import_equals() + .map(|import| import.is_type_only) + .or_else(|| module_decl.as_import().map(|import| import.type_only)) + .or_else(|| module_decl.as_export_named().map(|export| export.type_only)) + .or_else(|| module_decl.as_export_all().map(|export| export.type_only)) + .unwrap_or_else(|| module_decl.is_ts_namespace_export()); + !erased + }) +} + +fn stmt_is_instantiating(stmt: &Stmt) -> bool { + stmt.as_decl() + .map(decl_is_instantiating) + .unwrap_or_else(|| !stmt.is_empty()) +} + +/// Interfaces, type aliases, and (transitively) type-only namespaces do +/// not instantiate; every other declaration does. A function declaration +/// instantiates even without a body (an ambient `declare function` member +/// or an overload signature): TypeScript's `getModuleInstanceState` maps +/// every function declaration to `Instantiated`, so the enclosing +/// namespace still gets its object even though the declaration itself is +/// erased from the emit. +fn decl_is_instantiating(decl: &Decl) -> bool { + let erased = decl.is_ts_interface() + || decl.is_ts_type_alias() + || decl + .as_ts_module() + .map(|module| !ts_module_is_instantiated(module)) + .unwrap_or_default(); + !erased +} + +fn add_decl_export_names(decl: &Decl, scan: &mut NamespaceExportNames) { + match decl { + Decl::Var(v) => { + let kind = DeclKind::from(v.kind); + v.decls.iter().for_each(|d| { + find_pat_ids::<_, Id>(&d.name) + .into_iter() + .for_each(|(sym, _)| { + scan.values.insert(sym.clone(), kind); + scan.add_value_producing(sym); + }); + }) + } + Decl::Fn(f) => { + scan.values.insert(f.ident.sym.clone(), DeclKind::Function); + scan.add_value_producing(f.ident.sym.clone()); + } + Decl::Class(c) => { + scan.values.insert(c.ident.sym.clone(), DeclKind::Lexical); + scan.types.insert(c.ident.sym.clone()); + scan.add_value_producing(c.ident.sym.clone()); + } + Decl::TsEnum(e) => { + scan.values.insert(e.id.sym.clone(), DeclKind::Lexical); + scan.types.insert(e.id.sym.clone()); + scan.add_value_producing(e.id.sym.clone()); + } + Decl::TsTypeAlias(a) => { + scan.types.insert(a.id.sym.clone()); + } + Decl::TsInterface(i) => { + scan.types.insert(i.id.sym.clone()); + } + Decl::TsModule(m) => { + if let TsModuleName::Ident(id) = &m.id { + scan.values + .entry(id.sym.clone()) + .or_insert(DeclKind::Lexical); + scan.namespace_ids.insert(id.sym.clone()); + if ts_module_is_instantiated(m) { + scan.add_value_producing(id.sym.clone()); + } else { + scan.add_erased_namespace(id.sym.clone()); + } + } + } + Decl::Using(_) => {} + #[cfg(swc_ast_unknown)] + _ => {} + } +} + #[cfg(test)] mod tests; @@ -145,6 +459,7 @@ pub fn resolver( current: Scope::new(ScopeKind::Fn, top_level_mark, None), ident_type: IdentType::Ref, in_type: false, + in_ts_qualifier: false, is_module: false, in_ts_module: false, decl_kind: DeclKind::Lexical, @@ -154,6 +469,11 @@ pub fn resolver( unresolved_mark, top_level_mark, }, + namespace_tables: Rc::new(RefCell::new(FxHashMap::default())), + namespace_instances: Rc::new(RefCell::new(FxHashMap::default())), + namespace_export: None, + namespace_identity: None, + namespace_export_names: None, }) } @@ -173,6 +493,17 @@ struct Scope<'a> { /// All types declared in the scope declared_types: FxHashSet, + + /// When this scope is the *body* of a TypeScript namespace, `shared` + /// holds the merged table(s) layered onto this scope: the enclosing + /// namespace's table, plus (while a dotted-form child body is being + /// visited) the child namespace's table. Reference lookups walking + /// the scope chain consult these tables innermost-first (reverse push + /// order) in addition to `self.declared_symbols` / `declared_types`, + /// so identifiers exported from another re-open of the same namespace + /// resolve here, while non-exported declarations in this body remain + /// private to this re-open. + shared: Vec, } impl<'a> Scope<'a> { @@ -183,12 +514,20 @@ impl<'a> Scope<'a> { mark, declared_symbols: Default::default(), declared_types: Default::default(), + shared: Default::default(), } } - fn is_declared(&self, symbol: &Atom) -> Option<&DeclKind> { + fn is_declared(&self, symbol: &Atom) -> Option { self.declared_symbols .get(symbol) + .copied() + .or_else(|| { + self.shared + .iter() + .rev() + .find_map(|table| table.borrow().symbols.get(symbol).map(|(kind, _)| *kind)) + }) .or_else(|| self.parent?.is_declared(symbol)) } } @@ -202,12 +541,46 @@ struct Resolver<'a> { current: Scope<'a>, ident_type: IdentType, in_type: bool, + in_ts_qualifier: bool, is_module: bool, in_ts_module: bool, decl_kind: DeclKind, strict_mode: bool, config: InnerConfig, + + /// Cache of merged namespace tables, keyed by `(parent identity mark, + /// namespace name)`. Shared across sibling scopes so that re-opened + /// TypeScript namespaces accumulate their exports into the same + /// [NamespaceMergedTable]. + namespace_tables: NamespaceTableCache, + + /// Cache of per-instance binding marks, keyed by `(enclosing scope + /// mark, namespace name)`. See [NamespaceInstanceCache]. + namespace_instances: NamespaceInstanceCache, + + /// When inside a TypeScript namespace body, the merge target of the + /// immediately enclosing namespace. `modify` registers bindings here + /// (in addition to applying the target's bind mark) for any name that + /// is part of [`Self::namespace_export_names`] (i.e. is exported by + /// the current body), so exported declarations land in the + /// namespace's merged table while non-exported re-opens stay on the + /// per-re-open body scope. + namespace_export: Option, + + /// The enclosing namespace's merged-table id, used as the + /// parent-identity half of the cache key when a nested exported + /// namespace is itself declared inside this body. + namespace_identity: Option, + + /// Set of names exported by the immediately enclosing namespace body, + /// computed once before hoisting starts. Used by `modify` to decide + /// whether a binding should be routed into the merged export scope. + /// Looking at the *current body's* exports (not the cumulative export + /// scope) is important: it lets same-body redeclarations such as + /// `export var a = 1; for (var a; …)` merge while keeping sibling + /// re-opens that don't export the name isolated from each other. + namespace_export_names: Option, } #[derive(Debug, Clone, Copy)] @@ -225,11 +598,17 @@ impl<'a> Resolver<'a> { current, ident_type: IdentType::Ref, in_type: false, + in_ts_qualifier: false, is_module: false, in_ts_module: false, config, decl_kind: DeclKind::Lexical, strict_mode: false, + namespace_tables: Rc::new(RefCell::new(FxHashMap::default())), + namespace_instances: Rc::new(RefCell::new(FxHashMap::default())), + namespace_export: None, + namespace_identity: None, + namespace_export_names: None, } } @@ -246,10 +625,26 @@ impl<'a> Resolver<'a> { ident_type: IdentType::Ref, config: self.config, in_type: self.in_type, + in_ts_qualifier: self.in_ts_qualifier, is_module: self.is_module, in_ts_module: self.in_ts_module, decl_kind: self.decl_kind, strict_mode: self.strict_mode, + namespace_tables: self.namespace_tables.clone(), + namespace_instances: self.namespace_instances.clone(), + // The enclosing namespace's merge target is still relevant + // inside any nested scope (a function body inside a namespace, + // for example, may itself declare further nested namespaces + // that need to merge across re-opens of the outer namespace). + namespace_export: self.namespace_export.clone(), + namespace_identity: self.namespace_identity, + // The pre-scanned export name set applies only to declarations + // that live *directly* in the enclosing namespace body. A + // nested block or function body inside the body is a fresh + // scope, so a `let x` or `var x` declared there must not be + // re-routed into the namespace's export scope even if `x` + // happens to be exported at the namespace level. + namespace_export_names: None, }; op(&mut child); @@ -272,16 +667,39 @@ impl<'a> Resolver<'a> { fn mark_for_ref_inner(&self, sym: &Atom, stop_an_fn_scope: bool) -> Option { if self.config.handle_types && self.in_type { - let mut mark = self.current.mark; let mut scope = Some(&self.current); while let Some(cur) = scope { // if cur.declared_types.contains(sym) || // cur.hoisted_symbols.borrow().contains(sym) { if cur.declared_types.contains(sym) { - if mark == Mark::root() { + if cur.mark == Mark::root() { break; } + return Some(cur.mark); + } + + // A namespace body's merged table(s) sit alongside the body + // in the chain; check them (innermost first) before walking + // to the enclosing scope so that an exported type declared + // by a sibling re-open of the same namespace resolves here, + // to the declaring body's own context. + // A slot owned purely by namespace declarations has + // namespace meaning but no type meaning of its own: a + // bare type reference skips it and keeps walking outward + // (tsc rejects a namespace-only symbol for type meaning, + // TS2709), while the left side of a qualified name + // resolves with namespace meaning and binds it. + if let Some(mark) = cur.shared.iter().rev().find_map(|table| { + let table = table.borrow(); + table + .types + .get(sym) + .filter(|_| { + self.in_ts_qualifier || !table.namespace_owned_types.contains(sym) + }) + .copied() + }) { return Some(mark); } @@ -289,19 +707,15 @@ impl<'a> Resolver<'a> { return None; } - if let Some(parent) = &cur.parent { - mark = parent.mark; - } scope = cur.parent; } } - let mut mark = self.current.mark; let mut scope = Some(&self.current); while let Some(cur) = scope { if cur.declared_symbols.contains_key(sym) { - if mark == Mark::root() { + if cur.mark == Mark::root() { return None; } @@ -309,21 +723,32 @@ impl<'a> Resolver<'a> { // https://tc39.es/ecma262/multipage/global-object.html#sec-value-properties-of-the-global-object-infinity // non configurable global value "undefined" | "NaN" | "Infinity" - if mark == self.config.top_level_mark && !self.is_module => + if cur.mark == self.config.top_level_mark && !self.is_module => { Some(self.config.unresolved_mark) } - _ => Some(mark), + _ => Some(cur.mark), }; } + // Mirror of the type-lookup branch above: an exported value + // declared in a sibling re-open of the same namespace lives in + // the merged table rather than this body's own + // `declared_symbols`, and resolves to the declaring re-open's + // stored mark. + if let Some(mark) = cur + .shared + .iter() + .rev() + .find_map(|table| table.borrow().symbols.get(sym).map(|(_, mark)| *mark)) + { + return Some(mark); + } + if cur.kind == ScopeKind::Fn && stop_an_fn_scope { return None; } - if let Some(parent) = &cur.parent { - mark = parent.mark; - } scope = cur.parent; } @@ -331,6 +756,21 @@ impl<'a> Resolver<'a> { } /// Modifies a binding identifier. + /// + /// In a TS namespace body, a binding is routed to the namespace's + /// merged export scope (`namespace_export`) when its name appears in + /// the pre-scanned [`Self::namespace_export_names`] set for the + /// current body. Otherwise the binding lands on `self.current` and + /// stays local to this re-open. + /// + /// The pre-scan looks only at the body's own `export` declarations, + /// so: + /// + /// - `export var a = 1; for (var a; …)` puts both `a`s on the export scope + /// (TypeScript's var-merge inside one body); and + /// - `namespace N { export enum E { … } }` followed by `namespace N { enum + /// E { … } }` keeps the second `E` private to the second re-open, since + /// the second body never exports `E`. fn modify(&mut self, id: &mut Ident, kind: DeclKind) { if cfg!(debug_assertions) && LOG { #[cfg(debug_assertions)] @@ -344,19 +784,255 @@ impl<'a> Resolver<'a> { return; } - if self.in_type { - self.current.declared_types.insert(id.sym.clone()); - } else { - self.current.declared_symbols.insert(id.sym.clone(), kind); - } + let route_to_export = self + .namespace_export_names + .as_ref() + .and_then(|names| { + let listed = if self.in_type { + names.types.contains(&id.sym) + } else { + // Namespace ids never reach `modify` (they bind + // body-locally via `bind_namespace_id`), so a value + // declaration that shares its name with an exported + // namespace (class/function/enum merging with the + // namespace) routes to the merged scope like any other + // export. + names.values.contains_key(&id.sym) + }; + listed.then_some(self.namespace_export.as_ref()) + }) + .flatten(); - let mark = self.current.mark; + let mark = match route_to_export { + Some(target) => { + let mark = match target.bind_mark { + NamespaceBindMark::Instance(instance) => instance, + NamespaceBindMark::EnclosingScope => self.current.mark, + }; + let mut table = target.table.borrow_mut(); + if self.in_type { + table.namespace_owned_types.remove(&id.sym); + table.types.insert(id.sym.clone(), mark); + } else { + table.namespace_owned_symbols.remove(&id.sym); + table.symbols.insert(id.sym.clone(), (kind, mark)); + } + mark + } + None => { + if self.in_type { + self.current.declared_types.insert(id.sym.clone()); + } else { + self.current.declared_symbols.insert(id.sym.clone(), kind); + } + self.current.mark + } + }; if mark != Mark::root() { id.ctxt = id.ctxt.apply_mark(mark); } } + /// Derives the cache keys for a namespace declaration named `name` + /// appearing directly in the current scope. Used identically by the + /// hoisting pre-pass and by the phase-2 visitors + /// ([`Self::visit_mut_ts_module_decl`] / + /// [`Self::visit_mut_ts_namespace_decl`]), so the two phases can never + /// derive diverging keys. + /// + /// Exportedness is judged on *value* exports only: a sibling + /// `export interface N` populates just the type set, and a type-only + /// export must not drag a non-exported namespace `N` into the merged + /// keying. + fn namespace_keys(&self, name: &Atom) -> NamespaceKeys { + let exported = self + .namespace_export_names + .as_ref() + .is_some_and(|names| names.values.contains_key(name)); + let parent_identity = if exported { + self.namespace_identity.unwrap_or(self.current.mark) + } else { + self.current.mark + }; + NamespaceKeys { + parent_identity, + name: name.clone(), + } + } + + /// Gets or creates the merged table for `keys`. The table's `id` is a + /// fresh [Mark] that is never applied to any syntax context; it serves + /// only as the stable parent-identity handle for namespaces nested + /// inside this one. + fn namespace_table(&self, keys: &NamespaceKeys) -> NamespaceMergedTableRef { + self.namespace_tables + .borrow_mut() + .entry((keys.parent_identity, keys.name.clone())) + .or_insert_with(|| { + Rc::new(RefCell::new(NamespaceMergedTable { + id: Mark::fresh(self.config.top_level_mark), + symbols: Default::default(), + types: Default::default(), + namespace_owned_symbols: Default::default(), + namespace_owned_types: Default::default(), + })) + }) + .clone() + } + + /// Gets or creates the instance binding mark for `keys`. One mark per + /// merged-namespace identity: every re-open of an exported namespace + /// binds its exported members with the same mark, which is what lets + /// the TypeScript transform qualify a cross-body member reference with + /// the current body's alias. The mark is applied only when a binding + /// is actually rendered, so creating it eagerly (during the hoisting + /// pre-pass) interns no syntax-context numbers. + fn namespace_instance_mark(&self, keys: &NamespaceKeys) -> Mark { + *self + .namespace_instances + .borrow_mut() + .entry((keys.parent_identity, keys.name.clone())) + .or_insert_with(|| Mark::fresh(self.config.top_level_mark)) + } + + /// Binds a namespace declaration's own id. + /// + /// Namespace ids always bind body-locally with the current scope's + /// mark (namespace *declarations* do not unify across re-opens; only + /// member references resolve through the merged table). When the + /// enclosing namespace body exports the name, the name is also + /// registered in the enclosing merged table (both spaces) so that + /// sibling re-opens referencing the namespace by name resolve it. The + /// value-space registration uses the enclosing target's bind mark (the + /// mark every exported value member carries), not the id's own + /// body-local mark: a cross-body value reference then reads as a member + /// of the enclosing namespace instance, which the TypeScript transform + /// qualifies with the alias of the body the reference appears in. The + /// type-space registration keeps the declaring body's mark. + /// Registration yields to a slot already claimed by a non-namespace + /// declaration (a class, function, enum, or interface the namespace + /// merges with): references then bind that declaration, per + /// TypeScript's declaration-merge semantics. + /// + /// `instantiated` reports whether this declaration's body is + /// instantiated (per [ts_module_is_instantiated]). A non-instantiated + /// declaration registers in type space only: the namespace is fully + /// erased, so its name has no value meaning and must not shadow an + /// outer value binding for sibling re-opens. (Another re-open of the + /// same name that does have value members claims the value slot via + /// its own registration or the pre-seed.) + fn bind_namespace_id(&mut self, id: &mut Ident, instantiated: bool) { + if id.ctxt == SyntaxContext::empty() { + self.current + .declared_symbols + .insert(id.sym.clone(), DeclKind::Lexical); + + let mark = self.current.mark; + if mark != Mark::root() { + id.ctxt = id.ctxt.apply_mark(mark); + } + + let exported = self.namespace_export_names.as_ref().is_some_and(|names| { + names.values.contains_key(&id.sym) || names.namespace_ids.contains(&id.sym) + }); + if exported { + self.namespace_export.iter().for_each(|target| { + let member_mark = match target.bind_mark { + NamespaceBindMark::Instance(instance) => instance, + NamespaceBindMark::EnclosingScope => mark, + }; + let mut table = target.table.borrow_mut(); + let owns_value = !table.symbols.contains_key(&id.sym) + || table.namespace_owned_symbols.contains(&id.sym); + if owns_value && instantiated { + table + .symbols + .insert(id.sym.clone(), (DeclKind::Lexical, member_mark)); + table.namespace_owned_symbols.insert(id.sym.clone()); + } + let owns_type = !table.types.contains_key(&id.sym) + || table.namespace_owned_types.contains(&id.sym); + if owns_type { + table.types.insert(id.sym.clone(), mark); + table.namespace_owned_types.insert(id.sym.clone()); + } + }); + } + } + } + + /// Pre-seeds the merged table for the namespace named `name` with the + /// exports of `body`, so that a reference in an *earlier* re-open can + /// resolve a name exported only by a *later* sibling re-open (forward + /// references across bodies). + /// + /// Runs from the hoisting pre-pass, which sweeps the enclosing + /// statement list before any namespace body is resolved. Seeding + /// inserts into the maps only and applies no marks, so syntax-context + /// interning order is untouched. Exported namespace ids seed the value + /// side like every other export, with the instance mark: an earlier + /// re-open can then resolve a namespace that only a later sibling + /// re-open declares, and the reference reads as a member of the + /// enclosing instance (matching the registration in + /// [`Self::bind_namespace_id`]). Non-instantiated namespace ids are + /// excluded from the value seed (see + /// [NamespaceExportNames::erased_namespace_ids]): an erased namespace + /// has no value meaning, so references resolve past it to an outer + /// binding. + /// + /// Namespace ids also seed the *type* side (separately from the + /// pre-scan's type set, which holds only non-namespace type + /// declarations): every namespace declaration has namespace meaning, + /// erased or not, so the left side of an earlier re-open's qualified + /// type reference must not fall through to an outer binding. (A + /// *bare* type reference is different: a namespace alone has no type + /// meaning, so that lookup skips slots that only a namespace owns; + /// see [`Self::mark_for_ref_inner`].) The type seed claims only a + /// vacant slot, with the instance mark, and records namespace + /// ownership so that the declaring body's own registration overwrites + /// it with the body mark (see [`Self::bind_namespace_id`]). A slot + /// already claimed by a non-namespace type declaration is left alone, + /// and a non-namespace type declaration seen by any re-open revokes + /// namespace ownership of the slot, in either seeding order: + /// TypeScript merges the namespace into that declaration, so the + /// namespace registration must leave the merged slot in place and + /// every reference then agrees on the instance mark. Dotted bodies' + /// inner members are not seeded; forward references across sibling + /// re-opens therefore reach one nesting level deep. + fn seed_namespace_exports(&self, name: &Atom, body: &TsNamespaceBody) { + let keys = self.namespace_keys(name); + let table = self.namespace_table(&keys); + let instance_mark = self.namespace_instance_mark(&keys); + + let scan = pre_scan_namespace_exports(body); + let mut table = table.borrow_mut(); + let NamespaceMergedTable { + symbols, + types, + namespace_owned_types, + .. + } = &mut *table; + scan.values + .iter() + .filter(|(sym, _)| !scan.erased_namespace_ids.contains(*sym)) + .for_each(|(sym, kind)| { + symbols + .entry(sym.clone()) + .or_insert((*kind, instance_mark)); + }); + scan.types.iter().for_each(|sym| { + namespace_owned_types.remove(sym); + types.entry(sym.clone()).or_insert(instance_mark); + }); + scan.namespace_ids.iter().for_each(|sym| { + types.entry(sym.clone()).or_insert_with(|| { + namespace_owned_types.insert(sym.clone()); + instance_mark + }); + }); + } + fn mark_block(&mut self, ctxt: &mut SyntaxContext) { if *ctxt != SyntaxContext::empty() { return; @@ -369,6 +1045,39 @@ impl<'a> Resolver<'a> { } } + /// Records a TypeScript type binding in the merged export scope when + /// the name is exported by the current namespace body (per the + /// pre-scanned [`Self::namespace_export_names`]). Mirrors the + /// [`Self::modify`] routing for callers that only need the side-table + /// insert without applying a [`Mark`] to an identifier (the class + /// hoister, in particular). + fn declare_type_in_current(&mut self, name: Atom) { + let route_to_export = self + .namespace_export_names + .as_ref() + .and_then(|names| { + names + .types + .contains(&name) + .then_some(self.namespace_export.as_ref()) + }) + .flatten(); + match route_to_export { + Some(target) => { + let mark = match target.bind_mark { + NamespaceBindMark::Instance(instance) => instance, + NamespaceBindMark::EnclosingScope => self.current.mark, + }; + let mut table = target.table.borrow_mut(); + table.namespace_owned_types.remove(&name); + table.types.insert(name, mark); + } + None => { + self.current.declared_types.insert(name); + } + } + } + fn try_resolving_as_type(&mut self, i: &mut Ident) { if i.ctxt.outer() == self.config.unresolved_mark { i.ctxt = SyntaxContext::empty() @@ -1346,7 +2055,15 @@ impl VisitMut for Resolver<'_> { } fn visit_mut_ts_import_equals_decl(&mut self, n: &mut TsImportEqualsDecl) { + // A type-only alias (`import type A = ...`) introduces a + // type-space binding only: it must not create a value-space + // binding, and an exported one has to register in the enclosing + // namespace's merged *type* scope so that sibling re-opens + // resolve it. + let old_in_type = self.in_type; + self.in_type = n.is_type_only; self.modify(&mut n.id, DeclKind::Lexical); + self.in_type = old_in_type; n.module_ref.visit_mut_with(self); } @@ -1423,18 +2140,74 @@ impl VisitMut for Resolver<'_> { return; } - match &mut decl.id { + let instantiated = ts_module_is_instantiated(decl); + let namespace_name = match &mut decl.id { TsModuleName::Ident(i) => { - self.modify(i, DeclKind::Lexical); + // Marks the id and registers it in the enclosing merged + // table when exported. Usually a no-op here: the hoisting + // pre-pass has already bound the id. + self.bind_namespace_id(i, instantiated); + Some(i.sym.clone()) } - TsModuleName::Str(_) => {} + TsModuleName::Str(_) => None, #[cfg(swc_ast_unknown)] - _ => {} - } + _ => None, + }; + + // Key derivation (see [Self::namespace_keys]): + // + // * Top-level namespaces key their table on the enclosing scope's stable mark, + // so sibling re-opens collide on the same entry and merge. + // + // * Nested namespaces *exported* from the enclosing body key on the enclosing + // namespace's table id, so every re-open reaches the same table regardless of + // which outer re-open (or syntactic form) declares it. + // + // * Nested namespaces *not exported* from the enclosing body key on the + // per-re-open body mark, so they land in disjoint entries and stay isolated, + // matching TypeScript's rule that non-exported members are local to each + // declaration body. + // + // The per-instance binding mark is keyed on the enclosing scope + // instance, so re-opens under one parent body share a binding mark + // while re-opens under distinct parent bodies render with distinct + // contexts (their references still unify through the table). + let merge = namespace_name.map(|name| { + let keys = self.namespace_keys(&name); + ( + self.namespace_table(&keys), + self.namespace_instance_mark(&keys), + ) + }); + + // Pre-scan this body's exports. Doing it once up front makes the + // routing decision in `modify` order-independent and avoids + // leaking exports across re-opens of the same namespace. + let export_names: Option = decl + .body + .as_ref() + .map(|body| Rc::new(pre_scan_namespace_exports(body))); self.with_child(ScopeKind::Block, |child| { child.in_ts_module = true; + // Attach the merged table so that lookups for sibling-declared + // exports succeed, and route the body's own exported + // declarations into the same shared storage. A + // `TsModuleName::Str` body has no merge identity: it neither + // reads from nor registers into any enclosing table. + child.namespace_export = None; + child.namespace_identity = None; + merge.into_iter().for_each(|(table, instance_mark)| { + child.current.shared.push(table.clone()); + child.namespace_identity = Some(table.borrow().id); + child.namespace_export = Some(NamespaceExportTarget { + table, + bind_mark: NamespaceBindMark::Instance(instance_mark), + }); + }); + child.namespace_export_names = export_names; + decl.body.visit_mut_children_with(child); }); } @@ -1444,9 +2217,36 @@ impl VisitMut for Resolver<'_> { return; } - self.modify(&mut n.id, DeclKind::Lexical); + // A dotted body (`namespace A.B { ... }`) is sugar for an exported + // nested namespace, but it resolves *in the enclosing scope*: the + // id and every exported member bind with the enclosing body's own + // mark ([NamespaceBindMark::EnclosingScope]). The child's merged + // table is layered onto the current scope for the duration of the + // body so that references resolve through the merge, then popped. + let instantiated = ts_namespace_body_is_instantiated(&n.body); + self.bind_namespace_id(&mut n.id, instantiated); + + let keys = self.namespace_keys(&n.id.sym); + let table = self.namespace_table(&keys); + let table_id = table.borrow().id; + + let body_names: Option = + Some(Rc::new(pre_scan_namespace_exports(&n.body))); + + self.current.shared.push(table.clone()); + let saved_export = self.namespace_export.replace(NamespaceExportTarget { + table, + bind_mark: NamespaceBindMark::EnclosingScope, + }); + let saved_identity = self.namespace_identity.replace(table_id); + let saved_names = std::mem::replace(&mut self.namespace_export_names, body_names); n.body.visit_mut_with(self); + + self.namespace_export_names = saved_names; + self.namespace_identity = saved_identity; + self.namespace_export = saved_export; + self.current.shared.pop(); } fn visit_mut_ts_param_prop_param(&mut self, n: &mut TsParamPropParam) { @@ -1473,7 +2273,14 @@ impl VisitMut for Resolver<'_> { fn visit_mut_ts_qualified_name(&mut self, n: &mut TsQualifiedName) { self.ident_type = IdentType::Ref; - n.left.visit_mut_with(self) + // The left side resolves with namespace meaning: merged-table + // type slots that only a namespace declaration owns are visible + // here, unlike for a bare type reference (see + // `mark_for_ref_inner`). + let in_ts_qualifier = self.in_ts_qualifier; + self.in_ts_qualifier = true; + n.left.visit_mut_with(self); + self.in_ts_qualifier = in_ts_qualifier; } fn visit_mut_ts_satisfies_expr(&mut self, n: &mut TsSatisfiesExpr) { @@ -1722,9 +2529,7 @@ impl VisitMut for Hoister<'_, '_> { if self.resolver.config.handle_types { self.resolver - .current - .declared_types - .insert(node.ident.sym.clone()); + .declare_type_in_current(node.ident.sym.clone()); } } @@ -1772,11 +2577,17 @@ impl VisitMut for Hoister<'_, '_> { }, ) && !self.in_block => { - let old_in_type = self.resolver.in_type; - self.resolver.in_type = false; - let id = v.id.as_mut_ident().unwrap(); - self.resolver.modify(id, DeclKind::Lexical); - self.resolver.in_type = old_in_type; + // Bind the namespace id body-locally, then pre-seed the + // merged table with the body's exports so that an + // earlier sibling re-open can forward-reference them. + let instantiated = ts_module_is_instantiated(v); + if let TsModuleName::Ident(id) = &mut v.id { + self.resolver.bind_namespace_id(id, instantiated); + let name = id.sym.clone(); + v.body.iter().for_each(|body| { + self.resolver.seed_namespace_exports(&name, body); + }); + } } _ => {} } diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts index 80dabef10c24..604391263aa2 100644 --- a/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/internalModules/codeGeneration/importStatements/output.ts @@ -21,8 +21,8 @@ namespace D__2 { var p__7 = new a__7.Point(1, 1); } namespace E__2 { - import a__8 = A__2; - export function xDist__8(x__9: a__8.Point) { - return a__8.Origin.x - x__9.x; + import a__9 = A__2; + export function xDist__8(x__10: a__9.Point) { + return a__9.Origin.x - x__10.x; } } diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts new file mode 100644 index 000000000000..bf46cd6f04ab --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/input.ts @@ -0,0 +1,9 @@ +namespace Test { + export const a = 1; +} + +const a = "out"; + +namespace Test { + export const b = a + 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts new file mode 100644 index 000000000000..a971ad7aa3aa --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen/output.ts @@ -0,0 +1,7 @@ +namespace Test__2 { + export const a__3 = 1; +} +const a__2 = "out"; +namespace Test__2 { + export const b__3 = a__3 + 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts new file mode 100644 index 000000000000..46d43d147890 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/input.ts @@ -0,0 +1,18 @@ +var C = "outer"; +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export declare function f(): void; + } +} +var D = "outer"; +namespace M { + export const y = D; +} +namespace M { + export declare namespace D { + function g(): void; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts new file mode 100644 index 000000000000..535a565b909b --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ambient_fn_instantiates/output.ts @@ -0,0 +1,18 @@ +var C__2 = "outer"; +namespace N__2 { + export const x__3 = C__3; +} +namespace N__2 { + export namespace C__4 { + export declare function f__5(): void; + } +} +var D__2 = "outer"; +namespace M__2 { + export const y__7 = D__7; +} +namespace M__2 { + export declare namespace D__8 { + function g__9(): void; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts new file mode 100644 index 000000000000..b273d3f06a85 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/input.ts @@ -0,0 +1,9 @@ +type C = string; +namespace N { + export let v: C; + export type W = C2.Q; +} +namespace N { + export namespace C { export const q = 1; } + export namespace C2 { export type Q = number; } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts new file mode 100644 index 000000000000..2881e5c83031 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_bare_type_ref_outer/output.ts @@ -0,0 +1,13 @@ +type C__2 = string; +namespace N__2 { + export let v__3: C__2; + export type W__3 = C2__3.Q; +} +namespace N__2 { + export namespace C__4 { + export const q__5 = 1; + } + export namespace C2__4 { + export type Q__6 = number; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts new file mode 100644 index 000000000000..2d3c6fb567dc --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/input.ts @@ -0,0 +1,14 @@ +namespace N { + export class C {} +} + +namespace N { + export namespace C { + export const x = 1; + } +} + +namespace N { + export const y = new C(); + export let v: C; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts new file mode 100644 index 000000000000..8940442bf77d --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_class_namespace_merge/output.ts @@ -0,0 +1,13 @@ +namespace N__2 { + export class C__3 { + } +} +namespace N__2 { + export namespace C__4 { + export const x__5 = 1; + } +} +namespace N__2 { + export const y__3 = new C__3(); + export let v__3: C__3; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts new file mode 100644 index 000000000000..3784a495d3a6 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/input.ts @@ -0,0 +1,15 @@ +namespace Outer.Inner { + export class C {} + export function f() {} +} + +namespace Outer.Inner { + export const c = new C(); + export const g = f(); +} + +namespace Outer { + export namespace Inner { + export const a = c; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts new file mode 100644 index 000000000000..44e1d3bfe0ba --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_dotted_exports/output.ts @@ -0,0 +1,14 @@ +namespace Outer__2.Inner__3 { + export class C__3 { + } + export function f__3() {} +} +namespace Outer__2.Inner__5 { + export const c__5 = new C__3(); + export const g__5 = f__3(); +} +namespace Outer__2 { + export namespace Inner__6 { + export const a__7 = c__5; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts new file mode 100644 index 000000000000..e490096758f5 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/input.ts @@ -0,0 +1,16 @@ +var C = "outer"; +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export interface I {} + } +} +var D = "outer"; +namespace M { + export namespace D {} +} +namespace M { + export const y = D; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts new file mode 100644 index 000000000000..3442aab2b553 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_erased_namespace_no_shadow/output.ts @@ -0,0 +1,18 @@ +var C__2 = "outer"; +namespace N__2 { + export const x__3 = C__2; +} +namespace N__2 { + export namespace C__4 { + export interface I__5 { + } + } +} +var D__2 = "outer"; +namespace M__2 { + export namespace D__6 { + } +} +namespace M__2 { + export const y__7 = D__2; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts new file mode 100644 index 000000000000..8023828b316a --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/input.ts @@ -0,0 +1,11 @@ +namespace Outer { + export const x = 1; +} + +namespace N { + export import A = Outer; +} + +namespace N { + export const b = A.x + 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts new file mode 100644 index 000000000000..0aa2ca29fd2c --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_export_import/output.ts @@ -0,0 +1,9 @@ +namespace Outer__2 { + export const x__3 = 1; +} +namespace N__2 { + export import A__4 = Outer__2; +} +namespace N__2 { + export const b__4 = A__4.x + 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts new file mode 100644 index 000000000000..6a252bcd2e16 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/input.ts @@ -0,0 +1,18 @@ +namespace N { + export const x = foo; +} +namespace N { + export function foo() { return 1; } + export namespace foo { export interface Options {} } +} +namespace M { + export const before = B; +} +namespace M { + export namespace B { + export interface I {} + } + export namespace B { + export const c = 1; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts new file mode 100644 index 000000000000..0ad938dddb9d --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_fn_ns_value_merge/output.ts @@ -0,0 +1,24 @@ +namespace N__2 { + export const x__3 = foo__3; +} +namespace N__2 { + export function foo__3() { + return 1; + } + export namespace foo__4 { + export interface Options__6 { + } + } +} +namespace M__2 { + export const before__7 = B__7; +} +namespace M__2 { + export namespace B__8 { + export interface I__9 { + } + } + export namespace B__8 { + export const c__9 = 1; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts new file mode 100644 index 000000000000..df0ec0dc99a5 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/input.ts @@ -0,0 +1,29 @@ +namespace COut { + export interface I {} +} +import C = COut; +namespace N { + export type T = C.I; +} +namespace N { + export namespace C { + export interface I {} + } +} +namespace M { + export type U = E.J; +} +namespace M { + export namespace E { + export interface J {} + export const v = 1; + } +} +namespace P { + export namespace F { + export interface K {} + } +} +namespace P { + export type W = F.K; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts new file mode 100644 index 000000000000..7e97e2a57a7d --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_forward_type_ref/output.ts @@ -0,0 +1,33 @@ +namespace COut__2 { + export interface I__3 { + } +} +import C__2 = COut__2; +namespace N__2 { + export type T__4 = C__4.I; +} +namespace N__2 { + export namespace C__5 { + export interface I__6 { + } + } +} +namespace M__2 { + export type U__7 = E__7.J; +} +namespace M__2 { + export namespace E__8 { + export interface J__9 { + } + export const v__9 = 1; + } +} +namespace P__2 { + export namespace F__10 { + export interface K__11 { + } + } +} +namespace P__2 { + export type W__12 = F__10.K; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts new file mode 100644 index 000000000000..7bea148f0a65 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/input.ts @@ -0,0 +1,10 @@ +namespace Outer { + export type U = Inner; +} + +namespace Outer { + export interface Inner {} + export namespace Inner { + export const a = 1; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts new file mode 100644 index 000000000000..11c518445e5e --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_interface_namespace_forward/output.ts @@ -0,0 +1,10 @@ +namespace Outer__2 { + export type U__3 = Inner__3; +} +namespace Outer__2 { + export interface Inner__3 { + } + export namespace Inner__4 { + export const a__5 = 1; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts new file mode 100644 index 000000000000..132a58e76ac4 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/input.ts @@ -0,0 +1,7 @@ +namespace Test { + export var b = a + 1; +} + +namespace Test { + export var a = 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts new file mode 100644 index 000000000000..7e62c02f4946 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_export/output.ts @@ -0,0 +1,6 @@ +namespace Test__2 { + export var b__3 = a__3 + 1; +} +namespace Test__2 { + export var a__3 = 1; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts new file mode 100644 index 000000000000..c0fe0745237c --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/input.ts @@ -0,0 +1,9 @@ +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export const a = 1; + } +} +const C = "outer"; diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts new file mode 100644 index 000000000000..6bef540bead9 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_late_namespace_export/output.ts @@ -0,0 +1,9 @@ +namespace N__2 { + export const x__3 = C__3; +} +namespace N__2 { + export namespace C__4 { + export const a__5 = 1; + } +} +const C__2 = "outer"; diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts new file mode 100644 index 000000000000..b6c231f2fa22 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/input.ts @@ -0,0 +1,11 @@ +namespace Outer { + namespace Inner { + export const a = 1; + } +} + +namespace Outer { + namespace Inner { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts new file mode 100644 index 000000000000..d8d6d51a013e --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_private/output.ts @@ -0,0 +1,10 @@ +namespace Outer__2 { + namespace Inner__3 { + export const a__4 = 1; + } +} +namespace Outer__2 { + namespace Inner__5 { + export const b__6 = a; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts new file mode 100644 index 000000000000..08f34286047d --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/input.ts @@ -0,0 +1,10 @@ +namespace N { + export namespace I { + export const a = 1; + } +} +namespace N { + export namespace I { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts new file mode 100644 index 000000000000..d57630a36472 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_reopen/output.ts @@ -0,0 +1,10 @@ +namespace N__2 { + export namespace I__3 { + export const a__4 = 1; + } +} +namespace N__2 { + export namespace I__5 { + export const b__4 = a__4; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts new file mode 100644 index 000000000000..445cf45b6030 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/input.ts @@ -0,0 +1,13 @@ +namespace Outer { + export interface Inner {} + export namespace Inner { + export const a = 1; + } +} + +namespace Outer { + export interface Inner {} + export namespace Inner { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts new file mode 100644 index 000000000000..6c80f6869e06 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_nested_type_only_export/output.ts @@ -0,0 +1,14 @@ +namespace Outer__2 { + export interface Inner__3 { + } + export namespace Inner__4 { + export const a__5 = 1; + } +} +namespace Outer__2 { + export interface Inner__3 { + } + export namespace Inner__6 { + export const b__5 = a__5; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts new file mode 100644 index 000000000000..b23f39c883e6 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/input.ts @@ -0,0 +1,11 @@ +namespace A { + export namespace B { + export const c = 1; + export type T = number; + } + export let v: B; + export let w: B.T; +} +namespace A { + export interface B { z: number } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts new file mode 100644 index 000000000000..2776f59d8533 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_ns_iface_type_merge/output.ts @@ -0,0 +1,13 @@ +namespace A__2 { + export namespace B__3 { + export const c__5 = 1; + export type T__5 = number; + } + export let v__4: B__4; + export let w__4: B__4.T; +} +namespace A__2 { + export interface B__4 { + z__0: number; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts new file mode 100644 index 000000000000..83adec678206 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/input.ts @@ -0,0 +1,13 @@ +namespace Foo { + export var a; + var b; +} + +var a, b, c; + +namespace Foo { + var b; + foo(a); + foo(b); + foo(c); +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts new file mode 100644 index 000000000000..0e5c17e6b023 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private/output.ts @@ -0,0 +1,11 @@ +namespace Foo__2 { + export var a__3; + var b__4; +} +var a__2, b__2, c__2; +namespace Foo__2 { + var b__5; + foo(a__3); + foo(b__5); + foo(c__2); +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts new file mode 100644 index 000000000000..602f7eb3f9a1 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/input.ts @@ -0,0 +1,15 @@ +namespace O { + export const o = 1; +} +namespace Outer { + export import Inner = O; + namespace Inner { + export const a = 1; + } +} +const a = 2; +namespace Outer { + namespace Inner { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts new file mode 100644 index 000000000000..8250b3bc303e --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_private_export_import_shadow/output.ts @@ -0,0 +1,15 @@ +namespace O__2 { + export const o__3 = 1; +} +namespace Outer__2 { + export import Inner__5 = O__2; + namespace Inner__4 { + export const a__6 = 1; + } +} +const a__2 = 2; +namespace Outer__2 { + namespace Inner__7 { + export const b__8 = a__2; + } +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts new file mode 100644 index 000000000000..25a246e4fb3a --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/input.ts @@ -0,0 +1,11 @@ +namespace Outer { + export interface I {} +} + +namespace N { + export import type A = Outer; +} + +namespace N { + export type T = A.I; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts new file mode 100644 index 000000000000..4da2c1d433b4 --- /dev/null +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/namespace_reopen_type_only_export_import/output.ts @@ -0,0 +1,10 @@ +namespace Outer__2 { + export interface I__3 { + } +} +namespace N__2 { + export import type A__4 = Outer__2; +} +namespace N__2 { + export type T__4 = A__4.I; +} diff --git a/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts b/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts index 93b4d279f20b..a611ff52892d 100644 --- a/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts +++ b/crates/swc_ecma_transforms_base/tests/ts-resolver/type-alias/hoisting/1/output.ts @@ -3,9 +3,9 @@ namespace Foo__2 { export class IncomingMessage__3 extends http.IncomingMessage { originalUrl?: http.IncomingMessage["url"] | undefined; } - type NextFunction__3 = (err?: any) => void; + type NextFunction__4 = (err?: any) => void; export type SimpleHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse) => void; - export type NextHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__3) => void; - export type ErrorHandleFunction__3 = (err: any, req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__3) => void; + export type NextHandleFunction__3 = (req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__4) => void; + export type ErrorHandleFunction__3 = (err: any, req: IncomingMessage__3, res: http.ServerResponse, next: NextFunction__4) => void; export type HandleFunction__3 = SimpleHandleFunction__3 | NextHandleFunction__3 | ErrorHandleFunction__3; } diff --git a/crates/swc_ecma_transforms_typescript/src/retain.rs b/crates/swc_ecma_transforms_typescript/src/retain.rs index c9277ff2941e..a6d52f2baf26 100644 --- a/crates/swc_ecma_transforms_typescript/src/retain.rs +++ b/crates/swc_ecma_transforms_typescript/src/retain.rs @@ -1,4 +1,5 @@ use swc_ecma_ast::*; +use swc_ecma_transforms_base::ts_module_is_instantiated; /// Returns true if a module item should survive TS type-stripping. pub(crate) fn should_retain_module_item(module_item: &ModuleItem, in_namespace: bool) -> bool { @@ -37,25 +38,15 @@ pub(crate) trait IsConcrete { fn is_concrete(&self) -> bool; } +/// Delegates to the resolver's instantiation predicate so the emitter and +/// the resolver classify namespaces identically. A namespace whose only +/// members are bodyless function declarations (ambient `declare function` +/// members or overload signatures) is instantiated: TypeScript still +/// creates the namespace object for it, even though the function +/// declarations themselves are erased. impl IsConcrete for TsModuleDecl { fn is_concrete(&self) -> bool { - self.body - .as_ref() - .map(|body| body.is_concrete()) - .unwrap_or_default() - } -} - -impl IsConcrete for TsNamespaceBody { - fn is_concrete(&self) -> bool { - match self { - Self::TsModuleBlock(ts_module_block) => { - ts_module_block.body.iter().any(|item| item.is_concrete()) - } - Self::TsNamespaceDecl(ts_namespace_decl) => ts_namespace_decl.body.is_concrete(), - #[cfg(swc_ast_unknown)] - _ => panic!("unable to access unknown nodes"), - } + ts_module_is_instantiated(self) } } diff --git a/crates/swc_ecma_transforms_typescript/src/semantic.rs b/crates/swc_ecma_transforms_typescript/src/semantic.rs index 8d0e138c74ae..5279ca261f41 100644 --- a/crates/swc_ecma_transforms_typescript/src/semantic.rs +++ b/crates/swc_ecma_transforms_typescript/src/semantic.rs @@ -1,4 +1,5 @@ use rustc_hash::{FxHashMap, FxHashSet}; +use swc_atoms::Atom; use swc_common::{Mark, Span, SyntaxContext}; use swc_ecma_ast::*; use swc_ecma_utils::{find_pat_ids, stack_size::maybe_grow_default}; @@ -16,6 +17,24 @@ pub(crate) struct SemanticInfo { pub id_type: FxHashSet, pub id_value: FxHashSet, pub exported_binding: FxHashMap>, + /// One shared [`SyntaxContext`] per namespace declaration id. + /// + /// The resolver binds every exported value member of a merged namespace + /// (across all re-opens) with one shared instance mark, so every routed + /// member of one namespace carries the same context. The transform uses + /// this to recognize refs to members of the namespace instance currently + /// being emitted, including forward refs to members declared by a later + /// re-open. + pub member_ctxt: FxHashMap, + /// Per namespace body (keyed by the declaration's span, which is + /// unique per re-open even when re-opened declarations share an id): + /// the exported member names whose declarations stay local bindings in + /// that body's emit (an exported function or class keeps + /// `function f() {}` / `class C {}` plus a trailing `N.f = f` + /// assignment). A same-body reference to such a member must stay bare + /// (the local binding is the one in scope, and function references + /// rely on hoisting), while a cross-body reference must be qualified. + pub local_member: FxHashMap>, pub enum_record: TsEnumRecord, pub const_enum: FxHashSet, pub namespace_import_equals_usage: FxHashSet, @@ -58,6 +77,8 @@ pub(crate) fn analyze_program( import_chain: Default::default(), namespace_block_stack: Default::default(), namespace_id: None, + namespace_span: None, + namespace_parent: Default::default(), skip_transform_info: false, flow_syntax, }; @@ -73,6 +94,12 @@ struct SemanticAnalyzer { import_chain: FxHashMap, namespace_block_stack: Vec, namespace_id: Option, + /// Span of the namespace declaration whose body is being visited; keys + /// [SemanticInfo::local_member] per body. + namespace_span: Option, + /// Exported nested namespace declaration id -> enclosing namespace + /// body id; drives [Self::propagate_member_ctxt]. + namespace_parent: FxHashMap, skip_transform_info: bool, flow_syntax: bool, } @@ -118,6 +145,7 @@ impl NamespaceBlock { impl SemanticAnalyzer { fn finish(mut self) -> SemanticInfo { self.analyze_import_chain(); + self.propagate_member_ctxt(); self.info } @@ -143,6 +171,70 @@ impl SemanticAnalyzer { self.info.usage.extend(new_usage); } + /// Record the shared member context of the current namespace. All routed + /// members of one namespace carry the same context, so the first insert + /// wins. + fn record_member_ctxt(&mut self, ctxt: SyntaxContext) { + if let Some(namespace_id) = self.namespace_id.clone() { + self.info.member_ctxt.entry(namespace_id).or_insert(ctxt); + } + } + + /// Record an exported member of the current namespace body whose + /// declaration keeps a local binding in the emitted body. + fn record_local_member(&mut self, sym: Atom) { + if let Some(namespace_span) = self.namespace_span { + self.info + .local_member + .entry(namespace_span) + .or_default() + .insert(sym); + } + } + + /// The canonical grouping key of a namespace body id: the chain of + /// exported-namespace names from the outermost registered ancestor. + /// Re-opened bodies of one merged namespace map to one key: top-level + /// (and same-parent private) re-opens share their declaration id, and + /// exported nested re-opens under different parent bodies share the + /// ancestor chain. + fn canonical_namespace_key(&self, id: &Id) -> (Id, Vec) { + self.namespace_parent + .get(id) + .map(|parent| { + let (root, mut path) = self.canonical_namespace_key(parent); + path.push(id.0.clone()); + (root, path) + }) + .unwrap_or_else(|| (id.clone(), Vec::new())) + } + + /// Share the recorded member context across re-opened bodies of one + /// merged namespace. A body whose exports are all namespaces records + /// no member context itself, and exported nested re-opens under + /// different parent bodies have distinct declaration ids: group the + /// bodies by canonical key, then fill each unrecorded body from its + /// group, so every body arms the cross-body reference rewrite. + fn propagate_member_ctxt(&mut self) { + let group_ctxt: FxHashMap<(Id, Vec), SyntaxContext> = self + .info + .member_ctxt + .iter() + .map(|(id, ctxt)| (self.canonical_namespace_key(id), *ctxt)) + .collect(); + let filled: Vec<(Id, SyntaxContext)> = self + .namespace_parent + .keys() + .filter(|id| !self.info.member_ctxt.contains_key(*id)) + .filter_map(|id| { + group_ctxt + .get(&self.canonical_namespace_key(id)) + .map(|ctxt| (id.clone(), *ctxt)) + }) + .collect(); + self.info.member_ctxt.extend(filled); + } + fn collect_top_level_module_item(&mut self, item: &ModuleItem) { match item { ModuleItem::Stmt(Stmt::Decl(decl)) => self.collect_decl(decl), @@ -362,6 +454,9 @@ impl Visit for SemanticAnalyzer { self.info .exported_binding .insert(node.id.to_id(), self.namespace_id.clone()); + if !node.is_type_only { + self.record_member_ctxt(node.id.ctxt); + } } if node.is_type_only { @@ -404,6 +499,7 @@ impl Visit for SemanticAnalyzer { match &node.decl { Decl::Var(var_decl) => { let ids: Vec = find_pat_ids(&var_decl.decls); + ids.iter().for_each(|id| self.record_member_ctxt(id.1)); self.info.exported_binding.extend( ids.into_iter() .zip(std::iter::repeat(self.namespace_id.clone())), @@ -413,12 +509,31 @@ impl Visit for SemanticAnalyzer { self.info .exported_binding .insert(ts_enum_decl.id.to_id(), self.namespace_id.clone()); + self.record_member_ctxt(ts_enum_decl.id.ctxt); + } + // Exported functions and classes are deliberately absent from + // `exported_binding` (their declarations keep local bindings in + // the emitted body), but they carry the shared instance context + // like every routed value member: record it so bodies whose + // exports are only functions/classes still arm the + // cross-body-reference rewrite, and record the name as locally + // bound so same-body references stay bare. + Decl::Fn(fn_decl) => { + self.record_member_ctxt(fn_decl.ident.ctxt); + self.record_local_member(fn_decl.ident.sym.clone()); + } + Decl::Class(class_decl) => { + self.record_member_ctxt(class_decl.ident.ctxt); + self.record_local_member(class_decl.ident.sym.clone()); } Decl::TsModule(ts_module_decl) => { if let TsModuleName::Ident(ident) = &ts_module_decl.id { self.info .exported_binding .insert(ident.to_id(), self.namespace_id.clone()); + if let Some(parent) = self.namespace_id.clone() { + self.namespace_parent.insert(ident.to_id(), parent); + } } } _ => {} @@ -473,10 +588,12 @@ impl Visit for SemanticAnalyzer { } let namespace_id = self.namespace_id.replace(node.id.to_id()); + let namespace_span = self.namespace_span.replace(node.span); node.body.visit_with(self); self.namespace_id = namespace_id; + self.namespace_span = namespace_span; } fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) { @@ -499,9 +616,11 @@ impl Visit for SemanticAnalyzer { }; let namespace_id = self.namespace_id.replace(id); + let namespace_span = self.namespace_span.replace(node.span); body.visit_with(self); self.namespace_id = namespace_id; + self.namespace_span = namespace_span; } fn visit_ts_module_block(&mut self, node: &TsModuleBlock) { diff --git a/crates/swc_ecma_transforms_typescript/src/transform.rs b/crates/swc_ecma_transforms_typescript/src/transform.rs index 0f530f21b19b..f1ed1c56bf43 100644 --- a/crates/swc_ecma_transforms_typescript/src/transform.rs +++ b/crates/swc_ecma_transforms_typescript/src/transform.rs @@ -120,10 +120,11 @@ impl VisitMut for Transform { ); fn visit_mut_program(&mut self, node: &mut Program) { - if !self.semantic.exported_binding.is_empty() { + if !self.semantic.exported_binding.is_empty() || !self.semantic.member_ctxt.is_empty() { self.ref_rewriter = Some(RefRewriter { query: ExportQuery { export_name: self.semantic.exported_binding.clone(), + namespace_stack: Vec::new(), }, }); } @@ -326,21 +327,25 @@ impl VisitMut for Transform { fn visit_mut_ts_namespace_decl(&mut self, node: &mut TsNamespaceDecl) { let id = node.id.to_id(); + let pushed = self.enter_current_namespace(&id, node.span); let namespace_id = self.namespace_id.replace(id); node.body.visit_mut_with(self); self.namespace_id = namespace_id; + self.exit_current_namespace(pushed); } fn visit_mut_ts_module_decl(&mut self, node: &mut TsModuleDecl) { let id = node.id.to_id(); + let pushed = self.enter_current_namespace(&id, node.span); let namespace_id = self.namespace_id.replace(id); node.body.visit_mut_with(self); self.namespace_id = namespace_id; + self.exit_current_namespace(pushed); } fn visit_mut_stmt(&mut self, node: &mut Stmt) { @@ -680,6 +685,49 @@ enum FoldedDecl { } impl Transform { + /// Push the namespace body being entered onto the export query's + /// namespace stack: the body's alias id, the shared member context the + /// analyzer recorded for it, and the body's locally-bound member names + /// (keyed by the declaration's span, which is unique per re-open). + /// Returns whether a frame was pushed. Nothing is pushed when the body + /// has no routed value members, when no rewriter is active, or when the + /// recorded context equals the namespace id's own binding context: that + /// happens for dotted bodies (`namespace A.B`), whose exported members + /// bind with the enclosing scope's mark, so context equality would match + /// every body-local binding rather than just members. + fn enter_current_namespace(&mut self, id: &Id, span: Span) -> bool { + let frame = self + .semantic + .member_ctxt + .get(id) + .filter(|ctxt| **ctxt != id.1) + .map(|ctxt| NamespaceFrame { + alias: id.clone(), + member_ctxt: *ctxt, + local_members: self + .semantic + .local_member + .get(&span) + .cloned() + .unwrap_or_default(), + }); + + self.ref_rewriter + .as_mut() + .zip(frame) + .map(|(ref_rewriter, frame)| ref_rewriter.query.namespace_stack.push(frame)) + .is_some() + } + + /// Pop the frame pushed by [`Self::enter_current_namespace`], if any. + fn exit_current_namespace(&mut self, pushed: bool) { + let _ = self + .ref_rewriter + .as_mut() + .filter(|_| pushed) + .and_then(|ref_rewriter| ref_rewriter.query.namespace_stack.pop()); + } + fn normalize_flow_static_constructor_key( &self, key: &mut PropName, @@ -1868,16 +1916,56 @@ impl Transform { } } +/// One enclosing namespace body during emit: the body's alias id, the +/// shared member [`SyntaxContext`] of the namespace instance, and the +/// member names whose declarations stay local bindings in this body's +/// emit. +struct NamespaceFrame { + alias: Id, + member_ctxt: SyntaxContext, + local_members: FxHashSet, +} + struct ExportQuery { export_name: FxHashMap>, + /// The namespace bodies currently being emitted, innermost last. A ref + /// whose context equals a frame's member context is a member of that + /// namespace instance, even when its symbol is absent from + /// `export_name` (a forward ref to a member declared by a later + /// re-open). + namespace_stack: Vec, +} + +impl ExportQuery { + /// The alias to qualify `ident` with when it refers to a member of an + /// enclosing namespace instance currently being emitted. Detected by + /// context equality: every routed member of one namespace instance + /// carries one shared context, so refs to members seeded by another + /// re-open body match without an `export_name` entry. A member whose + /// declaration keeps a local binding in the matched body (an exported + /// function or class) stays unqualified: the local binding is in scope + /// at the reference site (from nested namespace bodies too), and + /// function references rely on hoisting. + fn query_current_member(&self, ident: &Ident) -> Option { + self.namespace_stack + .iter() + .rev() + .find(|frame| frame.member_ctxt == ident.ctxt) + .filter(|frame| !frame.local_members.contains(&ident.sym)) + .map(|frame| frame.alias.clone()) + } } impl QueryRef for ExportQuery { fn query_ref(&self, export_name: &Ident) -> Option> { - self.export_name - .get(&export_name.to_id())? - .clone() - .map(|namespace_id| namespace_id.make_member(export_name.clone().into()).into()) + self.query_current_member(export_name) + .map(|alias| alias.make_member(export_name.clone().into()).into()) + .or_else(|| { + self.export_name + .get(&export_name.to_id())? + .clone() + .map(|namespace_id| namespace_id.make_member(export_name.clone().into()).into()) + }) } fn query_lhs(&self, ident: &Ident) -> Option> { @@ -1885,17 +1973,28 @@ impl QueryRef for ExportQuery { } fn query_jsx(&self, ident: &Ident) -> Option { - self.export_name - .get(&ident.to_id())? - .clone() - .map(|namespace_id| { + self.query_current_member(ident) + .map(|alias| { JSXMemberExpr { span: DUMMY_SP, - obj: JSXObject::Ident(namespace_id.into()), + obj: JSXObject::Ident(alias.into()), prop: ident.clone().into(), } .into() }) + .or_else(|| { + self.export_name + .get(&ident.to_id())? + .clone() + .map(|namespace_id| { + JSXMemberExpr { + span: DUMMY_SP, + obj: JSXObject::Ident(namespace_id.into()), + prop: ident.clone().into(), + } + .into() + }) + }) } } diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js index bf2ef8ece779..cb829dc2cf42 100644 --- a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/namespace_004.js @@ -11,7 +11,7 @@ (function(MyNamespace) { (function(MyEnum) { MyEnum[MyEnum["B"] = 1] = "B"; - })(MyNamespace.MyEnum || (MyNamespace.MyEnum = {})); + })(MyNamespace.MyEnum); (function(MyInnerNamespace) { MyInnerNamespace.Dec2 = 2; })(MyNamespace.MyInnerNamespace || (MyNamespace.MyInnerNamespace = {})); diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js index 5b4f907dfc7c..d42cde3c20fe 100644 --- a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-and-enum/output.js @@ -30,7 +30,7 @@ var E1 = /*#__PURE__*/ function(E1) { (function(N) { (function(E) { E[E["c"] = 3] = "c"; - })(N.E || (N.E = {})); + })(N.E); (function(E) { E[E["d"] = 4] = "d"; })(N.E); @@ -42,7 +42,7 @@ var E1 = /*#__PURE__*/ function(E1) { (function(N1) { (function(E2) { E2[E2["f"] = 6] = "f"; - })(N1.E2 || (N1.E2 = {})); + })(N1.E2); })(N.N1 || (N.N1 = {})); (function(N2) { let E3 = /*#__PURE__*/ function(E3) { @@ -60,6 +60,6 @@ var E1 = /*#__PURE__*/ function(E1) { (function(N1) { (function(E2) { E2[E2["f"] = 6] = "f"; - })(N1.E2 || (N1.E2 = {})); + })(N1.E2); })(N1 || (N1 = {})); export var N, N1; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts new file mode 100644 index 000000000000..d0448ac50af2 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/input.ts @@ -0,0 +1,16 @@ +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export declare function f(): void; + } +} +namespace M { + export const y = D; +} +namespace M { + export namespace D { + declare function g(): void; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js new file mode 100644 index 000000000000..91d85b5fae4b --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/ambient-fn-namespace/output.js @@ -0,0 +1,13 @@ +(function(N) { + N.x = N.C; +})(N || (N = {})); +(function(N) { + (function(C) {})(N.C || (N.C = {})); +})(N || (N = {})); +(function(M) { + M.y = M.D; +})(M || (M = {})); +(function(M) { + (function(D) {})(M.D || (M.D = {})); +})(M || (M = {})); +var N, M; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts new file mode 100644 index 000000000000..cb43b775c697 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/input.ts @@ -0,0 +1,8 @@ +namespace N { + export const x = C; +} +namespace N { + export declare namespace C { + function f(): void; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js new file mode 100644 index 000000000000..2643bca108f2 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/declare-namespace-member/output.js @@ -0,0 +1,5 @@ +(function(N) { + N.x = N.C; +})(N || (N = {})); +(function(N) {})(N || (N = {})); +var N; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts new file mode 100644 index 000000000000..8d9b4add418c --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/input.ts @@ -0,0 +1,10 @@ +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export namespace D { + export declare function f(): void; + } + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js new file mode 100644 index 000000000000..966ea6a1f725 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/deep-ambient-fn/output.js @@ -0,0 +1,9 @@ +(function(N) { + N.x = N.C; +})(N || (N = {})); +(function(N) { + (function(C) { + (function(D) {})(C.D || (C.D = {})); + })(N.C || (N.C = {})); +})(N || (N = {})); +var N; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts new file mode 100644 index 000000000000..a2246d52c94d --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/input.ts @@ -0,0 +1,5 @@ +namespace A.B { + const t = 1; + export const x = t; + export const y = B.x; +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js new file mode 100644 index 000000000000..76ea06b7719e --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/dotted-body-locals/output.js @@ -0,0 +1,8 @@ +(function(A) { + (function(B) { + const t = 1; + B.x = t; + B.y = B.x; + })(A.B || (A.B = {})); +})(A || (A = {})); +var A; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts new file mode 100644 index 000000000000..b53bda30226d --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/input.ts @@ -0,0 +1,15 @@ +namespace T { + export function f() { + return 10; + } +} +namespace T { + export function g() { + return f(); + } + export class K { + v = 2; + } + export const k = new K().v; + export const h = g() * k; +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js new file mode 100644 index 000000000000..ca7f593c3bbd --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-members/output.js @@ -0,0 +1,21 @@ +(function(T) { + function f() { + return 10; + } + T.f = f; +})(T || (T = {})); +(function(T) { + function g() { + return T.f(); + } + T.g = g; + class K { + constructor(){ + this.v = 2; + } + } + T.K = K; + T.k = new K().v; + T.h = g() * T.k; +})(T || (T = {})); +var T; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts new file mode 100644 index 000000000000..2f219f2ec662 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/input.ts @@ -0,0 +1,10 @@ +namespace U { + export class C { + w = 3; + } +} +namespace U { + export function make() { + return new C().w; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js new file mode 100644 index 000000000000..f17f7242651e --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-class-only-bodies/output.js @@ -0,0 +1,15 @@ +(function(U) { + class C { + constructor(){ + this.w = 3; + } + } + U.C = C; +})(U || (U = {})); +(function(U) { + function make() { + return new U.C().w; + } + U.make = make; +})(U || (U = {})); +var U; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts new file mode 100644 index 000000000000..6a252bcd2e16 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/input.ts @@ -0,0 +1,18 @@ +namespace N { + export const x = foo; +} +namespace N { + export function foo() { return 1; } + export namespace foo { export interface Options {} } +} +namespace M { + export const before = B; +} +namespace M { + export namespace B { + export interface I {} + } + export namespace B { + export const c = 1; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js new file mode 100644 index 000000000000..1522f1b8f24f --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/fn-ns-value-merge/output.js @@ -0,0 +1,18 @@ +(function(N) { + N.x = N.foo; +})(N || (N = {})); +(function(N) { + function foo() { + return 1; + } + N.foo = foo; +})(N || (N = {})); +(function(M) { + M.before = M.B; +})(M || (M = {})); +(function(M) { + (function(B) { + B.c = 1; + })(M.B || (M.B = {})); +})(M || (M = {})); +var N, M; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts new file mode 100644 index 000000000000..541d838409e0 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/input.ts @@ -0,0 +1,12 @@ +namespace N { + export const x = C; +} +namespace N { + export namespace C { + export const a = 1; + } +} +namespace N { + export const y = C; +} +const C = "outer"; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js new file mode 100644 index 000000000000..cbb89b8fe9dc --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/late-namespace-export/output.js @@ -0,0 +1,13 @@ +(function(N) { + N.x = N.C; +})(N || (N = {})); +(function(N) { + (function(C) { + C.a = 1; + })(N.C || (N.C = {})); +})(N || (N = {})); +(function(N) { + N.y = N.C; +})(N || (N = {})); +const C = "outer"; +var N; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts new file mode 100644 index 000000000000..faadcdfb3222 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/input.ts @@ -0,0 +1,12 @@ +namespace P { + export namespace N { + export const a = 1; + } +} +namespace P { + export namespace N { + export namespace M { + export const c = a; + } + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js new file mode 100644 index 000000000000..22e0923566f8 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-chain/output.js @@ -0,0 +1,13 @@ +(function(P) { + (function(N) { + N.a = 1; + })(P.N || (P.N = {})); +})(P || (P = {})); +(function(P) { + (function(N) { + (function(M) { + M.c = N.a; + })(N.M || (N.M = {})); + })(P.N || (P.N = {})); +})(P || (P = {})); +var P; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts new file mode 100644 index 000000000000..894539f4bcb8 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/input.ts @@ -0,0 +1,8 @@ +namespace T { + export function f() { + return 5; + } + export namespace Sub { + export const v = f(); + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js new file mode 100644 index 000000000000..255de7180d0b --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-fn-ref/output.js @@ -0,0 +1,10 @@ +(function(T) { + function f() { + return 5; + } + T.f = f; + (function(Sub) { + Sub.v = f(); + })(T.Sub || (T.Sub = {})); +})(T || (T = {})); +var T; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts new file mode 100644 index 000000000000..08f34286047d --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/input.ts @@ -0,0 +1,10 @@ +namespace N { + export namespace I { + export const a = 1; + } +} +namespace N { + export namespace I { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js new file mode 100644 index 000000000000..36dfa725f919 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/nested-reopen/output.js @@ -0,0 +1,11 @@ +(function(N) { + (function(I) { + I.a = 1; + })(N.I || (N.I = {})); +})(N || (N = {})); +(function(N) { + (function(I) { + I.b = I.a; + })(N.I || (N.I = {})); +})(N || (N = {})); +var N; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts new file mode 100644 index 000000000000..602f7eb3f9a1 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/input.ts @@ -0,0 +1,15 @@ +namespace O { + export const o = 1; +} +namespace Outer { + export import Inner = O; + namespace Inner { + export const a = 1; + } +} +const a = 2; +namespace Outer { + namespace Inner { + export const b = a; + } +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js new file mode 100644 index 000000000000..d0b8fe5775c8 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/private-alias-shadow/output.js @@ -0,0 +1,18 @@ +(function(O) { + O.o = 1; +})(O || (O = {})); +(function(Outer) { + Outer.Inner = O; + (function(Inner) { + Inner.a = 1; + })(Inner || (Inner = {})); + var Inner; +})(Outer || (Outer = {})); +const a = 2; +(function(Outer) { + (function(Inner) { + Inner.b = a; + })(Inner || (Inner = {})); + var Inner; +})(Outer || (Outer = {})); +var O, Outer; diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts new file mode 100644 index 000000000000..fa2ce674ae52 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/input.ts @@ -0,0 +1,16 @@ +var C = "outer"; +namespace N { + export namespace C { + export interface I {} + } +} +namespace N { + export const x = C; +} +var D = "outer"; +namespace M { + export const y = D; +} +namespace M { + export namespace D {} +} diff --git a/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js new file mode 100644 index 000000000000..5b07a8b6e2fb --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/fixture/namespace-reopen/type-only-namespace-no-shadow/output.js @@ -0,0 +1,9 @@ +var C = "outer"; +(function(N) { + N.x = C; +})(N || (N = {})); +var D = "outer"; +(function(M) { + M.y = D; +})(M || (M = {})); +var N, M;