diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index d981eb71862d..31928258caee 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -694,6 +694,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { /// In addition, there is a map from each relevant symbol reference to the data /// associated with that namespace or namespace member: "ref_to_ts_namespace_member". /// This gives enough info to be able to resolve queries into the namespace. + /// Never `Property`: storing it, as esbuild does, replaces the record of a merged namespace. pub(crate) ref_to_ts_namespace_member: HashMap, /// Arena-allocated `TSNamespaceScope`s; their global-heap maps are freed in `Drop`. pub(crate) ts_namespace_scopes: Vec>, @@ -4618,8 +4619,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data: js_ast::ts::Data::Property, }, )?; - self.ref_to_ts_namespace_member - .insert(id.r#ref, js_ast::ts::Data::Property); } js_ast::b::B::BObject(obj) => { for prop in obj.properties.slice() { diff --git a/src/js_parser/parse/parse_typescript.rs b/src/js_parser/parse/parse_typescript.rs index eb4d49f92c69..a21859fbbae1 100644 --- a/src/js_parser/parse/parse_typescript.rs +++ b/src/js_parser/parse/parse_typescript.rs @@ -18,19 +18,6 @@ use bun_ast::{ }; use bun_core::strings; -// `ts::Data` carries only Copy payloads but lacks a `derive(Clone)` upstream; -// local helper so we can re-insert values fetched from `ref_to_ts_namespace_member`. -#[inline] -fn clone_ts_member_data(d: &TSNamespaceMemberData) -> TSNamespaceMemberData { - match d { - TSNamespaceMemberData::Property => TSNamespaceMemberData::Property, - TSNamespaceMemberData::Namespace(m) => TSNamespaceMemberData::Namespace(*m), - TSNamespaceMemberData::EnumNumber(n) => TSNamespaceMemberData::EnumNumber(*n), - TSNamespaceMemberData::EnumString(s) => TSNamespaceMemberData::EnumString(*s), - TSNamespaceMemberData::EnumProperty => TSNamespaceMemberData::EnumProperty, - } -} - impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> { pub(crate) fn parse_type_script_decorators(&mut self) -> Result { let p = self; @@ -295,8 +282,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data: TSNamespaceMemberData::Property, }, )?; - p.ref_to_ts_namespace_member - .insert(ref_, TSNamespaceMemberData::Property); } } StmtData::SClass(class) => { @@ -313,45 +298,39 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data: TSNamespaceMemberData::Property, }, )?; - p.ref_to_ts_namespace_member - .insert(ref_, TSNamespaceMemberData::Property); } } StmtData::SNamespace(ns) => { if ns.is_export { let ref_ = ns.name.ref_; - if let Some(member_data) = p.ref_to_ts_namespace_member.get(&ref_) { - let member_data = clone_ts_member_data(member_data); + if let Some(&data) = p.ref_to_ts_namespace_member.get(&ref_) { // SAFETY: original_name is arena-owned, valid for 'a. let ns_name: &[u8] = p.symbols[ref_.inner_index() as usize].original_name.slice(); exported_members.put( ns_name, TSNamespaceMember { - data: clone_ts_member_data(&member_data), + data, loc: ns.name.loc, }, )?; - p.ref_to_ts_namespace_member.insert(ref_, member_data); } } } StmtData::SEnum(ns) => { if ns.is_export { let ref_ = ns.name.ref_; - if let Some(member_data) = p.ref_to_ts_namespace_member.get(&ref_) { - let member_data = clone_ts_member_data(member_data); + if let Some(&data) = p.ref_to_ts_namespace_member.get(&ref_) { // SAFETY: original_name is arena-owned, valid for 'a. let enum_name: &[u8] = p.symbols[ref_.inner_index() as usize].original_name.slice(); exported_members.put( enum_name, TSNamespaceMember { - data: clone_ts_member_data(&member_data), + data, loc: ns.name.loc, }, )?; - p.ref_to_ts_namespace_member.insert(ref_, member_data); } } } diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 3366c8906499..4eafbca4a3ce 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1809,7 +1809,8 @@ pub(crate) mod __gated_printer { } // "**" can't contain certain unary expressions Op::Code::BinPow => { - match &e.left.data { + // An inlined enum member prints as its value, which can be a negative number. + match e.left.unwrap_inlined().data { ExprData::EUnary(left) => { if Op::Code::unary_assign_target(left.op) == js_ast::AssignTarget::None { diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 8d40a1bcfd28..4542bfb11006 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -77,7 +77,8 @@ bun_core::declare_scope!(cache, visible); /// Version 42: The body of a class with lowered class decorators names the class by the /// binding that holds what the decorators return. /// Version 43: A `bun:` specifier that names no builtin is printed as it is written. -const EXPECTED_VERSION: u32 = 43; +/// Version 44: A TypeScript namespace keeps its members when it merges with an exported function or class. +const EXPECTED_VERSION: u32 = 44; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index d6cfb2141007..6276b7ee9f64 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -1931,6 +1931,72 @@ describe("bundler", () => { stdout: "{\n Y: [class Y],\n} 2 1", }, }); + itBundled("edgecase/TypeScriptNamespaceSiblingFunctionLaterBlock", { + files: { + "/entry.ts": ` + namespace X { + export function Y() { + return 2; + } + export namespace Y { + export const Z = 1; + export enum E { + A = 7, + } + } + } + namespace X { + export namespace Y { + export const W = Z + 1; + } + } + namespace N { + export enum Q { + P = X.Y.E.A, + R, + } + } + console.log(X.Y(), X.Y.Z, X.Y.W, N.Q.P, N.Q.R); + `, + }, + run: { + stdout: "2 1 2 7 8", + }, + }); + itBundled("edgecase/TypeScriptNamespaceSiblingClassLaterBlock", { + files: { + "/entry.ts": ` + namespace X { + export class Y { + toJSON() { + return 2; + } + } + export namespace Y { + export const Z = 1; + export enum E { + A = 7, + } + } + } + namespace X { + export namespace Y { + export const W = Z + 1; + } + } + namespace N { + export enum Q { + P = X.Y.E.A, + R, + } + } + console.log(new X.Y().toJSON(), X.Y.Z, X.Y.W, N.Q.P, N.Q.R); + `, + }, + run: { + stdout: "2 1 2 7 8", + }, + }); itBundled("edgecase/TypeScriptNamespaceSiblingEnum", { files: { "/entry.ts": ` diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 4724a1bd2a31..8ac6d0d03213 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2163,6 +2163,113 @@ export default class { ts.expectPrinted_(input3, output3); }); + describe("a namespace that merges with an exported function or class of its block", () => { + // Runs the output and returns the value of `result`. + const run = (code, result) => new Function(`${ts.parsed(code)};\nreturn ${result};`)(); + + describe.each([ + ["function", "export function fn() {}"], + ["async function", "export async function fn() {}"], + ["generator function", "export function* fn() {}"], + ["function with overloads", "export function fn(a: string): void; export function fn(a?: any) {}"], + ["class", "export class fn {}"], + ["abstract class", "export abstract class fn {}"], + ])("after an exported %s", (_, declaration) => { + const pair = `${declaration} export namespace fn { export const x = 1; export enum E { A = 7 } }`; + + it("a later block of the namespace reads a member of the first block", () => { + // The outer `x` is not the one that the later block names. + const code = `var x = "outer";\nnamespace F { ${pair} }\nnamespace F { export namespace fn { export const y = x } }`; + expect(ts.parsed(code)).toContain("fn.y = fn.x;"); + expect(run(code, "F.fn.y")).toBe(1); + }); + + it("an enum of another namespace counts on from an enum member", () => { + const code = `namespace F { ${pair} }\nnamespace G { export enum Y { P = F.fn.E.A, R } }`; + expect(run(code, "G.Y")).toEqual({ P: 7, R: 8, 7: "P", 8: "R" }); + }); + + it("an enum of a later namespace in the same block counts on from an enum member", () => { + const code = `namespace F { ${pair} export namespace G { export enum Y { P = fn.E.A, R } } }`; + expect(run(code, "F.G.Y")).toEqual({ P: 7, R: 8, 7: "P", 8: "R" }); + }); + }); + + it("prints the value of an enum member", () => { + const out = ts.parsed( + "namespace F { export function fn() {} export namespace fn { export enum E { A = 7, B = 'b' } } export const a = [fn.E.A, fn.E.B] }\nexport const b = [F.fn.E.A, F.fn.E.B];", + ); + expect(out).toContain(`F.a = [7 /* A */, "b" /* B */];`); + expect(out).toEndWith(`export const b = [7 /* A */, "b" /* B */]`); + }); + + it("a negative enum member left of ** is in parentheses", () => { + const code = + "namespace F { export function fn() {} export namespace fn { export enum E { A = -3 } } }\nnamespace G { export const f = (n: number) => F.fn.E.A ** n }"; + expect(ts.parsed(code)).toContain("G.f = (n) => (-3) /* A */ ** n;"); + expect(run(code, "G.f(2)")).toBe(9); + }); + + it("a later block reads a member of the first block when bun runs the file", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `namespace F { export class fn {} export namespace fn { export const x = 1 } } + namespace F { export namespace fn { console.log(x) } }`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "1\n", stderr: "", exitCode: 0 }); + }); + + // Control: only an enum member folds. These reads print the same without the members. + it("prints a call, a construction and a member that is not an enum as before", () => { + const out = ts.parsed( + "namespace F { export function fn() {} export class C { static s = 1 } export function plain() {} export namespace fn { export const v = 1 } export namespace C { export const w = 1 } }\nexport const a = [F.fn(), new F.C(), F.C.s, F.plain(), F.fn.v, F.C.w, typeof F.fn, F.fn.name];", + ); + expect(out).toEndWith( + "export const a = [F.fn(), new F.C, F.C.s, F.plain(), F.fn.v, F.C.w, typeof F.fn, F.fn.name]", + ); + }); + + // A later block of the merged namespace now has the gaps that a later block of a plain + // namespace has. Each row holds the value that tsc gives. + describe("a later block of the namespace", () => { + const merged = members => `namespace F { export function fn() {} export namespace fn { ${members} } }\n`; + const later = body => `namespace F { export namespace fn { ${body} } }`; + + // Gives [1, 1, 6]. A write to an export of the first block is not rewritten and goes to the outer `x`. + it.todo("writes to a member of the first block", () => { + const code = `var x = "outer";\n${merged("export let x = 1")}${later("x = 5; x++; export const y = x")}`; + expect(run(code, "[F.fn.y, F.fn.x, x]")).toEqual([6, 6, "outer"]); + }); + + // Gives "param". Where no renamer runs, the closure argument and the parameter have the same name. + it.todo("reads a member of the first block under a parameter with the name of the namespace", () => { + const code = merged("export const x = 1") + later("export function g(fn: any) { return x }"); + expect(run(code, `F.fn.g({ x: "param" })`)).toBe(1); + }); + + // Gives `R: undefined`. A bare read of an enum of the first block stays a run-time read. + it.todo("counts on from a member of an enum of the first block", () => { + const code = merged("export enum E { A = 7 }") + later("export enum Y { P = E.A, R }"); + expect(run(code, "F.fn.Y")).toEqual({ P: 7, R: 8, 7: "P", 8: "R" }); + }); + + // Gives "param". The read of `E.A` stays a run-time read, under the parameter with the name of the closure argument. + it.todo("reads an enum member of the first block under a parameter with the name of the namespace", () => { + const code = merged("export enum E { A = 7 }") + later("export function f(fn: any) { return E.A }"); + expect(run(code, `F.fn.f({ E: { A: "param" } })`)).toBe(7); + }); + }); + }); + const input4 = `export enum x { y }`; const output4 = `export var x; ((x) => { @@ -3336,6 +3443,14 @@ console.log(
);`), expectParseError("await -x ** 0", "Unexpected **"); }); + it("exponentiation of an inlined enum member", () => { + ts.expectPrinted_( + "enum E { A = -1, B = 2 }\nx = [E.A ** 2, E['A'] ** 2, E.B ** 2];", + 'var E;\n((E) => {\n E[E["A"] = -1] = "A";\n E[E["B"] = 2] = "B";\n})(E ||= {});\n' + + "x = [(-1) /* A */ ** 2, (-1) /* A */ ** 2, 2 /* B */ ** 2];\n", + ); + }); + it("for-of loop variable named async", () => { // "\u0061sync" is the identifier `async`, which is legal as a for-of loop // variable, but printing it as the raw token sequence `async of` is a