diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 97de7facd86c..b0be6c22c8fa 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -295,47 +295,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - /// Build property access: target.name or target[key]. - fn member_target(&mut self, target_expr: Expr, prop: &Property) -> Expr { - let key_expr = prop.key.expect("infallible: prop has key"); - if prop.flags.contains(Flags::Property::IsComputed) - || matches!(key_expr.data, js_ast::ExprData::ENumber(_)) - { - return self.new_expr( - E::Index { - target: target_expr, - index: key_expr, - optional_chain: None, - }, - key_expr.loc, - ); - } - if let js_ast::ExprData::EString(s) = &key_expr.data { - // `E::Dot.name` is a UTF-8 `Str`; a UTF-16 `EString.data` stores - // u16-count bytes that are garbage as UTF-8. Fall through to - // `E::Index` for UTF-16 keys so the printer emits `["โ€ฆ"]`. - if s.is_utf8() { - return self.new_expr( - E::Dot { - target: target_expr, - name: s.data, - name_loc: key_expr.loc, - ..Default::default() - }, - key_expr.loc, - ); - } - } - self.new_expr( - E::Index { - target: target_expr, - index: key_expr, - optional_chain: None, - }, - key_expr.loc, - ) - } - fn init_flag(idx: usize) -> f64 { ((4 + 2 * idx) << 1) as f64 } @@ -2183,8 +2142,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O )); } else { let cn_e = p.use_ref(class_name_ref, class_name_loc); - let assign_target = p.member_target(cn_e, &entry.prop); - suffix_exprs.push(Expr::assign(assign_target, run_init_call)); + let key_e = entry.prop.key.expect("infallible: prop has key"); + suffix_exprs.push(p.call_rt( + loc, + b"__publicField", + &[cn_e, key_e, run_init_call], + )); } // Extra initializer @@ -2269,8 +2232,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O )); } else { let t_e = p.new_expr(E::This {}, loc); - let mt = p.member_target(t_e, &entry.prop); - constructor_inject_stmts.push(Stmt::assign(mt, run_init_call)); + let key_e = entry.prop.key.expect("infallible: prop has key"); + let call = p.call_rt(loc, b"__publicField", &[t_e, key_e, run_init_call]); + constructor_inject_stmts.push(p.s( + S::SExpr { + value: call, + ..Default::default() + }, + loc, + )); } // Extra initializer diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 2a4761ab6141..457eb1f52b57 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -6474,8 +6474,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut static_decorators = BumpVec::::new_in(self.arena); let mut instance_decorators = BumpVec::::new_in(self.arena); - let mut instance_members = BumpVec::::new_in(self.arena); - let mut static_members = BumpVec::::new_in(self.arena); let mut class_properties = BumpVec::::new_in(self.arena); for prop in s_class.class.properties.slice_mut().iter_mut() { @@ -6605,64 +6603,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - if prop.kind != PropertyKind::ClassStaticBlock - && !prop.flags.contains(Flags::Property::IsMethod) - && !matches!( - prop.key.map(|k| k.data), - Some(js_ast::ExprData::EPrivateIdentifier(_)) - ) - && prop.ts_decorators.len_u32() > 0 - { - // remove decorated fields without initializers to avoid assigning undefined. - let Some(initializer) = prop.initializer else { - continue; - }; - - let mut target: Expr; - if prop.flags.contains(Flags::Property::IsStatic) { - let class_name = s_class.class.class_name.unwrap(); - let class_ref = class_name.ref_; - self.record_usage(class_ref); - target = self.new_expr(E::Identifier::init(class_ref), class_name.loc); - } else { - target = self.new_expr( - E::This {}, - prop.key.expect("infallible: prop has key").loc, - ); - } - - let key = prop.key.expect("infallible: prop has key"); - target = match &key.data { - js_ast::ExprData::EString(s) - if s.is_utf8() - && !prop.flags.contains(Flags::Property::IsComputed) => - { - self.new_expr( - E::Dot { - target, - name: s.data, - name_loc: key.loc, - ..Default::default() - }, - key.loc, - ) - } - _ => self.new_expr( - E::Index { - target, - index: key, - optional_chain: None, - }, - key.loc, - ), - }; - - // remove fields with decorators from class body. Move static members outside of class. - if prop.flags.contains(Flags::Property::IsStatic) { - static_members.push(Stmt::assign(target, initializer)); - } else { - instance_members.push(Stmt::assign(target, initializer)); - } + // Undecorated `declare`/`abstract` fields were already dropped in `parse_property`. + if matches!(prop.kind, PropertyKind::Declare | PropertyKind::Abstract) { continue; } @@ -6674,122 +6616,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O s_class.class.properties = bun_ast::StoreSlice::new_mut(class_properties.into_bump_slice_mut()); - if !instance_members.is_empty() { - if let Some(mut cf) = constructor_function { - // `body.stmts` is an arena-owned `StoreSlice`. - let old_stmts: &[Stmt] = cf.func.body.stmts.slice(); - // statements coming from class body inserted after super call or beginning of constructor. - let mut super_index: Option = None; - for (index, item) in old_stmts.iter().enumerate() { - if !matches!(item.data, js_ast::StmtData::SExpr(se) if matches!(se.value.data, js_ast::ExprData::ECall(c) if matches!(c.target.data, js_ast::ExprData::ESuper(_)))) - { - continue; - } - super_index = Some(index); - break; - } - - let i = super_index.map(|j| j + 1).unwrap_or(0); - let mut constructor_stmts = BumpVec::::with_capacity_in( - old_stmts.len() + instance_members.len(), - self.arena, - ); - constructor_stmts.extend_from_slice(&old_stmts[..i]); - constructor_stmts.extend_from_slice(&instance_members); - constructor_stmts.extend_from_slice(&old_stmts[i..]); - - cf.func.body.stmts = - bun_ast::StoreSlice::new_mut(constructor_stmts.into_bump_slice_mut()); - } else { - // Rebuild the property list with the new entry at index 0 - // (Property is not Clone). - let old_props: bun_ast::StoreSlice = s_class.class.properties; - let old_len = old_props.len(); - let mut properties = - BumpVec::::with_capacity_in(old_len + 1, self.arena); - let mut constructor_stmts = BumpVec::::new_in(self.arena); - - if s_class.class.extends.is_some() { - let target = self.new_expr(E::Super {}, stmt.loc); - let arguments_ref = - self.new_symbol(js_ast::symbol::Kind::Unbound, arguments_str); - VecExt::append(&mut self.current_scope_mut().generated, arguments_ref); - - let spread_inner = - self.new_expr(E::Identifier::init(arguments_ref), stmt.loc); - let super_ = self.new_expr( - E::Spread { - value: spread_inner, - }, - stmt.loc, - ); - let args = ExprNodeList::init_one(super_); - - let call_value = self.new_expr( - E::Call { - target, - args, - ..Default::default() - }, - stmt.loc, - ); - constructor_stmts.push(self.s( - S::SExpr { - value: call_value, - ..Default::default() - }, - stmt.loc, - )); - } - - constructor_stmts.extend_from_slice(&instance_members); - - let key_expr = - self.new_expr(E::EString::from_static(b"constructor"), stmt.loc); - let value_expr = self.new_expr( - E::Function { - func: G::Fn { - name: None, - open_parens_loc: bun_ast::Loc::EMPTY, - args: bun_ast::StoreSlice::EMPTY, - body: G::FnBody { - loc: stmt.loc, - stmts: bun_ast::StoreSlice::new_mut( - constructor_stmts.into_bump_slice_mut(), - ), - }, - flags: Flags::FUNCTION_NONE, - ..Default::default() - }, - }, - stmt.loc, - ); - properties.push(G::Property { - flags: Flags::Property::IsMethod.into(), - key: Some(key_expr), - value: Some(value_expr), - ..Default::default() - }); - for old in old_props.slice_mut().iter_mut() { - properties.push(core::mem::take(old)); - } - - s_class.class.properties = - bun_ast::StoreSlice::new_mut(properties.into_bump_slice_mut()); - } - - // TODO: make sure "super()" comes before instance field initializers - // https://github.com/evanw/esbuild/blob/e9413cc4f7ab87263ea244a999c6fa1f1e34dc65/internal/js_parser/js_parser_lower.go#L2742 - } - let mut stmts_count: usize = - 1 + static_members.len() + instance_decorators.len() + static_decorators.len(); + 1 + instance_decorators.len() + static_decorators.len(); if s_class.class.ts_decorators.len_u32() > 0 { stmts_count += 1; } let mut stmts = BumpVec::::with_capacity_in(stmts_count, self.arena); stmts.push(stmt); - stmts.extend_from_slice(&static_members); stmts.extend_from_slice(&instance_decorators); stmts.extend_from_slice(&static_decorators); if s_class.class.ts_decorators.len_u32() > 0 { diff --git a/test/bundler/transpiler/decorators.test.ts b/test/bundler/transpiler/decorators.test.ts index 80800f17c415..315fe7456812 100644 --- a/test/bundler/transpiler/decorators.test.ts +++ b/test/bundler/transpiler/decorators.test.ts @@ -373,7 +373,10 @@ test("decorators random", () => { flavor = "vanilla"; } - // Property Decorator + // Property decorator that installs an accessor on the prototype. Under + // [[Define]] field semantics (useDefineForClassFields: true, which Bun + // assumes and which matches tsc/esbuild), the class field shadows that + // accessor with an own data property, so the setter is never reached. function Emoji() { return function (target: Object, key: string | symbol) { let val = target[key]; @@ -395,9 +398,11 @@ test("decorators random", () => { } const iceCream = new IceCreamComponent(); - expect(iceCream.flavor === "๐Ÿฆ vanilla ๐Ÿฆ").toBe(true); + expect(iceCream.flavor).toBe("vanilla"); + expect(Object.prototype.hasOwnProperty.call(iceCream, "flavor")).toBe(true); iceCream.flavor = "chocolate"; - expect(iceCream.flavor === "๐Ÿฆ chocolate ๐Ÿฆ").toBe(true); + expect(iceCream.flavor).toBe("chocolate"); + expect(Object.getOwnPropertyDescriptor(IceCreamComponent.prototype, "flavor")?.get).toBeInstanceOf(Function); const i: unique symbol = Symbol.for("i"); const h: unique symbol = Symbol.for("h"); @@ -409,18 +414,23 @@ test("decorators random", () => { const u6: unique symbol = Symbol.for("u6"); const u8: unique symbol = Symbol.for("u8"); + const decorated: Array<[Object, string | symbol]> = []; + function Tag(target: Object, key: string | symbol) { + decorated.push([target, key]); + } + class S { - @StringAppender("๐Ÿ˜›") k = 35; - @StringAppender("๐Ÿค ") static j = 4; - @StringAppender("๐Ÿ˜ตโ€๐Ÿ’ซ") private static [h] = 30; - @StringAppender("๐Ÿคฏ") private static u = 60; - @StringAppender("๐Ÿคช") private [t] = 32; - @StringAppender("๐Ÿค‘") [i] = 8; - @StringAppender("๐ŸŽƒ") private e = 10; - @StringAppender("๐Ÿ‘ป") static [q] = 202; - @StringAppender("๐Ÿ˜‡") r = S[h]; + @Tag k = 35; + @Tag static j = 4; + @Tag private static [h] = 30; + @Tag private static u = 60; + @Tag private [t] = 32; + @Tag [i] = 8; + @Tag private e = 10; + @Tag static [q] = 202; + @Tag r = S[h]; _y: number; - @StringAppender("๐Ÿคก") get y() { + @Tag get y() { return this._y; } set y(next) { @@ -428,36 +438,36 @@ test("decorators random", () => { } #o = 100; - @StringAppender("๐Ÿ˜") u1: number; - @StringAppender("๐Ÿฅณ") static u2: number; - @StringAppender("๐Ÿค“") private static [u3]: number; - @StringAppender("๐Ÿฅบ") private static u4: number; - @StringAppender("๐Ÿคฏ") private [u5]: number; - @StringAppender("๐Ÿคฉ") [u6]: number; - @StringAppender("โ˜น๏ธ") private u7: number; - @StringAppender("๐Ÿ™ƒ") static [u8]: number; - - @StringAppender("๐Ÿค”") u9 = this.u1; - @StringAppender("๐Ÿคจ") u10 = this.u2; - @StringAppender("๐Ÿ™‚") u11 = S[u3]; - @StringAppender("๐Ÿ™") u12 = S.u4; - @StringAppender("๐Ÿ˜") u13 = this[u5]; - @StringAppender("๐Ÿ˜‘") u14 = this[u6]; - @StringAppender("๐Ÿ˜ถ") u15 = this.u7; - @StringAppender("๐Ÿ˜") u16 = S[u8]; + @Tag u1: number; + @Tag static u2: number; + @Tag private static [u3]: number; + @Tag private static u4: number; + @Tag private [u5]: number; + @Tag [u6]: number; + @Tag private u7: number; + @Tag static [u8]: number; + + @Tag u9 = this.u1; + @Tag u10 = S.u2; + @Tag u11 = S[u3]; + @Tag u12 = S.u4; + @Tag u13 = this[u5]; + @Tag u14 = this[u6]; + @Tag u15 = this.u7; + @Tag u16 = S[u8]; constructor() { this.k = 3; - expect(this.k).toBe("3 ๐Ÿ˜›"); + expect(this.k).toBe(3); expect(S.j).toBe(4); - expect(this[i]).toBe("8 ๐Ÿค‘"); - expect(this.e).toBe("10 ๐ŸŽƒ"); + expect(this[i]).toBe(8); + expect(this.e).toBe(10); expect(S[h]).toBe(30); expect(S.u).toBe(60); - expect(this[t]).toBe("32 ๐Ÿคช"); + expect(this[t]).toBe(32); expect(S[q]).toBe(202); expect(this.#o).toBe(100); - expect(this.r).toBe("30 ๐Ÿ˜‡"); + expect(this.r).toBe(30); expect(this.y).toBe(undefined); this.y = 100; expect(this.y).toBe(100); @@ -471,89 +481,49 @@ test("decorators random", () => { expect(this.u7).toBe(undefined); expect(S[u8]).toBe(undefined); - expect(this.u9).toBe("undefined ๐Ÿค”"); - expect(this.u10).toBe("undefined ๐Ÿคจ"); - expect(this.u11).toBe("undefined ๐Ÿ™‚"); - expect(this.u12).toBe("undefined ๐Ÿ™"); - expect(this.u13).toBe("undefined ๐Ÿ˜"); - expect(this.u14).toBe("undefined ๐Ÿ˜‘"); - expect(this.u15).toBe("undefined ๐Ÿ˜ถ"); - expect(this.u16).toBe("undefined ๐Ÿ˜"); - - this.u1 = 100; - expect(this.u1).toBe("100 ๐Ÿ˜"); - S.u2 = 100; - expect(S.u2).toBe("100 ๐Ÿฅณ"); - S[u3] = 100; - expect(S[u3]).toBe("100 ๐Ÿค“"); - S.u4 = 100; - expect(S.u4).toBe("100 ๐Ÿฅบ"); - this[u5] = 100; - expect(this[u5]).toBe("100 ๐Ÿคฏ"); - this[u6] = 100; - expect(this[u6]).toBe("100 ๐Ÿคฉ"); - this.u7 = 100; - expect(this.u7).toBe("100 โ˜น๏ธ"); - S[u8] = 100; - expect(S[u8]).toBe("100 ๐Ÿ™ƒ"); - - expect(this.u9).toBe("undefined ๐Ÿค”"); - expect(this.u10).toBe("undefined ๐Ÿคจ"); - expect(this.u11).toBe("undefined ๐Ÿ™‚"); - expect(this.u12).toBe("undefined ๐Ÿ™"); - expect(this.u13).toBe("undefined ๐Ÿ˜"); - expect(this.u14).toBe("undefined ๐Ÿ˜‘"); - expect(this.u15).toBe("undefined ๐Ÿ˜ถ"); - expect(this.u16).toBe("undefined ๐Ÿ˜"); + for (const k of ["u9", "u10", "u11", "u12", "u13", "u14", "u15", "u16"] as const) { + expect(this[k]).toBe(undefined); + expect(Object.prototype.hasOwnProperty.call(this, k)).toBe(true); + } } } let s = new S(); - expect(s.u9).toBe("undefined ๐Ÿค”"); - expect(s.u10).toBe("undefined ๐Ÿคจ"); - expect(s.u11).toBe("undefined ๐Ÿ™‚"); - expect(s.u12).toBe("undefined ๐Ÿ™"); - expect(s.u13).toBe("undefined ๐Ÿ˜"); - expect(s.u14).toBe("undefined ๐Ÿ˜‘"); - expect(s.u15).toBe("undefined ๐Ÿ˜ถ"); - expect(s.u16).toBe("undefined ๐Ÿ˜"); - - s.u9 = 35; - expect(s.u9).toBe("35 ๐Ÿค”"); - s.u10 = 36; - expect(s.u10).toBe("36 ๐Ÿคจ"); - s.u11 = 37; - expect(s.u11).toBe("37 ๐Ÿ™‚"); - s.u12 = 38; - expect(s.u12).toBe("38 ๐Ÿ™"); - s.u13 = 39; - expect(s.u13).toBe("39 ๐Ÿ˜"); - s.u14 = 40; - expect(s.u14).toBe("40 ๐Ÿ˜‘"); - s.u15 = 41; - expect(s.u15).toBe("41 ๐Ÿ˜ถ"); - s.u16 = 42; - expect(s.u16).toBe("42 ๐Ÿ˜"); - - function StringAppender(emoji: string) { - return function (target: Object, key: string | symbol) { - let val = target[key]; - - const getter = () => { - return val; - }; - const setter = value => { - val = `${value} ${emoji}`; - }; - - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); - }; - } + for (const k of ["k", t, i, "e", "r", "u1", u5, u6, "u7"] as const) { + expect(Object.prototype.hasOwnProperty.call(s, k)).toBe(true); + } + for (const k of ["j", h, "u", q, "u2", u3, "u4", u8] as const) { + expect(Object.prototype.hasOwnProperty.call(S, k)).toBe(true); + } + + expect(decorated).toEqual([ + [S.prototype, "k"], + [S.prototype, t], + [S.prototype, i], + [S.prototype, "e"], + [S.prototype, "r"], + [S.prototype, "y"], + [S.prototype, "u1"], + [S.prototype, u5], + [S.prototype, u6], + [S.prototype, "u7"], + [S.prototype, "u9"], + [S.prototype, "u10"], + [S.prototype, "u11"], + [S.prototype, "u12"], + [S.prototype, "u13"], + [S.prototype, "u14"], + [S.prototype, "u15"], + [S.prototype, "u16"], + [S, "j"], + [S, h], + [S, "u"], + [S, q], + [S, "u2"], + [S, u3], + [S, "u4"], + [S, u8], + ]); }); test("class field order", () => { @@ -670,7 +640,7 @@ test("class extending from another class", () => { } }); -test("decorated fields moving to constructor", () => { +test("decorated field initializers", () => { class A { @d1 a = 3; @d2 b = 4; @@ -698,6 +668,66 @@ test("decorated fields moving to constructor", () => { expect(a.c).toBe(5); }); +test("decorated field keeps [[Define]] semantics over an inherited accessor", () => { + const seen: string[] = []; + function tag(_t: any, k: any) { + seen.push("tag:" + String(k)); + } + class Base { + set p(v: any) { + seen.push("SETTER-INVOKED:" + v); + } + get p() { + return "getter"; + } + static set s(v: any) { + seen.push("STATIC-SETTER:" + v); + } + static get s() { + return "static-getter"; + } + } + class C extends Base { + @tag p: any = "field-init"; + q: any = "plain-field"; + @tag static s: any = "static-init"; + @tag noInit: any; + } + const c: any = new C(); + expect({ + seen, + p: c.p, + ownP: Object.prototype.hasOwnProperty.call(c, "p"), + ownQ: Object.prototype.hasOwnProperty.call(c, "q"), + ownNoInit: Object.prototype.hasOwnProperty.call(c, "noInit"), + s: (C as any).s, + ownS: Object.prototype.hasOwnProperty.call(C, "s"), + }).toEqual({ + seen: ["tag:p", "tag:noInit", "tag:s"], + p: "field-init", + ownP: true, + ownQ: true, + ownNoInit: true, + s: "static-init", + ownS: true, + }); +}); + +test("decorated field does not throw on an inherited getter-only accessor", () => { + function tag(_t: any, _k: any) {} + class Base { + get p() { + return "getter"; + } + } + class C extends Base { + @tag p: any = "field-init"; + } + const c: any = new C(); + expect(c.p).toBe("field-init"); + expect(Object.prototype.hasOwnProperty.call(c, "p")).toBe(true); +}); + test("only class decorator", () => { let a = 0; @d1 @@ -1056,39 +1086,31 @@ test("decorator and declare", () => { expect(counter).toBe(1); }); -test("lowering many decorated instance fields into a large constructor body stays linear", async () => { - // Hold N fixed; compare M=100 vs M=50000. If the splice-after-super() were O(M*N) - // instead of O(M+N), tLarge/tSmall would be ~5x (debug) / ~90x (release) here, not ~2x. +test("many decorated instance fields alongside a large constructor body transpile correctly", async () => { + // Legacy-decorated fields stay in the class body (they are not spliced into the + // constructor), so constructor size is independent of field count. This guards the + // output shape and that lowering a large class completes. await using proc = Bun.spawn({ cmd: [ bunExe(), "-e", ` - const N = 500000; - function gen(M) { - let src = "function d(t,k){}\\nclass Base {}\\nclass Foo extends Base {\\n"; - for (let i = 0; i < M; i++) src += "@d f" + i + " = " + i + ";\\n"; - src += "constructor() {\\nsuper();\\n"; - src += Buffer.alloc(N * 2, "{}").toString(); - src += "\\n}\\n}\\n"; - return src; - } + const M = 50000; + let src = "function d(t,k){}\\nclass Base {}\\nclass Foo extends Base {\\n"; + for (let i = 0; i < M; i++) src += "@d f" + i + " = " + i + ";\\n"; + src += "constructor() {\\nsuper();\\n"; + src += Buffer.alloc(1000 * 2, "{}").toString(); + src += "\\n}\\n}\\n"; const t = new Bun.Transpiler({ loader: "ts", tsconfig: { compilerOptions: { experimentalDecorators: true } }, }); - function time(M) { - const src = gen(M); - const t0 = performance.now(); - const out = t.transformSync(src); - const ms = performance.now() - t0; - if (!out.includes("this.f0 = 0") || !out.includes("this.f" + (M - 1) + " = " + (M - 1))) - throw new Error("instance-field initializers missing from lowered constructor at M=" + M); - return ms; - } - const tSmall = time(100); - const tLarge = time(50000); - console.log(JSON.stringify({ tSmall, tLarge, ratio: tLarge / tSmall })); + const out = t.transformSync(src); + if (!out.includes("f0 = 0;") || !out.includes("f" + (M - 1) + " = " + (M - 1) + ";")) + throw new Error("decorated instance fields missing from class body"); + if (out.includes("this.f0")) + throw new Error("decorated instance field was moved into the constructor"); + console.log("ok"); `, ], env: bunEnv, @@ -1098,10 +1120,5 @@ test("lowering many decorated instance fields into a large constructor body stay killSignal: "SIGKILL", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toMatchObject({ - stdout: expect.stringMatching(/^\{"tSmall":[\d.]+,"tLarge":[\d.]+,"ratio":[\d.]+\}\n$/), - exitCode: 0, - }); - const { tSmall, tLarge } = JSON.parse(stdout); - expect(tLarge).toBeLessThan(tSmall * 3); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "ok\n", stderr: "", exitCode: 0 }); }, 90_000); diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index bdc61e68c48d..48bf45dcc1c7 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -242,6 +242,73 @@ describe("ES Decorators", () => { expect(stdout).toBe("field x true\n10\n"); expect(exitCode).toBe(0); }); + + test("decorated instance field keeps [[Define]] semantics over an inherited accessor", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const seen = []; + function tag(_v, ctx) { seen.push("tag:" + ctx.kind + ":" + ctx.name); } + class Base { + set p(v) { seen.push("SETTER-INVOKED:" + v); } + get p() { return "getter"; } + } + class C extends Base { + @tag p = "field-init"; + q = "plain-field"; + } + const c = new C(); + console.log(JSON.stringify({ + seen, + p: c.p, + ownP: Object.prototype.hasOwnProperty.call(c, "p"), + ownQ: Object.prototype.hasOwnProperty.call(c, "q"), + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + seen: ["tag:field:p"], + p: "field-init", + ownP: true, + ownQ: true, + }); + expect(exitCode).toBe(0); + }); + + test("decorated instance field does not throw on an inherited getter-only accessor", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function tag(_v, ctx) {} + class Base { get p() { return "getter"; } } + class C extends Base { @tag p = "field-init"; } + const c = new C(); + console.log(c.p, Object.prototype.hasOwnProperty.call(c, "p")); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("field-init true\n"); + expect(exitCode).toBe(0); + }); + + test("decorated static field keeps [[Define]] semantics over an inherited static accessor", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const seen = []; + function tag(_v, ctx) { seen.push("tag:" + ctx.kind + ":" + ctx.name); } + class Base { + static set s(v) { seen.push("STATIC-SETTER:" + v); } + static get s() { return "static-getter"; } + } + class C extends Base { @tag static s = "static-init"; } + console.log(JSON.stringify({ + seen, + s: C.s, + ownS: Object.prototype.hasOwnProperty.call(C, "s"), + })); + `); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + seen: ["tag:field:s"], + s: "static-init", + ownS: true, + }); + expect(exitCode).toBe(0); + }); }); describe("non-ASCII string-literal keys", () => { @@ -252,7 +319,7 @@ describe("ES Decorators", () => { test("Bun.Transpiler output preserves the key", () => { const t = new Bun.Transpiler({ loader: "js", target: "node", minifyWhitespace: true }); const out = t.transformSync(`class A{@(() => {})\n"\\u{20BB7}\\u{91BB6}"\n}`); - // The key appears twice in the lowered output (constructor assignment and + // The key appears twice in the lowered output (__publicField call and // __decorateElement call) and must be the same string both times, either // as literal UTF-8 or as \uXXXX escapes of the correct surrogate pair. const normalized = out.replace(/\\uD842\\uDFB7\\uDA06\\uDFB6/gi, key);