Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
42 changes: 39 additions & 3 deletions src/js_parser/lower/lower_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1226,8 +1226,13 @@
}
}

// For named class expressions: swap to expr_class_ref for suffix ops
// For named class expressions: swap to expr_class_ref for suffix ops.
// The inner class name binding only exists inside the class body, so
// static initializers relocated into the suffix chain must reference
// the hoisted `_class` temp instead (class declarations keep their
// module-level binding, so no replacement is needed there).
let mut original_class_name_for_decorator: Option<&'a [u8]> = None;
let mut relocated_name_rewrite: Option<RewriteKind> = None;
if is_expr
&& !expr_class_is_anonymous
&& let Some(ecr) = expr_class_ref
Expand All @@ -1238,6 +1243,10 @@
.original_name
.slice(),
);
relocated_name_rewrite = Some(RewriteKind::ReplaceRef {
old: class_name_ref,
new: ecr,
});
class_name_ref = ecr;
class_name_loc = loc;
}
Expand Down Expand Up @@ -1534,7 +1543,7 @@
..Default::default()
});

let init_val = prop
let mut init_val = prop
.initializer
.unwrap_or_else(|| p.new_expr(E::Undefined {}, loc));
if !prop.flags.contains(Flags::Property::IsStatic) {
Expand All @@ -1549,6 +1558,18 @@
loc,
));
} else {
// Relocated out of the class body: `this` and the
// inner class name must become the class reference.
p.rewrite_expr(
&mut init_val,
RewriteKind::ReplaceThis {
ref_: class_name_ref,
loc: class_name_loc,
},
);
if let Some(rk) = relocated_name_rewrite {
p.rewrite_expr(&mut init_val, rk);
}
let cn_e = p.use_ref(class_name_ref, class_name_loc);
let wm_e3 = p.use_ref(wm_ref, loc);
suffix_exprs.push(p.call_rt(
Expand Down Expand Up @@ -2021,6 +2042,9 @@
loc: class_name_loc,
},
);
if let Some(rk) = relocated_name_rewrite {
p.rewrite_stmts(stmts_slice, rk);
}

let all_exprs = stmts_slice
.iter()
Expand Down Expand Up @@ -2078,7 +2102,19 @@
loc,
));
run_args.push(p.use_ref(class_name_ref, class_name_loc));
if let Some(init_val) = entry.prop.initializer {
if let Some(mut init_val) = entry.prop.initializer {
// Relocated out of the class body: `this` and the
// inner class name must become the class reference.
p.rewrite_expr(
&mut init_val,
RewriteKind::ReplaceThis {
ref_: class_name_ref,
loc: class_name_loc,
},
);
if let Some(rk) = relocated_name_rewrite {
p.rewrite_expr(&mut init_val, rk);
}

Check warning on line 2117 in src/js_parser/lower/lower_decorators.rs

View check run for this annotation

Claude / Claude Code Review

rewrite_expr/rewrite_stmts coverage gaps leave some this/inner-name refs unrewritten

Heads-up: the `rewrite_expr`/`rewrite_stmts` walker these new call sites depend on is not exhaustive, so a few shapes of `this`/inner-name reference are still left unrewritten after relocation — e.g. inside a nested `EClass` (line 505, wrong for `ReplaceRef`), in arrow/function default-parameter values (492–504), in computed object-literal keys (473–481), under `EAwait`/`EYield` (catch-all at 506), and inside `SFunction`/`SClass` declarations reached via `rewrite_stmts` (catch-all at 637). These
Comment thread
claude[bot] marked this conversation as resolved.
run_args.push(init_val);
}
let run_args_list = ExprNodeList::from_bump_vec(run_args);
Expand Down
144 changes: 144 additions & 0 deletions test/bundler/transpiler/es-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,148 @@ describe("ES Decorators", () => {
expect(exitCode).toBe(0);
});
});

// https://github.com/oven-sh/bun/issues/31917
describe("relocated static initializers", () => {
test("static field initializer sees the class expression's inner name", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
const C = class Foo {
static #m = function (tag) { return { tag }; };
@dec static s = Foo.#m("s").tag;
};
console.log(C.s);
`);
expect(stderr).toBe("");
expect(stdout).toBe("s\n");
expect(exitCode).toBe(0);
});

test("this in a static field initializer is the class (class expression)", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
function pick(x) { return x; }
const C = class Foo {
static #m = function (tag) { return { tag }; };
@dec static s = pick(this).#m("s").tag;
};
console.log(C.s);
`);
expect(stderr).toBe("");
expect(stdout).toBe("s\n");
expect(exitCode).toBe(0);
});

test("this in a static field initializer is the class (class declaration)", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
class Foo {
@dec static s = this.name;
}
console.log(Foo.s);
`);
expect(stderr).toBe("");
expect(stdout).toBe("Foo\n");
expect(exitCode).toBe(0);
});

test("this in an anonymous class expression's static field initializer", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
const C = class {
@dec static s = this;
};
console.log(C.s === C);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true\n");
expect(exitCode).toBe(0);
});

test("decorated static accessor initializer sees the inner name and this", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
const C = class Foo {
@dec static accessor a = [Foo, this];
};
console.log(C.a[0] === C, C.a[1] === C);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true true\n");
expect(exitCode).toBe(0);
});

test("undecorated static accessor initializer sees the inner name and this", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
const C = class Foo {
static accessor a = [Foo, this];
};
console.log(C.a[0] === C, C.a[1] === C);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true true\n");
expect(exitCode).toBe(0);
});

test("static block sees the class expression's inner name", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
let captured;
const C = class Foo {
@dec static s = 1;
static { captured = Foo; }
};
console.log(captured === C);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true\n");
expect(exitCode).toBe(0);
});

test("this inside a nested function is not rewritten", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
const C = class Foo {
@dec static s = function () { return this; };
};
const obj = {};
console.log(C.s.call(obj) === obj);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true\n");
expect(exitCode).toBe(0);
});

test("a shadowing binding in the initializer is not rewritten", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
const C = class Foo {
@dec static s = (function Foo() { return Foo; })();
};
console.log(typeof C.s === "function" && C.s !== C);
`);
expect(stderr).toBe("");
expect(stdout).toBe("true\n");
expect(exitCode).toBe(0);
});

test("static field initializer sees the class replaced by a class decorator", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
function dec(v, c) { return v; }
function swap(v, c) {
return class extends v { static extra = 1; };
}
@swap class D {
@dec static s = this.extra;
}
const E = @swap class Foo {
@dec static s = Foo.extra;
};
console.log(D.s, E.s);
`);
expect(stderr).toBe("");
expect(stdout).toBe("1 1\n");
expect(exitCode).toBe(0);
});
});
});
Loading