Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 1 addition & 7 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> {
// If this is true, then all top-level statements are wrapped in a try/catch
pub(crate) will_wrap_module_in_try_catch_for_using: bool,

/// Used for react refresh, it must be able to insert `const _s = $RefreshSig$();`
/// Receives hoisted declarations; installed by `visit_stmts` and, for enum members, `s_enum`.
pub(crate) nearest_stmt_list: Option<NonNull<ListManaged<'a, Stmt>>>,
// Lifetime caution: points at a stack local saved/restored across calls.
/// Name from assignment context for anonymous decorated class expressions.
Expand Down Expand Up @@ -659,12 +659,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

/// Safe mutable projection of `nearest_stmt_list`.
///
/// The pointer targets a `ListManaged` living on a parent
/// `visit_stmts_and_prepend_temp_refs` stack frame (saved/restored around
/// each visit), disjoint from `*self`, so a transient `&mut` tied to
/// `&mut self` cannot alias any other live borrow. Centralises the
/// `unsafe` so call sites stay safe.
#[inline]
pub(crate) fn nearest_stmt_list_mut(&mut self) -> Option<&mut ListManaged<'a, Stmt>> {
// SAFETY: `nearest_stmt_list` is a back-pointer to stack storage on
Expand Down
9 changes: 8 additions & 1 deletion src/js_parser/visit/visit_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
p.should_fold_typescript_constant_expressions;
p.should_fold_typescript_constant_expressions = true;

// Declarations hoisted out of the initializers are emitted at the top of the enum closure.
let mut hoisted_stmts: StmtList<'a> = BumpVec::new_in(p.arena);
let prev_nearest_stmt_list = p.nearest_stmt_list;
p.nearest_stmt_list = core::ptr::NonNull::new(core::ptr::addr_of_mut!(hoisted_stmts));

// Create an assignment for each enum value
for value in values.iter_mut() {
let name: &'a [u8] = value.name.slice();
Expand Down Expand Up @@ -2313,11 +2318,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

p.nearest_stmt_list = prev_nearest_stmt_list;
p.pop_scope();
p.should_fold_typescript_constant_expressions =
old_should_fold_typescript_constant_expressions;

let mut value_stmts: StmtList<'a> = BumpVec::with_capacity_in(value_exprs.len(), p.arena);
let mut value_stmts: StmtList<'a> = hoisted_stmts;
value_stmts.reserve(value_exprs.len());
// Generate statements from expressions
for expr in value_exprs.iter() {
value_stmts.push(p.s(
Expand Down
124 changes: 109 additions & 15 deletions test/bundler/transpiler/es-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ async function runDecorator(code: string) {
return { stdout, stderr: filterStderr(rawStderr), exitCode };
}

async function runDecoratorTS(code: string) {
using dir = tempDir("es-dec-ts", {
"tsconfig.json": JSON.stringify({ compilerOptions: {} }),
"test.ts": code,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr: filterStderr(rawStderr), exitCode };
}

describe("ES Decorators", () => {
describe("class decorators", () => {
test("basic class decorator", async () => {
Expand Down Expand Up @@ -391,21 +406,6 @@ describe("ES Decorators", () => {
expect(exitCode).toBe(0);
});

async function runDecoratorTS(code: string) {
using dir = tempDir("es-dec-ts", {
"tsconfig.json": JSON.stringify({ compilerOptions: {} }),
"test.ts": code,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, rawStderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr: filterStderr(rawStderr), exitCode };
}

test("non-null assertion in decorator member expression", async () => {
const { stdout, stderr, exitCode } = await runDecoratorTS(`
const ns = {
Expand Down Expand Up @@ -1210,4 +1210,98 @@ describe("ES Decorators", () => {
expect(exitCode).toBe(0);
});
});

// Lowering a class expression hoists its temporaries (_dec, _init, _class,
// accessor storage, ...) as a `var` into the nearest statement list. An enum
// body is visited before the statements around it, so the temporaries used to
// land in the list enclosing the enum's own (none at the top level, where
// they were dropped) instead of in the closure the enum body compiles to.
describe("class expressions inside enum initializers", () => {
test.concurrent("top-level enum", async () => {
const { stdout, stderr, exitCode } = await runDecoratorTS(`
const kinds: string[] = [];
function dec(_value: any, ctx: any) { kinds.push(ctx.kind); }
let Decorated: any;
let WithAccessor: any;
enum E {
A = ((Decorated = @dec class { @dec m() {} }), 1),
B = ((WithAccessor = class { accessor x = "x" }), 2),
}
const instance = new WithAccessor();
instance.x += "!";
console.log(JSON.stringify([E.A, E.B, E[2], kinds, typeof Decorated, instance.x]));
`);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual([1, 2, "B", ["method", "class"], "function", "x!"]);
expect(exitCode).toBe(0);
});

test.concurrent("enum inside a function gets fresh temporaries per call", async () => {
const { stdout, stderr, exitCode } = await runDecoratorTS(`
let calls = 0;
function dec(_value: any, _ctx: any) {
const id = ++calls;
return { init: () => id };
}
function make() {
let K: any;
enum E {
A = ((K = class { @dec accessor x = 0 }), 0),
}
return K;
}
const K1 = make();
const K2 = make();
console.log(new K1().x, new K2().x);
`);
expect(stderr).toBe("");
expect(stdout).toBe("1 2\n");
expect(exitCode).toBe(0);
});

// Each enum body gets its own temporaries, so a class created by the first
// enum keeps working after the second enum has been evaluated.
test.concurrent("sibling enums do not share temporaries", async () => {
const { stdout, stderr, exitCode } = await runDecoratorTS(`
let A: any;
let B: any;
enum First {
M = ((A = class { accessor x = 1 }), 0),
}
const a = new A();
enum Second {
M = ((B = class { accessor x = 2 }), 0),
}
console.log(a.x, new B().x);
`);
expect(stderr).toBe("");
expect(stdout).toBe("1 2\n");
expect(exitCode).toBe(0);
});

test("Bun.Transpiler declares the temporaries inside the enum closure", () => {
const transpiler = new Bun.Transpiler({ loader: "ts" });
const declaredInsideClosure = /\(\(E\) => \{\s*var [^;]*\b_init\b[^;]*;/;

const topLevel = transpiler.transformSync(`
declare const dec: any;
enum E {
A = (@dec class {}, 0),
}
`);
expect(topLevel).toMatch(declaredInsideClosure);
expect(topLevel).not.toMatch(/^var [^;]*\b_init\b/m);

const nested = transpiler.transformSync(`
declare const dec: any;
function make() {
enum E {
A = (@dec class {}, 0),
}
}
`);
expect(nested).toMatch(declaredInsideClosure);
expect(nested).not.toMatch(/^var /m);
});
});
});
Loading