diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 77c2620dac5e..e678d3ab5fce 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -344,34 +344,215 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O (((5 + 2 * idx) << 1) | 1) as f64 } - /// Emit __privateAdd for a given storage ref. + /// Pick an unused `#`-private name (suffixing on collision) and record it. + fn fresh_private_name(&self, used: &mut HashMap<&'a [u8], ()>, base: &'a [u8]) -> &'a [u8] { + let mut name = base; + let mut tries: usize = 2; + while used.contains_key(name) { + name = self.bump_name(base, Some(tries)); + tries += 1; + } + used.insert(name, ()); + name + } + + /// Method brands (`init == None`) go to `hoisted`, which runs before every field. fn emit_private_add( &mut self, is_static: bool, storage_ref: Ref, - value: Option, + init: Option, loc: bun_ast::Loc, - constructor_inject: &mut BumpVec<'_, Stmt>, - static_blocks: &mut BumpVec<'_, Property>, + used: &mut HashMap<&'a [u8], ()>, + counter: &mut usize, + hoisted: &mut BumpVec<'a, Property>, + fields: &mut BumpVec<'a, Property>, ) { let target = self.new_expr(E::This {}, loc); let storage = self.use_ref(storage_ref, loc); - let call = if let Some(v) = value { - self.call_rt(loc, b"__privateAdd", &[target, storage, v]) + let is_field = init.is_some(); + let call = match init { + Some(v) => self.call_rt(loc, b"__privateAdd", &[target, storage, v]), + None => self.call_rt(loc, b"__privateAdd", &[target, storage]), + }; + let element = if is_static { + self.make_static_block(call, loc) } else { - self.call_rt(loc, b"__privateAdd", &[target, storage]) + self.inline_brand_field(used, counter, call, loc) + }; + if is_field { fields } else { hoisted }.push(element); + } + + /// Synthetic `#__N = void ` class-body field. + fn inline_brand_field( + &mut self, + used: &mut HashMap<&'a [u8], ()>, + counter: &mut usize, + expr: Expr, + loc: bun_ast::Loc, + ) -> Property { + let base = self.bump_name(b"#__lowered_", Some(*counter)); + *counter += 1; + let name = self.fresh_private_name(used, base); + let ref_ = self.new_sym(js_ast::symbol::Kind::PrivateField, name); + let key = self.new_expr(E::PrivateIdentifier { ref_ }, loc); + let init = self.new_expr( + E::Unary { + op: js_ast::OpCode::UnVoid, + value: expr, + flags: js_ast::e::UnaryFlags::empty(), + }, + loc, + ); + Property { + key: Some(key), + initializer: Some(init), + kind: PropertyKind::Normal, + flags: js_ast::flags::PROPERTY_NONE, + ..Default::default() + } + } + + /// `accessor k = v` → a `#k_accessor_storage = v` field plus a `get k`/`set k` pair over it. + fn expand_auto_accessor( + &mut self, + prop: &Property, + used: &mut HashMap<&'a [u8], ()>, + storage_counter: &mut usize, + loc: bun_ast::Loc, + out: &mut BumpVec<'a, Property>, + ) { + let bump = self.arena; + let is_static = prop.flags.contains(Flags::Property::IsStatic); + + let storage_base: &'a [u8] = 'brk: { + if let Some(k) = prop.key { + match &k.data { + js_ast::ExprData::EString(s) + if s.is_utf8() && js_lexer::is_identifier(&s.data) => + { + let mut v = BumpVec::::new_in(bump); + v.push(b'#'); + v.extend_from_slice(&s.data); + v.extend_from_slice(b"_accessor_storage"); + break 'brk v.into_bump_slice(); + } + js_ast::ExprData::EPrivateIdentifier(pi) => { + let orig: &'a [u8] = self.symbols[pi.ref_.inner_index() as usize] + .original_name + .slice(); + break 'brk self.bump_name2(orig, b"_accessor_storage"); + } + _ => {} + } + } + let name = self.bump_name(b"#_accessor_storage", Some(*storage_counter)); + *storage_counter += 1; + name }; - if is_static { - static_blocks.push(self.make_static_block(call, loc)); + let storage_name = self.fresh_private_name(used, storage_base); + let storage_kind = if is_static { + js_ast::symbol::Kind::PrivateStaticField } else { - constructor_inject.push(self.s( - S::SExpr { - value: call, - ..Default::default() - }, + js_ast::symbol::Kind::PrivateField + }; + let storage_ref = self.new_sym(storage_kind, storage_name); + let storage_key = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, loc); + out.push(Property { + key: Some(storage_key), + initializer: prop.initializer, + kind: PropertyKind::Normal, + flags: if is_static { + Flags::Property::IsStatic.into() + } else { + js_ast::flags::PROPERTY_NONE + }, + ..Default::default() + }); + + // get k() { return this.#storage; } + let this_e = self.new_expr(E::This {}, loc); + let idx_e = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, loc); + let get_ret = self.new_expr( + E::Index { + target: this_e, + index: idx_e, + optional_chain: None, + }, + loc, + ); + let get_body = bump.alloc_slice_copy(&[self.s( + S::Return { + value: Some(get_ret), + }, + loc, + )]); + let get_fn = G::Fn { + body: G::FnBody { + stmts: bun_ast::StoreSlice::new_mut(get_body), loc, - )); - } + }, + ..Default::default() + }; + + // set k(v) { this.#storage = v; } + let setter_param_ref = self.new_sym(js_ast::symbol::Kind::Other, b"v"); + let this_e2 = self.new_expr(E::This {}, loc); + let idx_e2 = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, loc); + let lhs = self.new_expr( + E::Index { + target: this_e2, + index: idx_e2, + optional_chain: None, + }, + loc, + ); + let v_e = self.use_ref(setter_param_ref, loc); + let set_body = bump.alloc_slice_copy(&[Stmt::assign(lhs, v_e)]); + let setter_binding = self.b( + B::Identifier { + r#ref: setter_param_ref, + }, + loc, + ); + let setter_fn_args = bump.alloc(G::Arg { + binding: setter_binding, + ..Default::default() + }); + let set_fn = G::Fn { + args: bun_ast::StoreSlice::new_mut(core::slice::from_mut(setter_fn_args)), + body: G::FnBody { + stmts: bun_ast::StoreSlice::new_mut(set_body), + loc, + }, + ..Default::default() + }; + + // `get [_k = expr]()` / `set [_k](v)`: the key runs once. + let (getter_key, setter_key) = match prop.key { + Some(key) if prop.flags.contains(Flags::Property::IsComputed) => { + let key_ref = self.generate_temp_ref(Some(b"_computedKey")); + let captured = self.assign_to(key_ref, key, key.loc); + (Some(captured), Some(self.use_ref(key_ref, key.loc))) + } + key => (key, key), + }; + let mut method_flags = prop.flags; + method_flags.insert(Flags::Property::IsMethod); + out.push(Property { + key: getter_key, + value: Some(self.new_expr(E::Function { func: get_fn }, loc)), + kind: PropertyKind::Get, + flags: method_flags, + ..Default::default() + }); + out.push(Property { + key: setter_key, + value: Some(self.new_expr(E::Function { func: set_fn }, loc)), + kind: PropertyKind::Set, + flags: method_flags, + ..Default::default() + }); } /// Get the method kind code (1=method, 2=getter, 3=setter). @@ -1197,6 +1378,95 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } + // Pre-scan: determine if all private members need lowering. + let mut lower_all_private = false; + { + let mut has_any_private = false; + let mut has_any_decorated = false; + let cprops: &[Property] = class.properties.slice(); + for cprop in cprops.iter() { + if cprop.kind == PropertyKind::ClassStaticBlock { + continue; + } + if cprop.ts_decorators.len_u32() > 0 { + has_any_decorated = true; + if cprop.key.is_some() + && matches!( + cprop.key.unwrap().data, + js_ast::ExprData::EPrivateIdentifier(_) + ) + { + lower_all_private = true; + } + } + if cprop.key.is_some() + && matches!( + cprop.key.unwrap().data, + js_ast::ExprData::EPrivateIdentifier(_) + ) + { + has_any_private = true; + } + } + if !lower_all_private && has_any_private && has_any_decorated { + lower_all_private = true; + } + } + + // `#`-names in scope here, which a generated private must not shadow. + let mut class_private_names: HashMap<&'a [u8], ()> = HashMap::default(); + for cprop in class.properties.slice() { + if let Some(k) = cprop.key + && let js_ast::ExprData::EPrivateIdentifier(pi) = &k.data + { + let name: &'a [u8] = p.symbols[pi.ref_.inner_index() as usize] + .original_name + .slice(); + class_private_names.insert(name, ()); + } + } + let mut scope = p.current_scope; + loop { + if scope.kind == js_ast::scope::Kind::ClassBody { + for member in scope.members.values() { + let name: &'a [u8] = p.symbols[member.ref_.inner_index() as usize] + .original_name + .slice(); + if name.first() == Some(&b'#') { + class_private_names.insert(name, ()); + } + } + } + let Some(parent) = scope.parent else { break }; + scope = parent; + } + + // Must precede Phase 2, which indexes `class.properties` by position. + let mut accessor_storage_counter: usize = 0; + if class + .properties + .slice() + .iter() + .any(|cp| cp.kind == PropertyKind::AutoAccessor && cp.ts_decorators.len_u32() == 0) + { + let old: &[Property] = class.properties.slice(); + let mut expanded = BumpVec::::with_capacity_in(old.len() + 2, bump); + for cprop in old.iter() { + if cprop.kind == PropertyKind::AutoAccessor && cprop.ts_decorators.len_u32() == 0 { + p.expand_auto_accessor( + cprop, + &mut class_private_names, + &mut accessor_storage_counter, + loc, + &mut expanded, + ); + } else { + expanded.push(prop_full_copy(cprop)); + } + } + class.properties = bun_ast::StoreSlice::new_mut(expanded.into_bump_slice_mut()); + } + // ── Phase 2: Pre-evaluate decorators/keys ──────── let mut dec_counter: usize = 0; let mut class_dec_ref: Option = None; @@ -1389,45 +1659,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O BumpVec::>::new_in(bump); let mut prefix_stmts = BumpVec::::new_in(bump); let mut private_lowered_map: PrivateLoweredMap = PrivateLoweredMap::default(); - let mut accessor_storage_counter: usize = 0; let mut emitted_private_adds: HashMap = HashMap::default(); - let mut static_private_add_blocks = BumpVec::::new_in(bump); - - // Pre-scan: determine if all private members need lowering - let mut lower_all_private = false; - { - let mut has_any_private = false; - let mut has_any_decorated = false; - let cprops: &[Property] = class.properties.slice(); - for cprop in cprops.iter() { - if cprop.kind == PropertyKind::ClassStaticBlock { - continue; - } - if cprop.ts_decorators.len_u32() > 0 { - has_any_decorated = true; - if cprop.key.is_some() - && matches!( - cprop.key.unwrap().data, - js_ast::ExprData::EPrivateIdentifier(_) - ) - { - lower_all_private = true; - break; - } - } - if cprop.key.is_some() - && matches!( - cprop.key.unwrap().data, - js_ast::ExprData::EPrivateIdentifier(_) - ) - { - has_any_private = true; - } - } - if !lower_all_private && has_any_private && has_any_decorated { - lower_all_private = true; - } - } + let mut hoisted_brands = BumpVec::::new_in(bump); + let mut brand_field_counter: usize = 0; let props_slice2: &mut [Property] = class.properties.slice_mut(); for (prop_idx, prop) in props_slice2.iter_mut().enumerate() { @@ -1437,7 +1671,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O && let Some(nk_expr) = prop.key && matches!(nk_expr.data, js_ast::ExprData::EPrivateIdentifier(_)) && prop.kind != PropertyKind::ClassStaticBlock - && prop.kind != PropertyKind::AutoAccessor { let npriv_ref = match &nk_expr.data { js_ast::ExprData::EPrivateIdentifier(pi) => pi.ref_, @@ -1506,8 +1739,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ws_ref, None, loc, - &mut constructor_inject_stmts, - &mut static_private_add_blocks, + &mut class_private_names, + &mut brand_field_counter, + &mut hoisted_brands, + &mut new_properties, ); } continue; @@ -1522,133 +1757,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let init_val = prop .initializer .unwrap_or_else(|| p.new_expr(E::Undefined {}, loc)); - let this_e = p.new_expr(E::This {}, loc); - let wm_e = p.use_ref(wm_ref, loc); - let call = p.call_rt(loc, b"__privateAdd", &[this_e, wm_e, init_val]); - if !prop.flags.contains(Flags::Property::IsStatic) { - constructor_inject_stmts.push(p.s( - S::SExpr { - value: call, - ..Default::default() - }, - loc, - )); - } else { - static_private_add_blocks.push(p.make_static_block(call, loc)); - } - continue; - } - } - // Undecorated auto-accessor → WeakMap + getter/setter - if prop.kind == PropertyKind::AutoAccessor { - let accessor_name: &'a [u8] = 'brk: { - if let Some(k) = prop.key { - if let js_ast::ExprData::EString(s) = &k.data - && s.is_utf8() - { - break 'brk p.bump_name2(b"_", &s.data); - } - } - let name = - p.bump_name(b"_accessor_storage", Some(accessor_storage_counter)); - accessor_storage_counter += 1; - name - }; - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, accessor_name); - let wme = p.new_weak_map_expr(loc); - prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); - - // Getter: get foo() { return __privateGet(this, _foo); } - let this_e = p.new_expr(E::This {}, loc); - let wm_e = p.use_ref(wm_ref, loc); - let get_ret = p.call_rt(loc, b"__privateGet", &[this_e, wm_e]); - let get_body = bump.alloc_slice_copy(&[p.s( - S::Return { - value: Some(get_ret), - }, - loc, - )]); - let get_fn = G::Fn { - body: G::FnBody { - stmts: bun_ast::StoreSlice::new_mut(get_body), - loc, - }, - ..Default::default() - }; - - // Setter: set foo(v) { __privateSet(this, _foo, v); } - let setter_param_ref = p.new_sym(js_ast::symbol::Kind::Other, b"v"); - let this_e2 = p.new_expr(E::This {}, loc); - let wm_e2 = p.use_ref(wm_ref, loc); - let v_e = p.use_ref(setter_param_ref, loc); - let set_call = p.call_rt(loc, b"__privateSet", &[this_e2, wm_e2, v_e]); - let set_body = bump.alloc_slice_copy(&[p.s( - S::SExpr { - value: set_call, - ..Default::default() - }, - loc, - )]); - let setter_binding = p.b( - B::Identifier { - r#ref: setter_param_ref, - }, - loc, - ); - let setter_fn_args = bump.alloc(G::Arg { - binding: setter_binding, - ..Default::default() - }); - let set_fn = G::Fn { - args: bun_ast::StoreSlice::new_mut(core::slice::from_mut(setter_fn_args)), - body: G::FnBody { - stmts: bun_ast::StoreSlice::new_mut(set_body), + p.emit_private_add( + prop.flags.contains(Flags::Property::IsStatic), + wm_ref, + Some(init_val), loc, - }, - ..Default::default() - }; - - let mut getter_flags = prop.flags; - getter_flags.insert(Flags::Property::IsMethod); - new_properties.push(Property { - key: prop.key, - value: Some(p.new_expr(E::Function { func: get_fn }, loc)), - kind: PropertyKind::Get, - flags: getter_flags, - ..Default::default() - }); - new_properties.push(Property { - key: prop.key, - value: Some(p.new_expr(E::Function { func: set_fn }, loc)), - kind: PropertyKind::Set, - flags: getter_flags, - ..Default::default() - }); - - let init_val = prop - .initializer - .unwrap_or_else(|| p.new_expr(E::Undefined {}, loc)); - if !prop.flags.contains(Flags::Property::IsStatic) { - let this_e3 = p.new_expr(E::This {}, loc); - let wm_e3 = p.use_ref(wm_ref, loc); - let call = p.call_rt(loc, b"__privateAdd", &[this_e3, wm_e3, init_val]); - constructor_inject_stmts.push(p.s( - S::SExpr { - value: call, - ..Default::default() - }, - loc, - )); - } else { - 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( - loc, - b"__privateAdd", - &[cn_e, wm_e3, init_val], - )); + &mut class_private_names, + &mut brand_field_counter, + &mut hoisted_brands, + &mut new_properties, + ); + continue; } - continue; } // Static blocks → extract to suffix if prop.kind == PropertyKind::ClassStaticBlock { @@ -1794,6 +1914,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let accessor_name: &'a [u8] = 'brk: { if let js_ast::ExprData::EString(s) = &key_expr.data && s.is_utf8() + && js_lexer::is_identifier(&s.data) { break 'brk p.bump_name2(b"_", &s.data); } @@ -1950,8 +2071,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O private_storage_ref.unwrap(), None, loc, - &mut constructor_inject_stmts, - &mut static_private_add_blocks, + &mut class_private_names, + &mut brand_field_counter, + &mut hoisted_brands, + &mut new_properties, ); } if prop.flags.contains(Flags::Property::IsStatic) { @@ -1972,12 +2095,20 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } + if !hoisted_brands.is_empty() { + hoisted_brands.extend(new_properties.drain(..)); + new_properties = hoisted_brands; + } + // ── Phase 5: Rewrite private accesses ──────────── if !private_lowered_map.is_empty() { for nprop in new_properties.iter_mut() { if let Some(v) = &mut nprop.value { p.rewrite_private_accesses_in_expr(v, &private_lowered_map); } + if let Some(ini) = &mut nprop.initializer { + p.rewrite_private_accesses_in_expr(ini, &private_lowered_map); + } if let Some(sb) = nprop.class_static_block_mut() { p.rewrite_private_accesses_in_stmts(sb.stmts.slice_mut(), &private_lowered_map); } @@ -2407,21 +2538,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - // Static private __privateAdd blocks at beginning - if !static_private_add_blocks.is_empty() { - let mut merged = BumpVec::::with_capacity_in( - static_private_add_blocks.len() + new_properties.len(), - bump, - ); - for sp in static_private_add_blocks.drain(..) { - merged.push(sp); - } - for np in new_properties.drain(..) { - merged.push(np); - } - new_properties = merged; - } - class.properties = bun_ast::StoreSlice::new_mut(new_properties.into_bump_slice_mut()); class.has_decorators = false; class.should_lower_standard_decorators = false; diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index bdc61e68c48d..099766b7f036 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -1210,4 +1210,369 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + describe("undecorated auto-accessor lowering", () => { + test("initializer runs in source order with sibling fields", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const log = []; + const mk = (name, v) => (log.push(name), v); + class C { + a = mk("a", 1); + accessor b = mk("b", 2); + c = mk("c", this.b); + } + const c = new C(); + console.log(log.join(",")); + console.log(c.a, c.b, c.c); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a,b,c\n1 2 2\n"); + expect(exitCode).toBe(0); + }); + + test("static accessor initializer runs in source order with sibling static fields", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const log = []; + const mk = (name, v) => (log.push(name), v); + class C { + static a = mk("a", 1); + static accessor b = mk("b", 2); + static c = mk("c", C.b); + } + console.log(log.join(",")); + console.log(C.a, C.b, C.c); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a,b,c\n1 2 2\n"); + expect(exitCode).toBe(0); + }); + + test("subclass may declare an accessor with the same name as its base", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class Base { accessor v = 1; } + class Sub extends Base { accessor v = 2; } + const s = new Sub(); + console.log(s.v); + s.v = 99; + console.log(s.v); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("2\n99\n"); + expect(exitCode).toBe(0); + }); + + test("two classes in a file may each declare a same-named accessor", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class A { accessor x = 1; } + class B { accessor x = 2; } + const a = new A(), b = new B(); + a.x = 10; + console.log(a.x, b.x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("10 2\n"); + expect(exitCode).toBe(0); + }); + + test("instance and static accessors may share a name", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class C { + accessor x = 1; + static accessor x = 2; + } + const c = new C(); + c.x = 10; + C.x = 20; + console.log(c.x, C.x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("10 20\n"); + expect(exitCode).toBe(0); + }); + + test("generated storage name avoids existing private names", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class C { + #y_accessor_storage = "user"; + accessor y = "acc"; + read() { return this.#y_accessor_storage; } + } + const c = new C(); + console.log(c.y, c.read()); + c.y = "changed"; + console.log(c.y, c.read()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("acc user\nchanged user\n"); + expect(exitCode).toBe(0); + }); + + test("generated storage name does not shadow a private referenced from an enclosing class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class Outer { + #x_accessor_storage = 42; + make() { + const outer = this; + return class Inner { + accessor x = 1; + readOuter() { return outer.#x_accessor_storage; } + }; + } + } + const Inner = new Outer().make(); + const i = new Inner(); + console.log(i.readOuter(), i.x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("42 1\n"); + expect(exitCode).toBe(0); + }); + + test("accessor with a private-name key round-trips", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class C { + accessor #z = 5; + get z() { return this.#z; } + set z(v) { this.#z = v; } + } + const c = new C(); + console.log(c.z); + c.z = 50; + console.log(c.z); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("5\n50\n"); + expect(exitCode).toBe(0); + }); + + test("static accessor with a private-name key round-trips", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class C { + static accessor #z = 5; + static get z() { return C.#z; } + static set z(v) { C.#z = v; } + } + console.log(C.z); + C.z = 50; + console.log(C.z); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("5\n50\n"); + expect(exitCode).toBe(0); + }); + + test("private-name accessor is lowered with its siblings when the class WeakMap-lowers its privates", async () => { + // #helper's body is hoisted out of the class, so its this.#z must be + // rewritten along with every other lowered private. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + class C { + @dec m() {} + accessor #z = 5; + #helper() { return this.#z; } + bump() { this.#z = this.#z + 1; return this.#helper(); } + static accessor #s = 10; + static #sh() { return C.#s; } + static readS() { C.#s = C.#s * 2; return C.#sh(); } + } + console.log(new C().bump(), C.readS()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("6 20\n"); + expect(exitCode).toBe(0); + }); + + test("accessor with a non-identifier string key round-trips", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + class C { + accessor "a-b" = 1; + static accessor "c d" = 2; + } + const c = new C(); + c["a-b"] = 10; + C["c d"] = 20; + console.log(c["a-b"], C["c d"]); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("10 20\n"); + expect(exitCode).toBe(0); + }); + + test("decorated accessor with a non-identifier string key round-trips", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const seen = []; + function dec(target, ctx) { seen.push(ctx.name); } + class C { + @dec accessor "a-b" = 1; + @dec static accessor "c d" = 2; + } + const c = new C(); + c["a-b"] = 10; + C["c d"] = 20; + console.log(seen.sort().join(","), c["a-b"], C["c d"]); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a-b,c d 10 20\n"); + expect(exitCode).toBe(0); + }); + + test("computed accessor key is evaluated once, in source order, and shared by get and set", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const order = []; + const k = (name) => (order.push(name), name); + class C { + [k("m")]() {} + accessor [k("a")] = 1; + static accessor [k("s")] = 2; + } + console.log(order.join(",")); + const c = new C(); + c.a = 10; + C.s = 20; + const d = Object.getOwnPropertyDescriptor(C.prototype, "a"); + console.log(c.a, C.s, typeof d.get, typeof d.set); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("m,a,s\n10 20 function function\n"); + expect(exitCode).toBe(0); + }); + + test("instance fields run in source order when sibling #-privates are WeakMap-lowered", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + const log = []; + class C { + @dec m() {} + #a = (log.push("a"), 1); + accessor b = (log.push("b"), this.readA()); + c = (log.push("c"), this.b); + readA() { return this.#a; } + } + const o = new C(); + console.log(log.join(","), o.b, o.c, o.readA()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a,b,c 1 1 1\n"); + expect(exitCode).toBe(0); + }); + + test("decorated instance #-private method's brand is installed before a following undecorated #-private field reads it", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + class C { + @dec #m() { return 1; } + #a = this.readM(); + readM() { return this.#m(); } + } + console.log(new C().readM()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("1\n"); + expect(exitCode).toBe(0); + }); + + test("lowered #-method and #-getter brands are installed before any field, while #-field brands keep source order", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + const log = []; + class C { + @dec y; + a = (log.push("a"), this.#m() + this.#g); + #f = (log.push("f"), 10); + b = (log.push("b"), this.#f); + #m() { return 1; } + get #g() { return 100; } + @dec #dm() { return 1000; } + c = this.#dm(); + } + const o = new C(); + console.log(log.join(","), o.a, o.b, o.c); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a,f,b 101 10 1000\n"); + expect(exitCode).toBe(0); + }); + + test("the same holds for static members: lowered static accessor storage and #-fields keep source order", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + const log = []; + const mk = (n, v) => (log.push(n), v); + class C { + @dec m() {} + #p; + static a = mk("a", 1); + static accessor b = mk("b", C.a); + static #c = mk("c", C.b + 1); + static d = mk("d", C.#c); + static #sm() { return 1; } + static e = mk("e", C.#sm()); + } + console.log(log.join(","), C.b, C.d, C.e); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a,b,c,d,e 1 2 1\n"); + expect(exitCode).toBe(0); + }); + + test("undecorated field, accessor and #-field initializers may read a WeakMap-lowered #-private directly", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + class C { + @dec m() {} + #a = 1; + b = this.#a; + accessor c = this.#a + 10; + #d = this.#a + 100; + readD() { return this.#d; } + } + const o = new C(); + console.log(o.b, o.c, o.readD()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("1 11 101\n"); + expect(exitCode).toBe(0); + }); + + test("undecorated static #-field initializer may read a WeakMap-lowered static #-private directly", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + class C { + @dec m() {} + static #a = 1; + static #b = C.#a + 1; + static readB() { return C.#b; } + } + console.log(C.readB()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("2\n"); + expect(exitCode).toBe(0); + }); + + test("WeakMap-lowered #-private does not strip NamedEvaluation or new.target from sibling fields", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(v) { return v; } + class C { + @dec m() {} + #p; + onClick = () => {}; + nt = new.target; + } + const c = new C(); + console.log(c.onClick.name, c.nt === undefined); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("onClick true\n"); + expect(exitCode).toBe(0); + }); + + test("storage is a class-body private field, not a module-scope WeakMap", () => { + const transpiler = new Bun.Transpiler({ loader: "js", target: "bun" }); + const out = transpiler.transformSync(`class C { accessor x = 1; }`); + expect(out).toContain("#x_accessor_storage"); + expect(out).not.toContain("WeakMap"); + expect(out).not.toContain("__privateAdd"); + }); + }); });