Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ref, js_ast::ts::Data>,
/// Arena-allocated `TSNamespaceScope`s; their global-heap maps are freed in `Drop`.
pub(crate) ts_namespace_scopes: Vec<js_ast::StoreRef<js_ast::TSNamespaceScope>>,
Expand Down Expand Up @@ -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() {
Expand Down
29 changes: 4 additions & 25 deletions src/js_parser/parse/parse_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExprNodeList, Error> {
let p = self;
Expand Down Expand Up @@ -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) => {
Expand All @@ -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_) {
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
// 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);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
robobun marked this conversation as resolved.
Resolved
ExprData::EUnary(left) => {
if Op::Code::unary_assign_target(left.op) == js_ast::AssignTarget::None
{
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": `
Expand Down
115 changes: 115 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Comment thread
robobun marked this conversation as resolved.
Resolved
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) => {
Expand Down Expand Up @@ -3336,6 +3443,14 @@ console.log(<div {...obj} key="after" />);`),
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
Expand Down
Loading