diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 3beff11a4014..375f6839b631 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -1970,6 +1970,7 @@ impl Data { properties, has_decorators: el.has_decorators, should_lower_standard_decorators: el.should_lower_standard_decorators, + ts_decorators_use_private_names: el.ts_decorators_use_private_names, }); Ok(Data::EClass(StoreRef::from_bump(item))) } diff --git a/src/ast/g.rs b/src/ast/g.rs index 96f0a28d6390..4ccce4fcfec8 100644 --- a/src/ast/g.rs +++ b/src/ast/g.rs @@ -63,6 +63,8 @@ pub struct Class { pub properties: StoreSlice, pub has_decorators: bool, pub should_lower_standard_decorators: bool, + /// A member or parameter decorator reads a `#private` name of this class. + pub ts_decorators_use_private_names: bool, } impl Default for Class { @@ -77,6 +79,7 @@ impl Default for Class { properties: StoreSlice::EMPTY, has_decorators: false, should_lower_standard_decorators: false, + ts_decorators_use_private_names: false, } } } diff --git a/src/ast/lib.rs b/src/ast/lib.rs index 3456860e472a..8e34ab3656c5 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -2945,6 +2945,8 @@ pub mod flags { IsStatic, WasShorthand, IsSpread, + /// The getter a TypeScript `accessor` member was lowered to (keeps its decorators). + IsAutoAccessorGetter, } pub type PropertySet = EnumSet; pub const PROPERTY_NONE: PropertySet = EnumSet::empty(); diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 77c2620dac5e..6f6c7dec8a1e 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -121,6 +121,7 @@ fn class_copy(c: &G::Class) -> G::Class { properties: c.properties, has_decorators: c.has_decorators, should_lower_standard_decorators: c.should_lower_standard_decorators, + ts_decorators_use_private_names: c.ts_decorators_use_private_names, } } @@ -273,7 +274,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } /// Create a static block property from a single expression. - fn make_static_block(&mut self, expr: Expr, l: bun_ast::Loc) -> Property { + pub(crate) fn make_static_block(&mut self, expr: Expr, l: bun_ast::Loc) -> Property { let bump = self.arena; let stmt = self.s( S::SExpr { @@ -890,7 +891,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O /// Drain receiver-capture temporaries created past `baseline` into a /// single `var` declaration statement; `None` if none were created. - fn drain_capture_temp_decls(&mut self, baseline: usize, loc: bun_ast::Loc) -> Option { + pub(crate) fn drain_capture_temp_decls( + &mut self, + baseline: usize, + loc: bun_ast::Loc, + ) -> Option { let total = self.temp_refs_to_declare.len(); if total == baseline { return None; diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 12d485079d93..3bab2fe87d05 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -536,6 +536,11 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub(crate) temp_refs_to_declare: List<'a, TempRef>, pub(crate) temp_ref_count: i32, + /// The class body scope while its TypeScript decorators are visited. + pub(crate) ts_decorator_class_scope: Option>, + /// Class body scopes with a decorator that read one of their `#private` names. + pub(crate) ts_decorator_scopes_using_private_names: List<'a, js_ast::StoreRef>, + // When bundling, hoisted top-level local variables declared with "var" in // nested scopes are moved up to be declared in the top-level scope instead. // The old "var" statements are turned into regular assignments instead. This @@ -6376,17 +6381,26 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O return self.arena.alloc_slice_copy(&[stmt]); } } + let class_loc = stmt.loc; let mut constructor_function: Option> = None; 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); + let temp_refs_before = self.temp_refs_to_declare.len(); + for prop in s_class.class.properties.slice_mut().iter_mut() { + if prop.kind == PropertyKind::ClassStaticBlock { + class_properties.push(core::mem::take(prop)); + continue; + } + + let is_method = prop.flags.contains(Flags::Property::IsMethod); + let is_static = prop.flags.contains(Flags::Property::IsStatic); + // merge parameter decorators with method decorators - if prop.flags.contains(Flags::Property::IsMethod) { + if is_method { if let Some(prop_value) = prop.value { match prop_value.data { js_ast::ExprData::EFunction(func) => { @@ -6429,44 +6443,45 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } - // TODO: prop.kind == .declare and prop.value == null + if prop.ts_decorators.len_u32() == 0 { + class_properties.push(core::mem::take(prop)); + continue; + } - if prop.ts_decorators.len_u32() > 0 { - let descriptor_key = prop.key.expect("infallible: prop has key"); - let loc = descriptor_key.loc; + let key_loc = prop.key.expect("infallible: prop has key").loc; + let (key_for_reuse, captured_key) = self.capture_computed_key(prop); - // TODO: when we have the `accessor` modifier, add `and !prop.flags.contains(.has_accessor_modifier)` to - // the if statement. - let descriptor_kind: Expr = - if !prop.flags.contains(Flags::Property::IsMethod) { - self.new_expr(E::Undefined {}, loc) - } else { - self.new_expr(E::Null {}, loc) - }; + { + // Only a field has no property descriptor to pass along. + let descriptor_kind: Expr = if is_method { + self.new_expr(E::Null {}, key_loc) + } else { + self.new_expr(E::Undefined {}, key_loc) + }; let class_name = s_class.class.class_name.unwrap(); let class_ref = class_name.ref_; - let target: Expr = if prop.flags.contains(Flags::Property::IsStatic) { - self.record_usage(class_ref); - self.new_expr(E::Identifier::init(class_ref), class_name.loc) + self.record_usage(class_ref); + let class_ident = + self.new_expr(E::Identifier::init(class_ref), class_name.loc); + let target: Expr = if is_static { + class_ident } else { - let inner = - self.new_expr(E::Identifier::init(class_ref), class_name.loc); self.new_expr( E::Dot { - target: inner, + target: class_ident, name: b"prototype".into(), - name_loc: loc, + name_loc: key_loc, ..Default::default() }, - loc, + key_loc, ) }; let mut array = BumpVec::::new_in(self.arena); if self.options.features.emit_decorator_metadata { - self.emit_decorator_metadata_for_prop(prop, &mut array, loc); + self.emit_decorator_metadata_for_prop(prop, &mut array, key_loc); } let mut full = BumpVec::::with_capacity_in( @@ -6481,21 +6496,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O items: full_items, ..Default::default() }, - loc, + key_loc, ); let args_slice = self.arena.alloc_slice_copy(&[ array_expr, target, - descriptor_key, + key_for_reuse, descriptor_kind, ]); let args = ExprNodeList::from_arena_slice(args_slice); - let decorator = self.call_runtime( - prop.key.expect("infallible: prop has key").loc, - b"__legacyDecorateClassTS", - args, - ); + let decorator = + self.call_runtime(key_loc, b"__legacyDecorateClassTS", args); let decorator_stmt = self.s( S::SExpr { value: decorator, @@ -6504,200 +6516,63 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O decorator.loc, ); - if prop.flags.contains(Flags::Property::IsStatic) { + if is_static { static_decorators.push(decorator_stmt); } else { instance_decorators.push(decorator_stmt); } } - 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)); + // A "declare" or "abstract" member emits nothing but its key. + if matches!(prop.kind, PropertyKind::Declare | PropertyKind::Abstract) { + if let Some(captured_key) = captured_key { + class_properties.push(self.make_static_block(captured_key, key_loc)); } continue; } - // The old backing slice is overwritten right after this loop, so - // `take` is fine here (Property: Default). class_properties.push(core::mem::take(prop)); } + // A decorator that reads a `#private` name only works inside the body. + let decorators_in_static_block = s_class.class.ts_decorators_use_private_names + && (!instance_decorators.is_empty() || !static_decorators.is_empty()); + if decorators_in_static_block { + let mut block_stmts = BumpVec::::with_capacity_in( + instance_decorators.len() + static_decorators.len(), + self.arena, + ); + block_stmts.extend_from_slice(&instance_decorators); + block_stmts.extend_from_slice(&static_decorators); + let static_block = self.arena.alloc(G::ClassStaticBlock { + loc: s_class.class.close_brace_loc, + stmts: bun_alloc::AstVec::::from_bump_vec(block_stmts), + }); + class_properties.push(G::Property { + kind: PropertyKind::ClassStaticBlock, + class_static_block: Some(bun_ast::StoreRef::from_bump(static_block)), + ..Default::default() + }); + } + 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(); + 2 + 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); + if let Some(temp_decls) = self.drain_capture_temp_decls(temp_refs_before, class_loc) + { + stmts.push(temp_decls); + } stmts.push(stmt); - stmts.extend_from_slice(&static_members); - stmts.extend_from_slice(&instance_decorators); - stmts.extend_from_slice(&static_decorators); + if !decorators_in_static_block { + stmts.extend_from_slice(&instance_decorators); + stmts.extend_from_slice(&static_decorators); + } if s_class.class.ts_decorators.len_u32() > 0 { let mut array: Vec = s_class.class.ts_decorators.move_to_list_managed(); @@ -6774,6 +6649,270 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } + /// Called for every resolved `obj.#name` and `#name in obj`. + pub(crate) fn note_private_name_use(&mut self, name: &[u8], resolved: Ref) { + if let Some(class_scope) = self.ts_decorator_class_scope + && class_scope + .members + .get(name) + .is_some_and(|member| member.ref_ == resolved) + && !self + .ts_decorator_scopes_using_private_names + .contains(&class_scope) + { + self.ts_decorator_scopes_using_private_names + .push(class_scope); + } + } + + /// `accessor` members under `experimentalDecorators`, before the useDefineForClassFields pass. + pub(crate) fn lower_ts_auto_accessors(&mut self, class: &mut G::Class) { + use js_ast::g::PropertyKind; + if !class + .properties + .iter() + .any(|p| p.kind == PropertyKind::AutoAccessor) + { + return; + } + + // A computed key temporary is a `var` in the scope around the class. + let class_body_scope = self.current_scope; + let mut enclosing_scope = class_body_scope; + while matches!( + enclosing_scope.kind, + js_ast::scope::Kind::ClassBody | js_ast::scope::Kind::ClassName + ) && let Some(parent) = enclosing_scope.parent + { + enclosing_scope = parent; + } + let temp_refs_before = self.temp_refs_to_declare.len(); + let mut storage_names = self.private_names_in_class(class); + let mut properties = + BumpVec::::with_capacity_in(class.properties.len() + 2, self.arena); + for prop in class.properties.slice_mut().iter_mut() { + if prop.kind != PropertyKind::AutoAccessor { + properties.push(core::mem::take(prop)); + continue; + } + self.current_scope = enclosing_scope; + let (key_for_reuse, _) = self.capture_computed_key(prop); + self.current_scope = class_body_scope; + self.lower_ts_auto_accessor(prop, key_for_reuse, &mut storage_names, &mut properties); + } + class.properties = bun_ast::StoreSlice::new_mut(properties.into_bump_slice_mut()); + + if let Some(temp_decls) = self.drain_capture_temp_decls(temp_refs_before, class.body_loc) { + self.nearest_stmt_list_mut() + .expect("visit_class runs while a statement list is visited") + .push(temp_decls); + } + } + + /// The `#names` a class body declares, so generated storage names stay unique. + fn private_names_in_class(&self, class: &G::Class) -> BumpVec<'a, &'a [u8]> { + let mut names = BumpVec::<&'a [u8]>::new_in(self.arena); + for prop in class.properties.iter() { + if let Some(js_ast::ExprData::EPrivateIdentifier(private)) = prop.key.map(|k| k.data) { + names.push(self.load_name_from_ref(private.ref_)); + } + } + names + } + + /// `[expr]` becomes `[_key = expr]`. Returns the key for other uses and the assignment. + fn capture_computed_key(&mut self, prop: &mut G::Property) -> (Expr, Option) { + let key = prop.key.expect("infallible: prop has key"); + if !prop.flags.contains(Flags::Property::IsComputed) + || matches!( + key.data, + js_ast::ExprData::EString(_) | js_ast::ExprData::ENumber(_) + ) + { + return (key, None); + } + // The `accessor` lowering in `visit_class` captured this key already. + if prop.flags.contains(Flags::Property::IsAutoAccessorGetter) + && let js_ast::ExprData::EBinary(binary) = key.data + && binary.op == js_ast::OpCode::BinAssign + && let js_ast::ExprData::EIdentifier(temp) = binary.left.data + { + self.record_usage(temp.ref_); + return (binary.left, None); + } + let temp_ref = self.generate_temp_ref(Some(b"_key")); + self.record_declared_symbol(temp_ref); + self.record_usage(temp_ref); + self.record_usage(temp_ref); + let temp_ident = self.new_expr(E::Identifier::init(temp_ref), key.loc); + let assign = Expr::assign(temp_ident, key); + prop.key = Some(assign); + ( + self.new_expr(E::Identifier::init(temp_ref), key.loc), + Some(assign), + ) + } + + /// `accessor x = init` becomes `#x_accessor_storage = init` plus a getter and a setter. + fn lower_ts_auto_accessor( + &mut self, + prop: &mut G::Property, + key_for_reuse: Expr, + storage_names: &mut BumpVec<'a, &'a [u8]>, + out: &mut BumpVec<'a, G::Property>, + ) { + use js_ast::g::PropertyKind; + + let key = prop.key.expect("infallible: prop has key"); + let key_loc = key.loc; + let is_static = prop.flags.contains(Flags::Property::IsStatic); + + let base: &'a [u8] = match key.data { + js_ast::ExprData::EString(s) + if s.is_utf8() + && !prop.flags.contains(Flags::Property::IsComputed) + && js_lexer::is_identifier(s.slice8()) => + { + s.string(self.arena).expect("oom") + } + js_ast::ExprData::EPrivateIdentifier(private) => { + &self.load_name_from_ref(private.ref_)[1..] + } + _ => b"", + }; + let mut storage_name: &'a [u8] = + bun_alloc::arena_format!(in self.arena, "#{}_accessor_storage", bstr::BStr::new(base)) + .into_bump_str() + .as_bytes(); + let mut suffix = 1; + while storage_names.contains(&storage_name) { + storage_name = bun_alloc::arena_format!( + in self.arena, + "#{}_accessor_storage_{}", + bstr::BStr::new(base), + suffix + ) + .into_bump_str() + .as_bytes(); + suffix += 1; + } + storage_names.push(storage_name); + + let storage_kind = if is_static { + js_ast::symbol::Kind::PrivateStaticField + } else { + js_ast::symbol::Kind::PrivateField + }; + let storage_ref = self.new_symbol(storage_kind, storage_name); + let storage_key = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, key_loc); + + let mut storage_flags = Flags::PropertySet::empty(); + if is_static { + storage_flags.insert(Flags::Property::IsStatic); + } + out.push(G::Property { + kind: PropertyKind::Normal, + flags: storage_flags, + key: Some(storage_key), + initializer: prop.initializer.take(), + ..Default::default() + }); + + // get x() { return this.#x_accessor_storage; } + let this_expr = self.new_expr(E::This {}, key_loc); + let storage_index = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, key_loc); + let get_value = self.new_expr( + E::Index { + target: this_expr, + index: storage_index, + optional_chain: None, + }, + key_loc, + ); + let get_body = self.arena.alloc_slice_copy(&[self.s( + S::Return { + value: Some(get_value), + }, + key_loc, + )]); + let get_fn = self.new_expr( + E::Function { + func: G::Fn { + body: G::FnBody { + stmts: bun_ast::StoreSlice::new_mut(get_body), + loc: key_loc, + }, + open_parens_loc: key_loc, + ..Default::default() + }, + }, + key_loc, + ); + + // set x(v) { this.#x_accessor_storage = v; } + let setter_arg_ref = self.new_symbol(js_ast::symbol::Kind::Other, b"v"); + let this_expr = self.new_expr(E::This {}, key_loc); + let storage_index = self.new_expr(E::PrivateIdentifier { ref_: storage_ref }, key_loc); + let set_target = self.new_expr( + E::Index { + target: this_expr, + index: storage_index, + optional_chain: None, + }, + key_loc, + ); + self.record_usage(setter_arg_ref); + let set_value = self.new_expr(E::Identifier::init(setter_arg_ref), key_loc); + let set_body = self + .arena + .alloc_slice_copy(&[Stmt::assign(set_target, set_value)]); + let setter_binding = self.b( + B::Identifier { + r#ref: setter_arg_ref, + }, + key_loc, + ); + let setter_args = self.arena.alloc(G::Arg { + binding: setter_binding, + ..Default::default() + }); + let set_fn = self.new_expr( + E::Function { + func: G::Fn { + args: bun_ast::StoreSlice::new_mut(core::slice::from_mut(setter_args)), + body: G::FnBody { + stmts: bun_ast::StoreSlice::new_mut(set_body), + loc: key_loc, + }, + open_parens_loc: key_loc, + ..Default::default() + }, + }, + key_loc, + ); + + let mut accessor_flags = prop.flags; + accessor_flags.insert(Flags::Property::IsMethod); + let mut getter_flags = accessor_flags; + getter_flags.insert(Flags::Property::IsAutoAccessorGetter); + out.push(G::Property { + kind: PropertyKind::Get, + flags: getter_flags, + key: prop.key, + value: Some(get_fn), + ts_decorators: bun_alloc::AstAlloc::take(&mut prop.ts_decorators), + ts_metadata: prop.ts_metadata.clone(), + ..Default::default() + }); + out.push(G::Property { + kind: PropertyKind::Set, + flags: accessor_flags, + key: Some(key_for_reuse), + value: Some(set_fn), + ..Default::default() + }); + } + // Helper extracted from lower_class to keep that fn readable; condenses // the per-kind metadata switch. #[cold] @@ -6799,7 +6938,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } match prop.kind { - PropertyKind::Normal | PropertyKind::Abstract => { + PropertyKind::Normal | PropertyKind::Abstract | PropertyKind::Declare => { { // design:type let v = self @@ -6844,6 +6983,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } } + PropertyKind::Get if prop.flags.contains(Flags::Property::IsAutoAccessorGetter) => { + // typescript only sets design:type for an auto-accessor. + let v = self + .serialize_metadata(prop.ts_metadata.clone()) + .expect("unreachable"); + push_metadata!(b"design:type", v); + } PropertyKind::Get => { if prop.flags.contains(Flags::Property::IsMethod) { // typescript sets design:type to the return value & design:paramtypes to []. @@ -6911,8 +7057,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } } - PropertyKind::Spread | PropertyKind::Declare | PropertyKind::AutoAccessor => {} // not allowed in a class (auto_accessor is standard decorators only) - PropertyKind::ClassStaticBlock => {} // not allowed to decorate this + PropertyKind::Spread | PropertyKind::AutoAccessor => {} // lowered before this runs + PropertyKind::ClassStaticBlock => {} // not allowed to decorate this } } @@ -8733,6 +8879,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O await_target: None, temp_refs_to_declare: BumpVec::new_in(arena), temp_ref_count: 0, + ts_decorator_class_scope: None, + ts_decorator_scopes_using_private_names: BumpVec::new_in(arena), relocated_top_level_vars: BumpVec::new_in(arena), after_arrow_body_loc: bun_ast::Loc::EMPTY, const_values: Default::default(), diff --git a/src/js_parser/parse/mod.rs b/src/js_parser/parse/mod.rs index 7b3519a9ba8a..9108494bd0c3 100644 --- a/src/js_parser/parse/mod.rs +++ b/src/js_parser/parse/mod.rs @@ -202,11 +202,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O allow_ts_decorators: class_opts.allow_ts_decorators, class_has_extends: extends.is_some(), has_argument_decorators: false, + decorator_scope: Some(p.current_scope), ..Default::default() }; // Parse decorators for this property let first_decorator_loc = p.lexer.loc(); + let first_decorator_range = p.lexer.range(); let property_scope_index = p.scopes_in_order.len(); if opts.allow_ts_decorators { opts.ts_decorators = p.parse_type_script_decorators()?; @@ -221,6 +223,26 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // read fields before move (G::Property is not Copy). let prop_kind = property.kind; let prop_key = property.key; + + // tsc and esbuild reject them too: the lowering needs a statement. + if Self::IS_TYPESCRIPT_ENABLED + && !p.options.features.standard_decorators + && class_opts.is_class_expr + { + let decorator_range = if opts.ts_decorators.len() > 0 { + Some(first_decorator_range) + } else { + Self::first_argument_decorator_range(&property) + }; + if let Some(range) = decorator_range { + p.log().add_range_error( + Some(p.source), + range, + b"TypeScript experimental decorators can only be used with class declarations", + ); + } + } + properties.push(property); has_auto_accessor = has_auto_accessor || prop_kind == js_ast::g::PropertyKind::AutoAccessor; @@ -275,9 +297,27 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O has_decorators: has_any_decorators, should_lower_standard_decorators: p.options.features.standard_decorators && (has_any_decorators || has_auto_accessor), + ts_decorators_use_private_names: false, }) } + /// The location of the first parameter decorator of a method property, if any. + fn first_argument_decorator_range(property: &G::Property) -> Option { + let value = property.value?; + let js_ast::expr::Data::EFunction(func) = value.data else { + return None; + }; + func.func + .args + .iter() + .flat_map(|arg| arg.ts_decorators.slice().iter()) + .next() + .map(|decorator| bun_ast::Range { + loc: decorator.loc, + len: 0, + }) + } + pub(crate) fn parse_template_parts( &mut self, include_raw: bool, diff --git a/src/js_parser/parse/parse_fn.rs b/src/js_parser/parse/parse_fn.rs index 664f4eaf94fb..65350401cdbe 100644 --- a/src/js_parser/parse/parse_fn.rs +++ b/src/js_parser/parse/parse_fn.rs @@ -228,7 +228,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut ts_decorators = bun_alloc::AstAlloc::vec(); if opts.allow_ts_decorators { - ts_decorators = p.parse_type_script_decorators()?; + // Parameter decorators are evaluated outside the method, with the class. + let inner_fn_or_arrow_data = p.fn_or_arrow_data_parse.clone(); + let inner_scope = p.current_scope; + p.fn_or_arrow_data_parse.allow_await = old_fn_or_arrow_data.allow_await; + p.fn_or_arrow_data_parse.allow_yield = old_fn_or_arrow_data.allow_yield; + p.fn_or_arrow_data_parse.needs_async_loc = old_fn_or_arrow_data.needs_async_loc; + p.fn_or_arrow_data_parse.allow_super_call = old_fn_or_arrow_data.allow_super_call; + p.fn_or_arrow_data_parse.allow_super_property = + old_fn_or_arrow_data.allow_super_property; + if let Some(decorator_scope) = opts.decorator_scope { + p.current_scope = decorator_scope; + } + let decorators = p.parse_type_script_decorators(); + p.current_scope = inner_scope; + p.fn_or_arrow_data_parse = inner_fn_or_arrow_data; + ts_decorators = decorators?; if ts_decorators.len_u32() > 0 { arg_has_decorators = true; } diff --git a/src/js_parser/parse/parse_prefix.rs b/src/js_parser/parse/parse_prefix.rs index bc5d24b92166..7ca86af43127 100644 --- a/src/js_parser/parse/parse_prefix.rs +++ b/src/js_parser/parse/parse_prefix.rs @@ -579,6 +579,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O &ParseClassOptions { allow_ts_decorators: Self::IS_TYPESCRIPT_ENABLED || p.options.features.standard_decorators, + is_class_expr: true, ..Default::default() }, )?; @@ -589,6 +590,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O fn pfx_t_at(p: &mut Self) -> PResult { // Parse decorators before a class expression: @dec class { ... } + if Self::IS_TYPESCRIPT_ENABLED && !p.options.features.standard_decorators { + p.log().add_range_error( + Some(p.source), + p.lexer.range(), + b"TypeScript experimental decorators cannot be used in expression position", + ); + } let ts_decorators = p.parse_type_script_decorators()?; // Expect class keyword after decorators @@ -650,6 +658,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O &ParseClassOptions { ts_decorators: ts_decorators_slice, allow_ts_decorators: true, + is_class_expr: true, ..Default::default() }, )?; diff --git a/src/js_parser/parse/parse_property.rs b/src/js_parser/parse/parse_property.rs index 81fa6a8f0224..8cd44f7dbbae 100644 --- a/src/js_parser/parse/parse_property.rs +++ b/src/js_parser/parse/parse_property.rs @@ -104,6 +104,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O allow_super_call: opts.class_has_extends && is_constructor, allow_super_property: true, allow_ts_decorators: opts.allow_ts_decorators, + decorator_scope: opts.decorator_scope, is_constructor, has_decorators: opts.ts_decorators.len() > 0 || (opts.has_class_decorators && is_constructor), @@ -449,8 +450,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if let Some(prop) = p.parse_property(kind, opts, None)? { - if prop.kind == PropertyKind::Normal - && prop.value.is_none() + if matches!( + prop.kind, + PropertyKind::Normal + | PropertyKind::AutoAccessor + ) && prop.value.is_none() && opts.ts_decorators.len() > 0 { let mut prop_ = prop; @@ -463,10 +467,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } PropertyModifierKeyword::PAccessor => { - // "accessor" keyword for auto-accessor fields (TC39 standard decorators) + // "accessor" keyword for auto-accessor fields if opts.is_class && !p.lexer.has_newline_before - && p.options.features.standard_decorators && PropertyModifierKeyword::find(raw) == Some(PropertyModifierKeyword::PAccessor) { diff --git a/src/js_parser/parse/parse_stmt.rs b/src/js_parser/parse/parse_stmt.rs index 67fb248ab74f..6190799e6f57 100644 --- a/src/js_parser/parse/parse_stmt.rs +++ b/src/js_parser/parse/parse_stmt.rs @@ -79,8 +79,23 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // Parse decorators before class statements, which are potentially exported if Self::IS_TYPESCRIPT_ENABLED || p.options.features.standard_decorators { let scope_index = p.scopes_in_order.len(); + let at_loc = p.lexer.loc(); let ts_decorators = p.parse_type_script_decorators()?; + // "@x export @y class Foo {}" + if opts.ts_decorators.is_some() { + p.log().add_range_error( + Some(p.source), + bun_ast::Range { + loc: at_loc, + len: p.lexer.range().end().start - at_loc.start, + }, + b"Decorators are not valid here", + ); + p.discard_scopes_up_to(scope_index); + return p.parse_stmt(opts); + } + // If this turns out to be a "declare class" statement, we need to undo the // scopes that were potentially pushed while parsing the decorator arguments. // That can look like any one of the following: @@ -848,6 +863,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if opts.ts_decorators.is_some() && p.lexer.token != T::TClass && p.lexer.token != T::TDefault + && p.lexer.token != T::TAt && !p.lexer.is_contextual_keyword(b"abstract") && !p.lexer.is_contextual_keyword(b"declare") { @@ -978,6 +994,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // "@decorator export default abstract class Foo {}" if opts.ts_decorators.is_some() && p.lexer.token != T::TClass + && p.lexer.token != T::TAt && !p.lexer.is_contextual_keyword(b"abstract") { p.lexer.expected(T::TClass)?; @@ -1037,8 +1054,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O )); } + // "export default @x class Foo {}" is a class declaration too if p.lexer.token == T::TFunction || p.lexer.token == T::TClass + || p.lexer.token == T::TAt || p.lexer.is_contextual_keyword(b"interface") { let mut _opts = ParseStatementOptions { @@ -1071,6 +1090,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ref_: name.ref_, }; } + + // Lowered as an expression the class is named "default". + if class.class.should_lower_standard_decorators { + let mut class_ref = *class; + let class_value = core::mem::take(&mut class_ref.class); + let expr = p.new_expr(class_value, stmt.loc); + let default_name = p.create_default_name(default_loc); + p.has_es_module_syntax = true; + return Ok(p.s( + S::ExportDefault { + default_name, + value: js_ast::StmtOrExpr::Expr(expr), + }, + loc, + )); + } } // "interface" turned out not to start an interface // declaration: the nested statement came back as an diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 408deeeb45c0..ab0f0f51d9a4 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -1216,6 +1216,9 @@ pub struct FnOrArrowDataParse { /// Allow TypeScript decorators in function arguments pub(crate) allow_ts_decorators: bool, + + /// The class body scope, where TypeScript parameter decorators are parsed. + pub(crate) decorator_scope: Option>, } impl Default for FnOrArrowDataParse { @@ -1237,6 +1240,7 @@ impl Default for FnOrArrowDataParse { track_arrow_arg_errors: false, allow_missing_body_for_type_script: false, allow_ts_decorators: false, + decorator_scope: None, } } } @@ -1321,6 +1325,8 @@ pub struct PropertyOpts { pub(crate) ts_decorators: ExprNodeList, pub(crate) has_argument_decorators: bool, pub(crate) has_class_decorators: bool, + /// See `FnOrArrowDataParse::decorator_scope`. + pub(crate) decorator_scope: Option>, } impl Default for PropertyOpts { @@ -1338,6 +1344,7 @@ impl Default for PropertyOpts { ts_decorators: bun_alloc::AstAlloc::vec(), has_argument_decorators: false, has_class_decorators: false, + decorator_scope: None, } } } @@ -1421,6 +1428,8 @@ pub struct ParseClassOptions<'a> { pub(crate) ts_decorators: &'a [Expr], pub(crate) allow_ts_decorators: bool, pub(crate) is_type_script_declare: bool, + /// TypeScript experimental decorators are rejected inside class expressions. + pub(crate) is_class_expr: bool, } #[repr(u8)] diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index 53786e4db37d..e2190440a836 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -218,7 +218,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O for arg in args.iter_mut() { if arg.ts_decorators.len_u32() > 0 { + // Parsed in the class body scope (see `parse_fn`), so visit it there. + let args_scope = self.current_scope; + let class_body_scope = args_scope + .parent + .expect("a method with parameter decorators is inside a class body"); + debug_assert!(class_body_scope.kind == ScopeKind::ClassBody); + self.current_scope = class_body_scope; self.visit_ts_decorators(&mut arg.ts_decorators); + self.current_scope = args_scope; } // reborrow per-iter. @@ -232,9 +240,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // `Vec` is not `Copy`; mutate in place. pub(crate) fn visit_ts_decorators(&mut self, decs: &mut ExprNodeList) { + let outer_class_scope = self.ts_decorator_class_scope.replace(self.current_scope); for dec in decs.slice_mut() { self.visit_expr(dec); } + self.ts_decorator_class_scope = outer_class_scope; } pub(crate) fn visit_decls( @@ -841,6 +851,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O { self.push_scope_for_visit_pass(ScopeKind::ClassBody, class.body_loc) .expect("unreachable"); + let class_body_scope = self.current_scope; let mut constructor_function: Option> = None; let properties: &mut [G::Property] = class.properties.slice_mut(); @@ -970,7 +981,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } if let Some(val) = property.initializer { - // if (property.flags.is_static and ) if let Some(name) = name_to_keep { let was_anon = val.is_anonymous_named(); let prev_dcn2 = self.decorator_class_name; @@ -996,7 +1006,20 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.fn_only_data_visit.is_this_nested = old_is_this_captured; } + class.ts_decorators_use_private_names = self + .ts_decorator_scopes_using_private_names + .iter() + .position(|scope| *scope == class_body_scope) + .inspect(|&i| { + self.ts_decorator_scopes_using_private_names.swap_remove(i); + }) + .is_some(); + if Self::IS_TYPESCRIPT_ENABLED { + if !class.should_lower_standard_decorators { + self.lower_ts_auto_accessors(class); + } + // `lower_standard_decorators_stmt` owns field placement for such classes. let use_define = self.options.use_define_for_class_fields || class.should_lower_standard_decorators; @@ -1133,10 +1156,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; injected.push(Stmt::assign(target, init)); } - // Keep `#x;` for brand; keep decorated props for `lower_class`. - if is_private || prop.ts_decorators.len_u32() > 0 { + // Keep `#x;` for brand. A decorated field stays only for its decorator call. + if is_private { + class_body.push(G::Property { + initializer: None, + ..prop + }); + } else if prop.ts_decorators.len_u32() > 0 { class_body.push(G::Property { initializer: None, + kind: PropertyKind::Declare, ..prop }); } @@ -1292,6 +1321,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut before: ListManaged<'a, Stmt> = ListManaged::new_in(p.arena); let mut after: ListManaged<'a, Stmt> = ListManaged::new_in(p.arena); + let prev_nearest_stmt_list = p.nearest_stmt_list; + // BACKREF via `addr_of_mut!` (Stacked Borrows); set before the enum pre-pass too. + p.nearest_stmt_list = NonNull::new(core::ptr::addr_of_mut!(before)); + // Preprocess TypeScript enums to improve code generation. Otherwise // uses of an enum before that enum has been declared won't be inlined: // @@ -1324,13 +1357,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut visited: ListManaged<'a, Stmt> = ListManaged::with_capacity_in(stmts.len(), p.arena); - let prev_nearest_stmt_list = p.nearest_stmt_list; - // BACKREF โ€” `before` outlives this block; raw NonNull avoids - // the `&'a mut` borrow conflict. Derive via `addr_of_mut!` (no intermediate - // `&mut`) so the pointer shares the local's base tag and survives the - // direct `&mut before` reborrows in the loop below (Stacked Borrows). - p.nearest_stmt_list = NonNull::new(core::ptr::addr_of_mut!(before)); - let mut preprocessed_enum_i: usize = 0; 'stmt_loop: for stmt in stmts.iter_mut() { diff --git a/src/js_parser/visit/visit_binary.rs b/src/js_parser/visit/visit_binary.rs index e86ba8da5398..61a0d2cbe36f 100644 --- a/src/js_parser/visit/visit_binary.rs +++ b/src/js_parser/visit/visit_binary.rs @@ -715,6 +715,7 @@ impl BinaryExpressionVisitor { let name = p.load_name_from_ref(private.ref_); let result = p.find_symbol(e_.left.loc, name).expect("unreachable"); private.ref_ = result.r#ref; + p.note_private_name_use(name, result.r#ref); // Unlike regular identifiers, there are no unbound private identifiers let kind = p.symbols[result.r#ref.inner_index() as usize].kind; diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index b843756bb2e5..1846873f72a8 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -917,6 +917,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let name = p.load_name_from_ref(private.ref_); let result = p.find_symbol(e_.index.loc, name).expect("unreachable"); private.ref_ = result.r#ref; + p.note_private_name_use(name, result.r#ref); // Unlike regular identifiers, there are no unbound private identifiers let kind: js_ast::symbol::Kind = diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 597bcd6952aa..b60a351d5341 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -55,7 +55,8 @@ bun_core::declare_scope!(cache, visible); /// offsets picked by a header byte) plus a body of tagged records with /// u8/u16/u32 ids and implied slots dropped, instead of fixed u32 arrays. /// Version 27: ModuleInfo string table holds Latin-1 / UTF-16 bodies, not WTF-8. -const EXPECTED_VERSION: u32 = 27; +/// Version 28: TypeScript experimental decorator lowering output changed. +const EXPECTED_VERSION: u32 = 28; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/bundler/transpiler/decorators-legacy-lowering.test.ts b/test/bundler/transpiler/decorators-legacy-lowering.test.ts new file mode 100644 index 000000000000..ac41f35f2847 --- /dev/null +++ b/test/bundler/transpiler/decorators-legacy-lowering.test.ts @@ -0,0 +1,419 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, nodeExe, tempDir } from "harness"; +import { join } from "node:path"; + +// TypeScript "experimentalDecorators" lowering. Every fixture runs three ways: +// with `bun `, bundled and minified with `Bun.build`, and as the +// JavaScript tsc emits for it, run with node. All three must print the same +// thing, so the expected output is tsc's behavior. + +interface Fixture { + source: string; + expected: string; + useDefineForClassFields?: boolean; + /** Extra files next to index.ts. */ + files?: Record; +} + +const fixtures: Record = { + "decorated fields stay in place: init order, this, super, no initializer": { + source: ` + function dec(t: any, k?: any, d?: any) {} + class Base { static x = 5 } + class A extends Base { + static a = (console.log("a"), 1); @dec static b = (console.log("b"), 2); static c = (console.log("c"), A.b); + @dec static t = this.a; + @dec static s = super.x; + x = (console.log("x"), 1); @dec y = (console.log("y"), 2); z = (console.log("z"), 3); + @dec w; + } + new A(); + const a: any = new A(); + console.log(A.c, A.t, A.s, "w" in a, Object.getOwnPropertyDescriptor(a, "y")?.value); + `, + expected: "a\nb\nc\nx\ny\nz\nx\ny\nz\n2 1 5 true 2\n", + }, + "useDefineForClassFields false: decorated fields follow [[Set]] semantics too": { + useDefineForClassFields: false, + source: ` + function dec(t: any, k?: any, d?: any) {} + class Base { static x = 5; set y(v: any) { console.log("setter", v) } get y() { return "getter" } } + class A extends Base { + static a = (console.log("a"), 1); @dec static b = (console.log("b"), 2); static c = (console.log("c"), A.b); + x = (console.log("x"), 1); @dec y = (console.log("y"), 2); z = (console.log("z"), 3); + @dec w; + } + const a: any = new A(); + console.log(A.c, "w" in a, a.y, Object.hasOwn(a, "y")); + `, + expected: "a\nb\nc\nx\ny\nsetter 2\nz\n2 false getter false\n", + }, + "computed keys are evaluated once and the decorator sees the same key": { + source: ` + let n = 0; const key = () => "k" + ++n; + function d2(t: any, p: string) { console.log("decorating", p) } + class F { @d2 [key()]() {} @d2 [key()] = 1 } + new F(); new F(); + console.log(n, JSON.stringify(Object.getOwnPropertyNames(F.prototype)), JSON.stringify(Object.keys(new F()))); + let k: any; + class G { @d2 [k = "a"]() {} @d2 [k = "b"]() {} @d2 accessor [k = "c"] = 1 } + console.log(k, Object.getOwnPropertyNames(G.prototype).join(",")); + `, + expected: + 'decorating k1\ndecorating k2\n2 ["constructor","k1"] ["k2"]\ndecorating a\ndecorating b\ndecorating c\nc constructor,a,b,c\n', + }, + "a decorated declare field is dropped but its computed key still runs once": { + source: ` + function dec(t: any, k: any) { console.log("dec", String(k)) } + let n = 0; const k = () => "k" + ++n; + class A { @dec declare [k()]: number; @dec declare d: number; x = 1 } + console.log(n, JSON.stringify(Object.getOwnPropertyNames(new A()))); + `, + expected: 'dec k1\ndec d\n1 ["x"]\n', + }, + "useDefineForClassFields false: accessor initializers keep their place": { + useDefineForClassFields: false, + source: ` + function dec(t: any, k: any, d: any) { console.log("dec", k, typeof d.get) } + const log: string[] = []; + class C { + a = (log.push("a"), 1); + @dec accessor x = (log.push("x"), 2); + b = (log.push("b"), 3); + static accessor s = (log.push("s"), 4); + } + const c = new C(); + c.x = 5; + console.log(log.join(","), c.x, C.s, JSON.stringify(Object.getOwnPropertyNames(c))); + `, + expected: 'dec x function\ns,a,x,b 5 4 ["a","b"]\n', + }, + "useDefineForClassFields false: decorated fields next to a computed key": { + useDefineForClassFields: false, + source: ` + function dec(t: any, k: any) { console.log("dec", String(k)) } + const s = Symbol("s"); + class A { @dec w: number; [s] = 1; @dec [Symbol.iterator]: any; y = 2 } + const a: any = new A(); + console.log(JSON.stringify([a.y, a[s], a.w, a[Symbol.iterator]])); + `, + // Bun keeps a class with a computed instance key native instead of hoisting + // the key like tsc, so "w" is an own property here and not with tsc. The + // values and the decorator calls agree. + expected: "dec w\ndec Symbol(Symbol.iterator)\n[2,1,null,null]\n", + }, + "a parameter decorator is evaluated in the scope around the class": { + source: ` + function pd(v: any) { console.log("dec arg =", v); return (...a: any[]) => {} } + let arg = 1; + class P { m(@pd(arg) arg = 2) { return arg } } + console.log(new P().m()); + `, + expected: "dec arg = 1\n2\n", + }, + "a parameter decorator can await or yield in the enclosing function": { + source: ` + function pd(v: any) { console.log("dec arg =", v); return (...a: any[]) => {} } + async function f(foo: Promise) { class C { m(@pd(await foo) a: any) {} } return new C() } + await f(Promise.resolve(42)); + function* g() { class D { *m(@pd(yield 1) a: any) {} } return new D() } + const it = g(); console.log(it.next().value, it.next(43).done); + console.log("done"); + `, + expected: "dec arg = 42\ndec arg = 43\n1 true\ndone\n", + }, + "decorators that read a static #private name run inside the class": { + source: ` + function pd(v: any) { console.log("dec arg =", v); return (...a: any[]) => {} } + class Q { + static #p = 1; + static { console.log("static block") } + m(@pd(Q.#p) a: any) {} + @pd(Q.#p + 1) b: any; + @pd(new (class { read() { return Q.#p + 2 } })().read()) c: any; + static #q = (console.log("static field"), 2); + } + console.log(Object.getOwnPropertyNames(Q.prototype).join(",")); + `, + expected: "static block\nstatic field\ndec arg = 1\ndec arg = 2\ndec arg = 3\nconstructor,m\n", + }, + "a private name of another class in a decorator keeps the calls outside": { + source: ` + function dec(...a: any[]) { return (...b: any[]) => {} } + class Other { static #p = 1; static read() { return Other.#p } } + class C { + @dec(class { #id = 7; get() { return this.#id } }) x = 1; + @dec(Other.read()) y = 2; + } + console.log(Object.keys(new C()).join(",")); + `, + expected: "x,y\n", + }, + "export default @dec class keeps the binding and applies the decorator": { + files: { + "mod.ts": ` + function cd(cls: any) { console.log("decorated", cls.x); return class extends cls { static y = 2 } } + export default @cd class Bar { static x = 1 } + console.log(Bar.x, Bar.y); + `, + }, + source: ` + import Bar from "./mod"; + console.log(Bar.x, Bar.y); + `, + expected: "decorated 1\n1 2\n1 2\n", + }, + "accessor fields are lowered and can be decorated": { + source: ` + function dec(t: any, k: any, d: any) { console.log("dec", String(k), d === undefined ? "undefined" : typeof d.get) } + const sym = Symbol("s"); + abstract class Acc { + accessor y = 2; + @dec accessor z = 3; + @dec static accessor s: string; + @dec accessor [sym] = 4; + accessor #p = 5; + @dec abstract accessor q: number; + p() { return this.#p } + } + class Impl extends Acc { accessor q = 6 } + const a = new Impl(); + console.log(a.y, a.z, Acc.s, a[sym], a.p(), a.q); + a.y = 20; a.z = 30; Acc.s = "s"; a[sym] = 40; + console.log(a.y, a.z, Acc.s, a[sym]); + console.log(JSON.stringify(Object.getOwnPropertyNames(a)), JSON.stringify(Object.getOwnPropertyNames(Acc.prototype))); + `, + expected: + 'dec z function\ndec Symbol(s) function\ndec q undefined\ndec s function\n2 3 undefined 4 5 6\n20 30 s 40\n[] ["constructor","y","z","p"]\n', + }, + "accessor storage names stay unique and class expressions are lowered too": { + source: ` + class A { + accessor x = 1; + static accessor x = 2; + accessor #x = 3; + #x_accessor_storage = 4; + get(): number[] { return [this.x, A.x, this.#x, this.#x_accessor_storage] } + } + let n = 0; + const k = () => "k" + ++n; + const B = class { accessor y = 5; accessor [k()] = 7 }; + const b: any = new B(); + b.y += 1; + enum E { A = (new (class { accessor [k()] = 8 })() as any).k2 } + console.log(JSON.stringify([new A().get(), b.y, b.k1, Object.getOwnPropertyNames(b).length, E.A, n])); + `, + expected: "[[1,2,3,4],6,7,0,8,2]\n", + }, +}; + +function tsconfig(useDefineForClassFields: boolean | undefined) { + return JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + ...(useDefineForClassFields === undefined ? {} : { useDefineForClassFields }), + }, + }); +} + +async function run(cmd: string[], cwd: string) { + await using proc = Bun.spawn({ cmd, cwd, env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; +} + +// Runs in node: transpiles every .ts file in the directory with the typescript +// package, then imports index.mjs. Loading typescript in a debug build of bun +// takes far longer than a test may. +const tscEmitAndRun = ` + const fs = require("node:fs"); + const path = require("node:path"); + const ts = require(process.argv[2]); + const useDefineForClassFields = process.argv[3] === "" ? undefined : process.argv[3] === "true"; + for (const file of fs.readdirSync(".")) { + if (!file.endsWith(".ts")) continue; + const out = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { + experimentalDecorators: true, + useDefineForClassFields, + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + }, + }).outputText; + fs.writeFileSync(file.replace(/\\.ts$/, ".mjs"), out.replaceAll(/from "(\\.\\/[^"]+)"/g, 'from "$1.mjs"')); + } + import("file://" + path.resolve("index.mjs")); +`; +const typescriptPath = require.resolve("typescript"); + +describe("experimentalDecorators lowering", () => { + describe.each(Object.entries(fixtures))("%s", (_name, fixture) => { + const files = { + "tsconfig.json": tsconfig(fixture.useDefineForClassFields), + "index.ts": fixture.source, + ...fixture.files, + }; + + test("bun", async () => { + using dir = tempDir("legacy-dec-run", files); + const { stdout, stderr, exitCode } = await run([bunExe(), "index.ts"], String(dir)); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: fixture.expected, stderr: "", exitCode: 0 }); + }); + + test("bundled and minified", async () => { + using dir = tempDir("legacy-dec-build", files); + const build = await Bun.build({ + entrypoints: [join(String(dir), "index.ts")], + outdir: join(String(dir), "out"), + minify: true, + }); + expect(build.logs).toEqual([]); + const { stdout, stderr, exitCode } = await run([bunExe(), "out/index.js"], String(dir)); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: fixture.expected, stderr: "", exitCode: 0 }); + }); + + test.skipIf(!nodeExe())("tsc emit", async () => { + // The reference: tsc's own output for the same source, run as plain JavaScript. + using dir = tempDir("legacy-dec-tsc", { ...files, "tsc-emit-and-run.cjs": tscEmitAndRun }); + const { stdout, stderr, exitCode } = await run( + [nodeExe()!, "tsc-emit-and-run.cjs", typescriptPath, String(fixture.useDefineForClassFields ?? "")], + String(dir), + ); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: fixture.expected, stderr: "", exitCode: 0 }); + }); + }); + + test("decorated auto-accessor and declare fields get design:type metadata", async () => { + using dir = tempDir("legacy-dec-metadata", { + "tsconfig.json": JSON.stringify({ + compilerOptions: { experimentalDecorators: true, emitDecoratorMetadata: true }, + }), + "index.ts": ` + (Reflect as any).metadata = (key: string, value: any) => (target: any, prop: string) => { + console.log(prop, key, value.name); + }; + function dec(t: any, k: any, d: any) {} + class Acc { + @dec accessor n: number = 1; + @dec accessor s: string; + @dec declare d: boolean; + } + console.log(new Acc().n, "d" in new Acc()); + `, + }); + const { stdout, stderr, exitCode } = await run([bunExe(), "index.ts"], String(dir)); + expect(stderr).toBe(""); + expect(stdout).toBe("n design:type Number\ns design:type String\nd design:type Boolean\n1 false\n"); + expect(exitCode).toBe(0); + }); + + test("decorators on a class expression are a syntax error", async () => { + using dir = tempDir("legacy-dec-class-expr", { + "tsconfig.json": tsconfig(undefined), + "before.ts": ` + function cd(cls: any) {} + const X = @cd class Y {}; + `, + "member.ts": ` + function dec(t: any, k?: any) {} + const X = class Y { @dec x = 1 }; + `, + "param.ts": ` + function dec(t: any, k?: any, i?: any) {} + const X = class { m(@dec a: any) {} }; + `, + }); + const [before, member, param] = await Promise.all([ + run([bunExe(), "before.ts"], String(dir)), + run([bunExe(), "member.ts"], String(dir)), + run([bunExe(), "param.ts"], String(dir)), + ]); + expect(before.stderr).toContain("TypeScript experimental decorators cannot be used in expression position"); + expect(before.exitCode).not.toBe(0); + expect(member.stderr).toContain("TypeScript experimental decorators can only be used with class declarations"); + expect(member.exitCode).not.toBe(0); + expect(param.stderr).toContain("TypeScript experimental decorators can only be used with class declarations"); + expect(param.exitCode).not.toBe(0); + }); + + test("decorators on both sides of export default are rejected", async () => { + using dir = tempDir("legacy-dec-double", { + "tsconfig.json": tsconfig(undefined), + "index.ts": ` + function cd(cls: any) {} + @cd export default @cd class Foo {} + `, + }); + const { stderr, exitCode } = await run([bunExe(), "index.ts"], String(dir)); + expect(stderr).toContain("Decorators are not valid here"); + expect(exitCode).not.toBe(0); + }); + + test("Bun.Transpiler output shape matches tsc", async () => { + const transpiler = new Bun.Transpiler({ + loader: "ts", + tsconfig: { compilerOptions: { experimentalDecorators: true } }, + }); + // A private name of another class does not move the calls into a static + // block, so `await` in a sibling decorator keeps working. + expect( + transpiler.transformSync(` + class C { + @dec(class { #id = 7; get() { return this.#id } }) x = 1; + @dec(await p) y = 2; + } + `), + ).not.toContain("static {"); + const out = transpiler.transformSync(` + function dec(t: any, k?: any, d?: any) {} + let n = 0; const key = () => "k" + ++n; + class A { + static #p = 1; + @dec static a = this.x; + @dec b; + @dec [key()] = 2; + accessor c = 3; + m(@dec(A.#p) arg = 1) {} + } + `); + // Fields stay in the body, computed keys are captured once, and the + // decorator calls run in a static block because one reads `A.#p`. + expect(out).toMatchInlineSnapshot(` + "import { __legacyDecorateClassTS as __legacyDecorateClassTS_3r173x8m, __legacyDecorateParamTS as __legacyDecorateParamTS_1ycx8dha } from "bun:wrap"; + function dec(t, k, d) {} + let n = 0; + const key = () => "k" + ++n; + var __bun_temp_ref_1$; + + class A { + static #p = 1; + static a = this.x; + b; + [__bun_temp_ref_1$ = key()] = 2; + #c_accessor_storage = 3; + get c() { + return this.#c_accessor_storage; + } + set c(v) { + this.#c_accessor_storage = v; + } + m(arg = 1) {} + static { + __legacyDecorateClassTS_3r173x8m([ + dec + ], A.prototype, "b", undefined); + __legacyDecorateClassTS_3r173x8m([ + dec + ], A.prototype, __bun_temp_ref_1$, undefined); + __legacyDecorateClassTS_3r173x8m([ + __legacyDecorateParamTS_1ycx8dha(0, dec(A.#p)) + ], A.prototype, "m", null); + __legacyDecorateClassTS_3r173x8m([ + dec + ], A, "a", undefined); + } + } + " + `); + }); +}); diff --git a/test/bundler/transpiler/decorators-set-semantics/set-semantics-fixture.ts b/test/bundler/transpiler/decorators-set-semantics/set-semantics-fixture.ts new file mode 100644 index 000000000000..d3c4597fb9b3 --- /dev/null +++ b/test/bundler/transpiler/decorators-set-semantics/set-semantics-fixture.ts @@ -0,0 +1,177 @@ +// @ts-nocheck +// Run by decorators.test.ts with this directory as the working directory, so +// this directory's tsconfig.json applies: experimentalDecorators with +// useDefineForClassFields: false. Decorated field initializers then use +// [[Set]] semantics and run the accessors the decorators define. +function expect(actual: unknown) { + return { + toBe(expected: unknown) { + if (actual !== expected) { + throw new Error(`Expected ${String(expected)} but received ${String(actual)}`); + } + }, + }; +} + +class IceCreamComponent { + @Emoji() + flavor = "vanilla"; +} + +// Property Decorator +function Emoji() { + return function (target: Object, key: string | symbol) { + let val = target[key]; + + const getter = () => { + return val; + }; + const setter = next => { + val = `๐Ÿฆ ${next} ๐Ÿฆ`; + }; + + Object.defineProperty(target, key, { + get: getter, + set: setter, + enumerable: true, + configurable: true, + }); + }; +} + +const iceCream = new IceCreamComponent(); +expect(iceCream.flavor).toBe("๐Ÿฆ vanilla ๐Ÿฆ"); +iceCream.flavor = "chocolate"; +expect(iceCream.flavor).toBe("๐Ÿฆ chocolate ๐Ÿฆ"); + +// No instance field below has a computed key: with useDefineForClassFields +// false, one such key keeps every instance field of the class native. + +const h: unique symbol = Symbol.for("h"); +const q: unique symbol = Symbol.for("q"); +const u3: unique symbol = Symbol.for("u3"); +const u8: unique symbol = Symbol.for("u8"); + +class S { + @StringAppender("๐Ÿ˜›") k = 35; + @StringAppender("๐Ÿค ") static j = 4; + @StringAppender("๐Ÿ˜ตโ€๐Ÿ’ซ") private static [h] = 30; + @StringAppender("๐Ÿคฏ") private static u = 60; + @StringAppender("๐ŸŽƒ") private e = 10; + @StringAppender("๐Ÿ‘ป") static [q] = 202; + @StringAppender("๐Ÿ˜‡") r = S[h]; + _y: number; + @StringAppender("๐Ÿคก") get y() { + return this._y; + } + set y(next) { + this._y = next; + } + #o = 100; + + @StringAppender("๐Ÿ˜") u1: number; + @StringAppender("๐Ÿฅณ") static u2: number; + @StringAppender("๐Ÿค“") private static [u3]: number; + @StringAppender("๐Ÿฅบ") private static u4: 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("๐Ÿ˜ถ") u15 = this.u7; + @StringAppender("๐Ÿ˜") u16 = S[u8]; + + constructor() { + this.k = 3; + expect(this.k).toBe("3 ๐Ÿ˜›"); + expect(S.j).toBe(4); + expect(this.e).toBe("10 ๐ŸŽƒ"); + expect(S[h]).toBe(30); + expect(S.u).toBe(60); + expect(S[q]).toBe(202); + expect(this.#o).toBe(100); + expect(this.r).toBe("30 ๐Ÿ˜‡"); + expect(this.y).toBe(undefined); + this.y = 100; + expect(this.y).toBe(100); + + expect(this.u1).toBe(undefined); + expect(S.u2).toBe(undefined); + expect(S[u3]).toBe(undefined); + expect(S.u4).toBe(undefined); + 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.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.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.u15).toBe("undefined ๐Ÿ˜ถ"); + expect(this.u16).toBe("undefined ๐Ÿ˜"); + } +} + +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.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.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, + }); + }; +} + +console.log("ok"); diff --git a/test/bundler/transpiler/decorators-set-semantics/tsconfig.json b/test/bundler/transpiler/decorators-set-semantics/tsconfig.json new file mode 100644 index 000000000000..00b8a14afab3 --- /dev/null +++ b/test/bundler/transpiler/decorators-set-semantics/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "useDefineForClassFields": false + } +} diff --git a/test/bundler/transpiler/decorators.test.ts b/test/bundler/transpiler/decorators.test.ts index 80800f17c415..84222e7fa80f 100644 --- a/test/bundler/transpiler/decorators.test.ts +++ b/test/bundler/transpiler/decorators.test.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; +import { join } from "node:path"; import DecoratedClass from "./decorator-export-default-class-fixture"; import DecoratedAnonClass from "./decorator-export-default-class-fixture-anon"; @@ -368,6 +369,10 @@ test("decorators random", () => { expect(Object.isFrozen(IceCream)).toBe(true); + // Decorated fields keep [[Define]] semantics, like tsc and esbuild with + // useDefineForClassFields unset (the default for this test's tsconfig). The + // own property created by `flavor = "vanilla"` shadows the accessor the + // decorator put on the prototype. class IceCreamComponent { @Emoji() flavor = "vanilla"; @@ -395,165 +400,32 @@ test("decorators random", () => { } const iceCream = new IceCreamComponent(); - expect(iceCream.flavor === "๐Ÿฆ vanilla ๐Ÿฆ").toBe(true); + expect(Object.getOwnPropertyDescriptor(iceCream, "flavor")).toEqual({ + value: "vanilla", + writable: true, + enumerable: true, + configurable: true, + }); + expect(iceCream.flavor).toBe("vanilla"); iceCream.flavor = "chocolate"; - expect(iceCream.flavor === "๐Ÿฆ chocolate ๐Ÿฆ").toBe(true); - - const i: unique symbol = Symbol.for("i"); - const h: unique symbol = Symbol.for("h"); - const t: unique symbol = Symbol.for("t"); - const q: unique symbol = Symbol.for("q"); - const p: unique symbol = Symbol.for("p"); - const u3: unique symbol = Symbol.for("u3"); - const u5: unique symbol = Symbol.for("u5"); - const u6: unique symbol = Symbol.for("u6"); - const u8: unique symbol = Symbol.for("u8"); - - 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]; - _y: number; - @StringAppender("๐Ÿคก") get y() { - return this._y; - } - set y(next) { - this._y = next; - } - #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]; - - constructor() { - this.k = 3; - expect(this.k).toBe("3 ๐Ÿ˜›"); - expect(S.j).toBe(4); - 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(S[q]).toBe(202); - expect(this.#o).toBe(100); - expect(this.r).toBe("30 ๐Ÿ˜‡"); - expect(this.y).toBe(undefined); - this.y = 100; - expect(this.y).toBe(100); - - expect(this.u1).toBe(undefined); - expect(S.u2).toBe(undefined); - expect(S[u3]).toBe(undefined); - expect(S.u4).toBe(undefined); - expect(this[u5]).toBe(undefined); - expect(this[u6]).toBe(undefined); - 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 ๐Ÿ˜"); - } - } - - 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}`; - }; + expect(iceCream.flavor).toBe("chocolate"); + expect(Object.getOwnPropertyDescriptor(IceCreamComponent.prototype, "flavor").get()).toBe(undefined); +}); - Object.defineProperty(target, key, { - get: getter, - set: setter, - enumerable: true, - configurable: true, - }); - }; - } +test("decorators random (useDefineForClassFields: false)", async () => { + // The same decorators with [[Set]] semantics: every field initializer runs + // through the accessor the decorator defined on the prototype. The fixture + // directory has its own tsconfig.json with useDefineForClassFields: false. + await using proc = Bun.spawn({ + cmd: [bunExe(), "set-semantics-fixture.ts"], + env: bunEnv, + cwd: join(import.meta.dir, "decorators-set-semantics"), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("ok\n"); + expect(exitCode).toBe(0); }); test("class field order", () => { @@ -1057,8 +929,11 @@ test("decorator and declare", () => { }); 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. + // Hold N fixed; compare M=100 vs M=20000 after a warm-up. Parsing the N-sized body + // dominates both runs when the lowering is O(M+N), so tLarge/tSmall stays under ~2x. + // If the splice-after-super() were O(M*N), every field would copy the body again + // and the ratio would be far above 3x in both debug and release builds. + // Only useDefineForClassFields: false moves field initializers into the constructor. await using proc = Bun.spawn({ cmd: [ bunExe(), @@ -1075,7 +950,7 @@ test("lowering many decorated instance fields into a large constructor body stay } const t = new Bun.Transpiler({ loader: "ts", - tsconfig: { compilerOptions: { experimentalDecorators: true } }, + tsconfig: { compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false } }, }); function time(M) { const src = gen(M); @@ -1086,8 +961,9 @@ test("lowering many decorated instance fields into a large constructor body stay throw new Error("instance-field initializers missing from lowered constructor at M=" + M); return ms; } + time(100); const tSmall = time(100); - const tLarge = time(50000); + const tLarge = time(20000); console.log(JSON.stringify({ tSmall, tLarge, ratio: tLarge / tSmall })); `, ], diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index bdc61e68c48d..82fd7a6af0ef 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -527,7 +527,7 @@ describe("ES Decorators", () => { const { stderr, exitCode } = await runDecoratorTS(` @x export @y class Foo {} `); - expect(stderr).toContain('Expected "class" but found "@"'); + expect(stderr).toContain("Decorators are not valid here"); expect(exitCode).not.toBe(0); });