diff --git a/crates/oxc_transformer/src/es2022/class_properties/class.rs b/crates/oxc_transformer/src/es2022/class_properties/class.rs index 02f130c158db6..df9724c28f420 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/class.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/class.rs @@ -15,13 +15,9 @@ use oxc_traverse::{BoundIdentifier, TraverseCtx}; use crate::common::helper_loader::Helper; use super::{ - super::ClassStaticBlock, constructor::InstanceInitsInsertLocation, private_props::{PrivateProp, PrivateProps}, - utils::{ - create_assignment, create_underscore_ident_name, create_variable_declaration, - exprs_into_stmts, - }, + utils::{create_assignment, create_variable_declaration, exprs_into_stmts}, ClassBindings, ClassProperties, FxIndexMap, }; @@ -530,463 +526,18 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { self.private_props_stack.pop(); } - /// Convert instance property to initialization expression. - /// Property `foo = 123;` -> Expression `this.foo = 123` or `_defineProperty(this, "foo", 123)`. - fn convert_instance_property( - &mut self, - prop: &mut PropertyDefinition<'a>, - instance_inits: &mut Vec>, - ctx: &mut TraverseCtx<'a>, - ) { - // Get value - let value = match &mut prop.value { - Some(value) => { - self.transform_instance_initializer(value, ctx); - ctx.ast.move_expression(value) - } - None => ctx.ast.void_0(SPAN), - }; - - let init_expr = if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { - self.create_private_instance_init_assignment(ident, value, ctx) - } else { - // Convert to assignment or `_defineProperty` call, depending on `loose` option - let this = ctx.ast.expression_this(SPAN); - self.create_init_assignment(prop, value, this, false, ctx) - }; - instance_inits.push(init_expr); - } - - /// Convert static property to initialization expression. - /// Property `static foo = 123;` -> Expression `C.foo = 123` or `_defineProperty(C, "foo", 123)`. - fn convert_static_property( - &mut self, - prop: &mut PropertyDefinition<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - // Get value, and transform it to replace `this` with reference to class name, - // and transform class property accesses (`object.#prop`) - let value = match &mut prop.value { - Some(value) => { - self.transform_static_initializer(value, ctx); - ctx.ast.move_expression(value) - } - None => ctx.ast.void_0(SPAN), - }; - - if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { - self.insert_private_static_init_assignment(ident, value, ctx); - } else { - // Convert to assignment or `_defineProperty` call, depending on `loose` option - let class_binding = if self.is_declaration { - // Class declarations always have a name except `export default class {}`. - // For default export, binding is created when static prop found in 1st pass. - self.class_bindings.name.as_ref().unwrap() - } else { - // Binding is created when static prop found in 1st pass. - self.class_bindings.temp.as_ref().unwrap() - }; - - let assignee = class_binding.create_read_expression(ctx); - let init_expr = self.create_init_assignment(prop, value, assignee, true, ctx); - self.insert_expr_after_class(init_expr, ctx); - } - } - - /// `assignee.foo = value` or `_defineProperty(assignee, "foo", value)` - fn create_init_assignment( - &mut self, - prop: &mut PropertyDefinition<'a>, - value: Expression<'a>, - assignee: Expression<'a>, - is_static: bool, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - if self.set_public_class_fields { - // `assignee.foo = value` - self.create_init_assignment_loose(prop, value, assignee, is_static, ctx) - } else { - // `_defineProperty(assignee, "foo", value)` - self.create_init_assignment_not_loose(prop, value, assignee, ctx) - } - } - - /// `this.foo = value` or `_Class.foo = value` - fn create_init_assignment_loose( - &mut self, - prop: &mut PropertyDefinition<'a>, - value: Expression<'a>, - assignee: Expression<'a>, - is_static: bool, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - // In-built static props `name` and `length` need to be set with `_defineProperty` - let needs_define = |name| is_static && (name == "name" || name == "length"); - - let left = match &mut prop.key { - PropertyKey::StaticIdentifier(ident) => { - if needs_define(&ident.name) { - return self.create_init_assignment_not_loose(prop, value, assignee, ctx); - } - ctx.ast.member_expression_static(SPAN, assignee, ident.as_ref().clone(), false) - } - PropertyKey::StringLiteral(str_lit) if needs_define(&str_lit.value) => { - return self.create_init_assignment_not_loose(prop, value, assignee, ctx); - } - key @ match_expression!(PropertyKey) => { - // TODO: This can also be a numeric key (non-computed). Maybe other key types? - let key = self.create_computed_key_temp_var(key.to_expression_mut(), ctx); - ctx.ast.member_expression_computed(SPAN, assignee, key, false) - } - PropertyKey::PrivateIdentifier(_) => { - // Handled in `convert_instance_property` and `convert_static_property` - unreachable!(); - } - }; - - // TODO: Should this have span of the original `PropertyDefinition`? - ctx.ast.expression_assignment( - SPAN, - AssignmentOperator::Assign, - AssignmentTarget::from(left), - value, - ) - } - - /// `_defineProperty(this, "foo", value)` or `_defineProperty(_Class, "foo", value)` - fn create_init_assignment_not_loose( - &mut self, - prop: &mut PropertyDefinition<'a>, - value: Expression<'a>, - assignee: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - let key = match &mut prop.key { - PropertyKey::StaticIdentifier(ident) => { - ctx.ast.expression_string_literal(ident.span, ident.name.clone(), None) - } - key @ match_expression!(PropertyKey) => { - // TODO: This can also be a numeric key (non-computed). Maybe other key types? - self.create_computed_key_temp_var(key.to_expression_mut(), ctx) - } - PropertyKey::PrivateIdentifier(_) => { - // Handled in `convert_instance_property` and `convert_static_property` - unreachable!(); - } - }; - - let arguments = ctx.ast.vec_from_array([ - Argument::from(assignee), - Argument::from(key), - Argument::from(value), - ]); - // TODO: Should this have span of the original `PropertyDefinition`? - self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx) - } - - /// Create init assignment for private instance prop, to be inserted into class constructor. - /// - /// Loose: `Object.defineProperty(this, _prop, {writable: true, value: value})` - /// Not loose: `_classPrivateFieldInitSpec(this, _prop, value)` - fn create_private_instance_init_assignment( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - if self.private_fields_as_properties { - let this = ctx.ast.expression_this(SPAN); - self.create_private_init_assignment_loose(ident, value, this, ctx) - } else { - self.create_private_instance_init_assignment_not_loose(ident, value, ctx) - } - } - - /// `Object.defineProperty(, _prop, {writable: true, value: value})` - fn create_private_init_assignment_loose( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - assignee: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - // `Object.defineProperty` - let object_symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), "Object"); - let object = ctx.create_ident_expr( - SPAN, - Atom::from("Object"), - object_symbol_id, - ReferenceFlags::Read, - ); - let property = ctx.ast.identifier_name(SPAN, "defineProperty"); - let callee = - Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)); - - // `{writable: true, value: }` - let prop_def = ctx.ast.expression_object( - SPAN, - ctx.ast.vec_from_array([ - ctx.ast.object_property_kind_object_property( - SPAN, - PropertyKind::Init, - ctx.ast.property_key_identifier_name(SPAN, Atom::from("writable")), - ctx.ast.expression_boolean_literal(SPAN, true), - false, - false, - false, - ), - ctx.ast.object_property_kind_object_property( - SPAN, - PropertyKind::Init, - ctx.ast.property_key_identifier_name(SPAN, Atom::from("value")), - value, - false, - false, - false, - ), - ]), - None, - ); - - let private_props = self.private_props_stack.last().unwrap(); - let prop = &private_props.props[&ident.name]; - let arguments = ctx.ast.vec_from_array([ - Argument::from(assignee), - Argument::from(prop.binding.create_read_expression(ctx)), - Argument::from(prop_def), - ]); - // TODO: Should this have span of original `PropertyDefinition`? - ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) - } - - /// `_classPrivateFieldInitSpec(this, _prop, value)` - fn create_private_instance_init_assignment_not_loose( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - let private_props = self.private_props_stack.last().unwrap(); - let prop = &private_props.props[&ident.name]; - let arguments = ctx.ast.vec_from_array([ - Argument::from(ctx.ast.expression_this(SPAN)), - Argument::from(prop.binding.create_read_expression(ctx)), - Argument::from(value), - ]); - // TODO: Should this have span of original `PropertyDefinition`? - self.ctx.helper_call_expr(Helper::ClassPrivateFieldInitSpec, SPAN, arguments, ctx) - } - - /// Insert after class: - /// - /// Not loose: - /// * Class declaration: `var _prop = {_: value};` - /// * Class expression: `_prop = {_: value}` - /// - /// Loose: - /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` - fn insert_private_static_init_assignment( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - if self.private_fields_as_properties { - self.insert_private_static_init_assignment_loose(ident, value, ctx); - } else { - self.insert_private_static_init_assignment_not_loose(ident, value, ctx); - } - } - - /// Insert after class: - /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` - fn insert_private_static_init_assignment_loose( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - // TODO: This logic appears elsewhere. De-duplicate it. - let class_binding = if self.is_declaration { - // Class declarations always have a name except `export default class {}`. - // For default export, binding is created when static prop found in 1st pass. - self.class_bindings.name.as_ref().unwrap() - } else { - // Binding is created when static prop found in 1st pass. - self.class_bindings.temp.as_ref().unwrap() - }; - - let assignee = class_binding.create_read_expression(ctx); - let assignment = self.create_private_init_assignment_loose(ident, value, assignee, ctx); - self.insert_expr_after_class(assignment, ctx); - } - - /// Insert after class: - /// - /// * Class declaration: `var _prop = {_: value};` - /// * Class expression: `_prop = {_: value}` - fn insert_private_static_init_assignment_not_loose( - &mut self, - ident: &PrivateIdentifier<'a>, - value: Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) { - // `_prop = {_: value}` - let property = ctx.ast.object_property_kind_object_property( - SPAN, - PropertyKind::Init, - PropertyKey::StaticIdentifier(ctx.ast.alloc(create_underscore_ident_name(ctx))), - value, - false, - false, - false, - ); - let obj = ctx.ast.expression_object(SPAN, ctx.ast.vec1(property), None); - - // Insert after class - let private_props = self.private_props_stack.last().unwrap(); - let prop = &private_props.props[&ident.name]; - - if self.is_declaration { - // `var _prop = {_: value};` - let var_decl = create_variable_declaration(&prop.binding, obj, ctx); - self.insert_after_stmts.push(var_decl); - } else { - // `_prop = {_: value}` - let assignment = create_assignment(&prop.binding, obj, ctx); - self.insert_after_exprs.push(assignment); - } - } - - /// Substitute temp var for method computed key. - /// `class C { [x()]() {} }` -> `let _x; _x = x(); class C { [_x]() {} }` - /// This transform is only required if class has properties or a static block. - fn substitute_temp_var_for_method_computed_key( + /// Insert an expression after the class. + pub(super) fn insert_expr_after_class( &mut self, - method: &mut MethodDefinition<'a>, + expr: Expression<'a>, ctx: &mut TraverseCtx<'a>, ) { - let Some(key) = method.key.as_expression_mut() else { return }; - - // TODO: Don't alter key if it's provable evaluating it has no side effects. - // TODO(improve-on-babel): It's unnecessary to create temp vars for method keys unless: - // 1. Properties also have computed keys. - // 2. Some of those properties' computed keys have side effects and require temp vars. - // 3. At least one property satisfying the above is after this method, - // or class contains a static block which is being transformed - // (static blocks are always evaluated after computed keys, regardless of order) - method.key = PropertyKey::from(self.create_computed_key_temp_var(key, ctx)); - } - - /// Convert static block to `Expression`. - /// - /// `static { x = 1; }` -> `x = 1` - /// `static { x = 1; y = 2; } -> `(() => { x = 1; y = 2; })()` - /// - /// TODO: Add tests for this if there aren't any already. - /// Include tests for evaluation order inc that static block goes before class expression - /// unless also static properties, or static block uses class name. - fn convert_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { - // TODO: Convert `this` and references to class name. - // `x = class C { static { this.C = C; } }` -> `x = (_C = class C {}, _C.C = _C, _C)` - // TODO: Scope of static block contents becomes outer scope, not scope of class. - - // If class expression, assignment in static block moves to a position where it's read from. - // e.g.: `x` here now has read+write `ReferenceFlags`: - // `C = class C { static { x = 1; } }` -> `C = (_C = class C {}, x = 1, _C)` - let expr = ClassStaticBlock::convert_block_to_expression(block, ctx); - self.insert_expr_after_class(expr, ctx); - } - - /// Insert an expression after the class. - fn insert_expr_after_class(&mut self, expr: Expression<'a>, ctx: &mut TraverseCtx<'a>) { if self.is_declaration { self.insert_after_stmts.push(ctx.ast.statement_expression(SPAN, expr)); } else { self.insert_after_exprs.push(expr); } } - - /// Convert computed property/method key to a temp var. - /// - /// Transformation is: - /// * Class declaration: - /// `class C { [x()] = 1; }` -> `let _x; _x = x(); class C { constructor() { this[_x] = 1; } }` - /// * Class expression: - /// `C = class { [x()] = 1; }` -> `let _x; C = (_x = x(), class C { constructor() { this[_x] = 1; } })` - /// - /// This function: - /// * Creates the `let _x;` statement and inserts it. - /// * Creates the `_x = x()` assignments. - /// * Inserts assignments before class declaration, or adds to `state` if class expression. - /// * Returns `_x`. - fn create_computed_key_temp_var( - &mut self, - key: &mut Expression<'a>, - ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - let key = ctx.ast.move_expression(key); - - // Bound vars and literals do not need temp var - return unchanged. - // e.g. `let x = 'x'; class C { [x] = 1; }` or `class C { ['x'] = 1; }` - // - // `this` does not have side effects, but it needs a temp var anyway, because `this` in computed - // key and `this` within class constructor resolve to different `this` bindings. - // So we need to create a temp var outside of the class to get the correct `this`. - // `class C { [this] = 1; }` - // -> `let _this; _this = this; class C { constructor() { this[_this] = 1; } }` - // - // TODO(improve-on-babel): Can avoid the temp var if key is for a static prop/method, - // as in that case the usage of `this` stays outside the class. - // - // TODO: Do fuller analysis to detect expressions which cannot have side effects e.g. `'x' + 'y'`. - let cannot_have_side_effects = match &key { - Expression::BooleanLiteral(_) - | Expression::NullLiteral(_) - | Expression::NumericLiteral(_) - | Expression::BigIntLiteral(_) - | Expression::RegExpLiteral(_) - | Expression::StringLiteral(_) => true, - Expression::Identifier(ident) => { - // Cannot have side effects if is bound. - // Additional check that the var is not mutated is required for cases like - // `let x = 1; class { [x] = 1; [++x] = 2; }` - // `++x` is hoisted to before class in output, so `x` in 1st key would get the wrong - // value unless it's hoisted out too. - // TODO: Add an exec test for this odd case. - // TODO(improve-on-babel): That case is rare. - // Test for it in first pass over class elements, and avoid temp vars where possible. - match ctx.symbols().get_reference(ident.reference_id()).symbol_id() { - Some(symbol_id) => { - ctx.symbols().get_flags(symbol_id).contains(SymbolFlags::ConstVariable) - || ctx - .symbols() - .get_resolved_references(symbol_id) - .all(|reference| !reference.is_write()) - } - None => false, - } - } - _ => false, - }; - if cannot_have_side_effects { - return key; - } - - // We entered transform via `enter_expression` or `enter_statement`, - // so `ctx.current_scope_id()` is the scope outside the class - let parent_scope_id = ctx.current_scope_id(); - // TODO: Handle if is a class expression defined in a function's params. - let binding = - ctx.generate_uid_based_on_node(&key, parent_scope_id, SymbolFlags::BlockScopedVariable); - - self.ctx.var_declarations.insert_let(&binding, None, ctx); - - let assignment = create_assignment(&binding, key, ctx); - self.insert_before.push(assignment); - - binding.create_read_expression(ctx) - } } /// Create `new WeakMap()` expression. diff --git a/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs b/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs new file mode 100644 index 0000000000000..4a82170557185 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs @@ -0,0 +1,114 @@ +//! ES2022: Class Properties +//! Transform of class property/method computed keys. + +use oxc_ast::ast::*; +use oxc_syntax::symbol::SymbolFlags; +use oxc_traverse::TraverseCtx; + +use super::{utils::create_assignment, ClassProperties}; + +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Substitute temp var for method computed key. + /// `class C { [x()]() {} }` -> `let _x; _x = x(); class C { [_x]() {} }` + /// This transform is only required if class has properties or a static block. + pub(super) fn substitute_temp_var_for_method_computed_key( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // TODO: Don't alter numeric literal key e.g. `class C { 123() {} }` + // TODO: Don't re-create key if it doesn't need to be altered + let Some(key) = method.key.as_expression_mut() else { return }; + + // TODO: Don't alter key if it's provable evaluating it has no side effects. + // TODO(improve-on-babel): It's unnecessary to create temp vars for method keys unless: + // 1. Properties also have computed keys. + // 2. Some of those properties' computed keys have side effects and require temp vars. + // 3. At least one property satisfying the above is after this method, + // or class contains a static block which is being transformed + // (static blocks are always evaluated after computed keys, regardless of order) + method.key = PropertyKey::from(self.create_computed_key_temp_var(key, ctx)); + } + + /// Convert computed property/method key to a temp var. + /// + /// Transformation is: + /// * Class declaration: + /// `class C { [x()] = 1; }` -> `let _x; _x = x(); class C { constructor() { this[_x] = 1; } }` + /// * Class expression: + /// `C = class { [x()] = 1; }` -> `let _x; C = (_x = x(), class C { constructor() { this[_x] = 1; } })` + /// + /// This function: + /// * Creates the `let _x;` statement and inserts it. + /// * Creates the `_x = x()` assignments. + /// * Inserts assignments before class declaration, or adds to `state` if class expression. + /// * Returns `_x`. + pub(super) fn create_computed_key_temp_var( + &mut self, + key: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let key = ctx.ast.move_expression(key); + + // Bound vars and literals do not need temp var - return unchanged. + // e.g. `let x = 'x'; class C { [x] = 1; }` or `class C { ['x'] = 1; }` + // + // `this` does not have side effects, but it needs a temp var anyway, because `this` in computed + // key and `this` within class constructor resolve to different `this` bindings. + // So we need to create a temp var outside of the class to get the correct `this`. + // `class C { [this] = 1; }` + // -> `let _this; _this = this; class C { constructor() { this[_this] = 1; } }` + // + // TODO(improve-on-babel): Can avoid the temp var if key is for a static prop/method, + // as in that case the usage of `this` stays outside the class. + // + // TODO: Do fuller analysis to detect expressions which cannot have side effects e.g. `'x' + 'y'`. + let cannot_have_side_effects = match &key { + Expression::BooleanLiteral(_) + | Expression::NullLiteral(_) + | Expression::NumericLiteral(_) + | Expression::BigIntLiteral(_) + | Expression::RegExpLiteral(_) + | Expression::StringLiteral(_) => true, + Expression::Identifier(ident) => { + // Cannot have side effects if is bound. + // Additional check that the var is not mutated is required for cases like + // `let x = 1; class { [x] = 1; [++x] = 2; }` + // `++x` is hoisted to before class in output, so `x` in 1st key would get the wrong + // value unless it's hoisted out too. + // TODO: Add an exec test for this odd case. + // TODO(improve-on-babel): That case is rare. + // Test for it in first pass over class elements, and avoid temp vars where possible. + match ctx.symbols().get_reference(ident.reference_id()).symbol_id() { + Some(symbol_id) => { + // TODO: Use `SymbolTable::symbol_is_mutated` + ctx.symbols().get_flags(symbol_id).contains(SymbolFlags::ConstVariable) + || ctx + .symbols() + .get_resolved_references(symbol_id) + .all(|reference| !reference.is_write()) + } + None => false, + } + } + _ => false, + }; + if cannot_have_side_effects { + return key; + } + + // We entered transform via `enter_expression` or `enter_statement`, + // so `ctx.current_scope_id()` is the scope outside the class + let parent_scope_id = ctx.current_scope_id(); + // TODO: Handle if is a class expression defined in a function's params. + let binding = + ctx.generate_uid_based_on_node(&key, parent_scope_id, SymbolFlags::BlockScopedVariable); + + self.ctx.var_declarations.insert_let(&binding, None, ctx); + + let assignment = create_assignment(&binding, key, ctx); + self.insert_before.push(assignment); + + binding.create_read_expression(ctx) + } +} diff --git a/crates/oxc_transformer/src/es2022/class_properties/mod.rs b/crates/oxc_transformer/src/es2022/class_properties/mod.rs index 7cca38806688d..5dc3c7e52e99d 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/mod.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/mod.rs @@ -127,9 +127,12 @@ //! //! * `mod.rs`: Setup and visitor. //! * `class.rs`: Transform of class body. +//! * `prop_decl.rs`: Transform of property declarations (instance and static). //! * `constructor.rs`: Insertion of property initializers into class constructor. //! * `instance_prop_init.rs`: Transform of instance property initializers. //! * `static_prop_init.rs`: Transform of static property initializers. +//! * `static_block.rs`: Transform of static blocks. +//! * `computed_key.rs`: Transform of property/method computed keys. //! * `private_field.rs`: Transform of private fields (`this.#prop`). //! * `super.rs`: Transform `super` expressions. //! * `class_bindings.rs`: Structure containing bindings for class name and temp var. @@ -159,10 +162,13 @@ use crate::TransformCtx; mod class; mod class_bindings; +mod computed_key; mod constructor; mod instance_prop_init; mod private_field; mod private_props; +mod prop_decl; +mod static_block; mod static_prop_init; mod supers; mod utils; diff --git a/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs b/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs new file mode 100644 index 0000000000000..65cf601d78fc1 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs @@ -0,0 +1,351 @@ +//! ES2022: Class Properties +//! Transform of class property declarations (instance or static properties). + +use oxc_ast::{ast::*, NONE}; +use oxc_span::SPAN; +use oxc_syntax::reference::ReferenceFlags; +use oxc_traverse::TraverseCtx; + +use crate::common::helper_loader::Helper; + +use super::{ + utils::{create_assignment, create_underscore_ident_name, create_variable_declaration}, + ClassProperties, +}; + +// Instance properties +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Convert instance property to initialization expression. + /// Property `foo = 123;` -> Expression `this.foo = 123` or `_defineProperty(this, "foo", 123)`. + pub(super) fn convert_instance_property( + &mut self, + prop: &mut PropertyDefinition<'a>, + instance_inits: &mut Vec>, + ctx: &mut TraverseCtx<'a>, + ) { + // Get value + let value = match &mut prop.value { + Some(value) => { + self.transform_instance_initializer(value, ctx); + ctx.ast.move_expression(value) + } + None => ctx.ast.void_0(SPAN), + }; + + let init_expr = if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { + self.create_private_instance_init_assignment(ident, value, ctx) + } else { + // Convert to assignment or `_defineProperty` call, depending on `loose` option + let this = ctx.ast.expression_this(SPAN); + self.create_init_assignment(prop, value, this, false, ctx) + }; + instance_inits.push(init_expr); + } + + /// Create init assignment for private instance prop, to be inserted into class constructor. + /// + /// Loose: `Object.defineProperty(this, _prop, {writable: true, value: value})` + /// Not loose: `_classPrivateFieldInitSpec(this, _prop, value)` + fn create_private_instance_init_assignment( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if self.private_fields_as_properties { + let this = ctx.ast.expression_this(SPAN); + self.create_private_init_assignment_loose(ident, value, this, ctx) + } else { + self.create_private_instance_init_assignment_not_loose(ident, value, ctx) + } + } + + /// `_classPrivateFieldInitSpec(this, _prop, value)` + fn create_private_instance_init_assignment_not_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let private_props = self.private_props_stack.last().unwrap(); + let prop = &private_props.props[&ident.name]; + let arguments = ctx.ast.vec_from_array([ + Argument::from(ctx.ast.expression_this(SPAN)), + Argument::from(prop.binding.create_read_expression(ctx)), + Argument::from(value), + ]); + // TODO: Should this have span of original `PropertyDefinition`? + self.ctx.helper_call_expr(Helper::ClassPrivateFieldInitSpec, SPAN, arguments, ctx) + } +} + +// Static properties +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Convert static property to initialization expression. + /// Property `static foo = 123;` -> Expression `C.foo = 123` or `_defineProperty(C, "foo", 123)`. + pub(super) fn convert_static_property( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Get value, and transform it to replace `this` with reference to class name, + // and transform class property accesses (`object.#prop`) + let value = match &mut prop.value { + Some(value) => { + self.transform_static_initializer(value, ctx); + ctx.ast.move_expression(value) + } + None => ctx.ast.void_0(SPAN), + }; + + if let PropertyKey::PrivateIdentifier(ident) = &mut prop.key { + self.insert_private_static_init_assignment(ident, value, ctx); + } else { + // Convert to assignment or `_defineProperty` call, depending on `loose` option + let class_binding = if self.is_declaration { + // Class declarations always have a name except `export default class {}`. + // For default export, binding is created when static prop found in 1st pass. + self.class_bindings.name.as_ref().unwrap() + } else { + // Binding is created when static prop found in 1st pass. + self.class_bindings.temp.as_ref().unwrap() + }; + + let assignee = class_binding.create_read_expression(ctx); + let init_expr = self.create_init_assignment(prop, value, assignee, true, ctx); + self.insert_expr_after_class(init_expr, ctx); + } + } + + /// Insert after class: + /// + /// Not loose: + /// * Class declaration: `var _prop = {_: value};` + /// * Class expression: `_prop = {_: value}` + /// + /// Loose: + /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` + fn insert_private_static_init_assignment( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.private_fields_as_properties { + self.insert_private_static_init_assignment_loose(ident, value, ctx); + } else { + self.insert_private_static_init_assignment_not_loose(ident, value, ctx); + } + } + + /// Insert after class: + /// `Object.defineProperty(Class, _prop, {writable: true, value: value});` + fn insert_private_static_init_assignment_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // TODO: This logic appears elsewhere. De-duplicate it. + let class_binding = if self.is_declaration { + // Class declarations always have a name except `export default class {}`. + // For default export, binding is created when static prop found in 1st pass. + self.class_bindings.name.as_ref().unwrap() + } else { + // Binding is created when static prop found in 1st pass. + self.class_bindings.temp.as_ref().unwrap() + }; + + let assignee = class_binding.create_read_expression(ctx); + let assignment = self.create_private_init_assignment_loose(ident, value, assignee, ctx); + self.insert_expr_after_class(assignment, ctx); + } + + /// Insert after class: + /// + /// * Class declaration: `var _prop = {_: value};` + /// * Class expression: `_prop = {_: value}` + fn insert_private_static_init_assignment_not_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // `_prop = {_: value}` + let property = ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + PropertyKey::StaticIdentifier(ctx.ast.alloc(create_underscore_ident_name(ctx))), + value, + false, + false, + false, + ); + let obj = ctx.ast.expression_object(SPAN, ctx.ast.vec1(property), None); + + // Insert after class + let private_props = self.private_props_stack.last().unwrap(); + let prop = &private_props.props[&ident.name]; + + if self.is_declaration { + // `var _prop = {_: value};` + let var_decl = create_variable_declaration(&prop.binding, obj, ctx); + self.insert_after_stmts.push(var_decl); + } else { + // `_prop = {_: value}` + let assignment = create_assignment(&prop.binding, obj, ctx); + self.insert_after_exprs.push(assignment); + } + } +} + +// Used for both instance and static properties +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// `assignee.foo = value` or `_defineProperty(assignee, "foo", value)` + fn create_init_assignment( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if self.set_public_class_fields { + // `assignee.foo = value` + self.create_init_assignment_loose(prop, value, assignee, is_static, ctx) + } else { + // `_defineProperty(assignee, "foo", value)` + self.create_init_assignment_not_loose(prop, value, assignee, ctx) + } + } + + /// `this.foo = value` or `_Class.foo = value` + fn create_init_assignment_loose( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + is_static: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // In-built static props `name` and `length` need to be set with `_defineProperty` + let needs_define = |name| is_static && (name == "name" || name == "length"); + + let left = match &mut prop.key { + PropertyKey::StaticIdentifier(ident) => { + if needs_define(&ident.name) { + return self.create_init_assignment_not_loose(prop, value, assignee, ctx); + } + ctx.ast.member_expression_static(SPAN, assignee, ident.as_ref().clone(), false) + } + PropertyKey::StringLiteral(str_lit) if needs_define(&str_lit.value) => { + return self.create_init_assignment_not_loose(prop, value, assignee, ctx); + } + key @ match_expression!(PropertyKey) => { + // TODO: This can also be a numeric key (non-computed). Maybe other key types? + let key = self.create_computed_key_temp_var(key.to_expression_mut(), ctx); + ctx.ast.member_expression_computed(SPAN, assignee, key, false) + } + PropertyKey::PrivateIdentifier(_) => { + // Handled in `convert_instance_property` and `convert_static_property` + unreachable!(); + } + }; + + // TODO: Should this have span of the original `PropertyDefinition`? + ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + AssignmentTarget::from(left), + value, + ) + } + + /// `_defineProperty(this, "foo", value)` or `_defineProperty(_Class, "foo", value)` + fn create_init_assignment_not_loose( + &mut self, + prop: &mut PropertyDefinition<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let key = match &mut prop.key { + PropertyKey::StaticIdentifier(ident) => { + ctx.ast.expression_string_literal(ident.span, ident.name.clone(), None) + } + key @ match_expression!(PropertyKey) => { + // TODO: This can also be a numeric key (non-computed). Maybe other key types? + self.create_computed_key_temp_var(key.to_expression_mut(), ctx) + } + PropertyKey::PrivateIdentifier(_) => { + // Handled in `convert_instance_property` and `convert_static_property` + unreachable!(); + } + }; + + let arguments = ctx.ast.vec_from_array([ + Argument::from(assignee), + Argument::from(key), + Argument::from(value), + ]); + // TODO: Should this have span of the original `PropertyDefinition`? + self.ctx.helper_call_expr(Helper::DefineProperty, SPAN, arguments, ctx) + } + + /// `Object.defineProperty(, _prop, {writable: true, value: value})` + fn create_private_init_assignment_loose( + &mut self, + ident: &PrivateIdentifier<'a>, + value: Expression<'a>, + assignee: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + // `Object.defineProperty` + let object_symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), "Object"); + let object = ctx.create_ident_expr( + SPAN, + Atom::from("Object"), + object_symbol_id, + ReferenceFlags::Read, + ); + let property = ctx.ast.identifier_name(SPAN, "defineProperty"); + let callee = + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)); + + // `{writable: true, value: }` + let prop_def = ctx.ast.expression_object( + SPAN, + ctx.ast.vec_from_array([ + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_identifier_name(SPAN, Atom::from("writable")), + ctx.ast.expression_boolean_literal(SPAN, true), + false, + false, + false, + ), + ctx.ast.object_property_kind_object_property( + SPAN, + PropertyKind::Init, + ctx.ast.property_key_identifier_name(SPAN, Atom::from("value")), + value, + false, + false, + false, + ), + ]), + None, + ); + + let private_props = self.private_props_stack.last().unwrap(); + let prop = &private_props.props[&ident.name]; + let arguments = ctx.ast.vec_from_array([ + Argument::from(assignee), + Argument::from(prop.binding.create_read_expression(ctx)), + Argument::from(prop_def), + ]); + // TODO: Should this have span of original `PropertyDefinition`? + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } +} diff --git a/crates/oxc_transformer/src/es2022/class_properties/static_block.rs b/crates/oxc_transformer/src/es2022/class_properties/static_block.rs new file mode 100644 index 0000000000000..605454284bbb1 --- /dev/null +++ b/crates/oxc_transformer/src/es2022/class_properties/static_block.rs @@ -0,0 +1,35 @@ +//! ES2022: Class Properties +//! Transform of class static blocks. + +use oxc_ast::ast::*; +use oxc_traverse::TraverseCtx; + +use super::super::ClassStaticBlock; + +use super::ClassProperties; + +impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Convert static block to `Expression`. + /// + /// `static { x = 1; }` -> `x = 1` + /// `static { x = 1; y = 2; } -> `(() => { x = 1; y = 2; })()` + /// + /// TODO: Add tests for this if there aren't any already. + /// Include tests for evaluation order inc that static block goes before class expression + /// unless also static properties, or static block uses class name. + pub(super) fn convert_static_block( + &mut self, + block: &mut StaticBlock<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // TODO: Convert `this` and references to class name. + // `x = class C { static { this.C = C; } }` -> `x = (_C = class C {}, _C.C = _C, _C)` + // TODO: Scope of static block contents becomes outer scope, not scope of class. + + // If class expression, assignment in static block moves to a position where it's read from. + // e.g.: `x` here now has read+write `ReferenceFlags`: + // `C = class C { static { x = 1; } }` -> `C = (_C = class C {}, x = 1, _C)` + let expr = ClassStaticBlock::convert_block_to_expression(block, ctx); + self.insert_expr_after_class(expr, ctx); + } +}