diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 5c847a25266c..a52ca93c43fc 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -472,6 +472,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } js_ast::ExprData::EObject(e) => { for prop in e.properties.slice_mut() { + // Computed keys; non-computed keys are literals (no-op). + if let Some(k) = &mut prop.key { + self.rewrite_expr(k, kind); + } if let Some(v) = &mut prop.value { self.rewrite_expr(v, kind); } @@ -489,24 +493,98 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.rewrite_expr(&mut part.value, kind); } } + js_ast::ExprData::EAwait(e) => self.rewrite_expr(&mut e.value, kind), + js_ast::ExprData::EYield(e) => { + if let Some(v) = &mut e.value { + self.rewrite_expr(v, kind); + } + } + js_ast::ExprData::EImport(e) => { + self.rewrite_expr(&mut e.expr, kind); + self.rewrite_expr(&mut e.options, kind); + } js_ast::ExprData::EArrow(e) => { + // Arrows inherit `this`: defaults and body get both kinds. + self.rewrite_args(e.args.slice_mut(), kind); let stmts = e.body.stmts.slice_mut(); self.rewrite_stmts(stmts, kind); } js_ast::ExprData::EFunction(e) => match kind { RewriteKind::ReplaceThis { .. } => {} RewriteKind::ReplaceRef { .. } => { + self.rewrite_args(e.func.args.slice_mut(), kind); let stmts = e.func.body.stmts.slice_mut(); if !stmts.is_empty() { self.rewrite_stmts(stmts, kind); } } }, - js_ast::ExprData::EClass(_) => {} + js_ast::ExprData::EClass(e) => self.rewrite_class(e, kind), _ => {} } } + /// Rewrite inside a nested class. The extends clause and computed keys + /// evaluate in the enclosing scope, so they get both kinds. Method + /// bodies, field initializers, and static blocks bind their own `this`, + /// so they are only walked for `ReplaceRef` (ref identity keeps + /// shadowing bindings intact). + fn rewrite_class(&mut self, class: &mut G::Class, kind: RewriteKind) { + if let Some(ext) = &mut class.extends { + self.rewrite_expr(ext, kind); + } + for prop in class.properties.slice_mut() { + if let Some(k) = &mut prop.key { + self.rewrite_expr(k, kind); + } + if matches!(kind, RewriteKind::ReplaceThis { .. }) { + continue; + } + if let Some(v) = &mut prop.value { + self.rewrite_expr(v, kind); + } + if let Some(ini) = &mut prop.initializer { + self.rewrite_expr(ini, kind); + } + if let Some(mut sb) = prop.class_static_block { + self.rewrite_stmts(sb.stmts.slice_mut(), kind); + } + } + } + + fn rewrite_args(&mut self, args: &mut [G::Arg], kind: RewriteKind) { + for arg in args.iter_mut() { + self.rewrite_binding(&mut arg.binding, kind); + if let Some(d) = &mut arg.default { + self.rewrite_expr(d, kind); + } + } + } + + /// Rewrite default values (and computed keys) in destructuring patterns. + fn rewrite_binding(&mut self, binding: &mut js_ast::Binding, kind: RewriteKind) { + match &mut binding.data { + js_ast::b::B::BIdentifier(_) | js_ast::b::B::BMissing(_) => {} + js_ast::b::B::BArray(arr) => { + for item in arr.items_mut() { + self.rewrite_binding(&mut item.binding, kind); + if let Some(d) = &mut item.default_value { + self.rewrite_expr(d, kind); + } + } + } + js_ast::b::B::BObject(obj) => { + for prop in obj.properties_mut() { + self.rewrite_expr(&mut prop.key, kind); + self.rewrite_binding(&mut prop.value, kind); + if let Some(d) = &mut prop.default_value { + self.rewrite_expr(d, kind); + } + } + } + } + } + fn rewrite_stmts(&mut self, stmts: &mut [Stmt], kind: RewriteKind) { for cur_stmt in stmts.iter_mut() { let cur_loc = cur_stmt.loc; @@ -524,6 +602,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } js_ast::StmtData::SLocal(local) => { for decl in local.decls.slice_mut() { + self.rewrite_binding(&mut decl.binding, kind); if let Some(v) = &mut decl.value { self.rewrite_expr(v, kind); } @@ -565,6 +644,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data.body = body; } js_ast::StmtData::SForIn(data) => { + let mut init = data.init; + self.rewrite_stmts(core::slice::from_mut(&mut init), kind); + data.init = init; let mut v = data.value; self.rewrite_expr(&mut v, kind); data.value = v; @@ -573,6 +655,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O data.body = body; } js_ast::StmtData::SForOf(data) => { + let mut init = data.init; + self.rewrite_stmts(core::slice::from_mut(&mut init), kind); + data.init = init; let mut v = data.value; self.rewrite_expr(&mut v, kind); data.value = v; @@ -613,6 +698,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let body = data.body.slice_mut(); self.rewrite_stmts(body, kind); if let Some(c) = &mut data.catch_ { + if let Some(b) = &mut c.binding { + self.rewrite_binding(b, kind); + } let cb = c.body.slice_mut(); self.rewrite_stmts(cb, kind); } @@ -634,6 +722,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.rewrite_stmts(core::slice::from_mut(&mut body), kind); data.body = body; } + js_ast::StmtData::SFunction(data) => match kind { + // Function declarations bind their own `this`. + RewriteKind::ReplaceThis { .. } => {} + RewriteKind::ReplaceRef { .. } => { + self.rewrite_args(data.func.args.slice_mut(), kind); + let stmts = data.func.body.stmts.slice_mut(); + self.rewrite_stmts(stmts, kind); + } + }, + js_ast::StmtData::SClass(data) => self.rewrite_class(&mut data.class, kind), _ => {} } } @@ -1226,8 +1324,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - // 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 = None; if is_expr && !expr_class_is_anonymous && let Some(ecr) = expr_class_ref @@ -1238,6 +1341,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O .original_name .slice(), ); + relocated_name_rewrite = Some(RewriteKind::ReplaceRef { + old: class_name_ref, + new: ecr, + }); class_name_ref = ecr; class_name_loc = loc; } @@ -1534,7 +1641,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ..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) { @@ -1549,6 +1656,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O 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( @@ -2021,6 +2140,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O loc: class_name_loc, }, ); + if let Some(rk) = relocated_name_rewrite { + p.rewrite_stmts(stmts_slice, rk); + } let all_exprs = stmts_slice .iter() @@ -2078,7 +2200,19 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O 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); + } run_args.push(init_val); } let run_args_list = ExprNodeList::from_bump_vec(run_args); diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index 552260cf7e93..8dd32cff4b7d 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -809,4 +809,233 @@ 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); + }); + + test("references inside a nested class in the initializer", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v, c) { return v; } + function pick(x) { return x; } + const A = class Foo { @dec static s = (class { static x = Foo; }).x; }; + console.log(A.s === A); + const B = class Foo { + static kname = "m"; + @dec static s = new (class extends pick(this) { [pick(this).kname]() { return 7; } })(); + }; + console.log(B.s instanceof B, B.s.m()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\ntrue 7\n"); + expect(exitCode).toBe(0); + }); + + test("references in parameter defaults in the initializer", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v, c) { return v; } + const A = class Foo { @dec static s = (x = this) => x; }; + console.log(A.s() === A); + const B = class Foo { @dec static s = (function (x = Foo) { return x; })(); }; + console.log(B.s === B); + const C = class Foo { @dec static s = (({ v = Foo } = {}) => v)(); }; + console.log(C.s === C); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\ntrue\ntrue\n"); + expect(exitCode).toBe(0); + }); + + test("references in computed object keys in the initializer", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v, c) { return v; } + const A = class Foo { static k = "a"; @dec static s = { [Foo.k]: 1 }; }; + console.log(A.s.a); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("1\n"); + expect(exitCode).toBe(0); + }); + + test("references under await, yield, and import() in the initializer", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v, c) { return v; } + const A = class Foo { @dec static s = (async () => await Foo)(); }; + console.log((await A.s) === A); + const B = class Foo { @dec static s = [...(function* () { yield Foo; })()][0]; }; + console.log(B.s === B); + const C = class Foo { + static tag = "marked"; + @dec static s = import("data:text/javascript,export default " + JSON.stringify(Foo.tag)); + }; + console.log((await C.s).default); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true\ntrue\nmarked\n"); + expect(exitCode).toBe(0); + }); + + test("references in declarations and patterns inside a static block", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v, c) { return v; } + let fn, cls, def, caught, loop; + const C = class Foo { + @dec static x = 1; + static { + function bar() { return Foo; } + fn = bar(); + class Helper { static z = Foo; } + cls = Helper.z; + const { v = Foo } = {}; + def = v; + try { throw {}; } catch ({ w = Foo }) { caught = w; } + for (const { u = Foo } of [{}]) loop = u; + } + }; + console.log(fn === C, cls === C, def === C, caught === C, loop === C); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true true true true true\n"); + expect(exitCode).toBe(0); + }); + }); });