diff --git a/crates/oxc_transformer/src/es2022/class_properties/class.rs b/crates/oxc_transformer/src/es2022/class_properties/class.rs index 8cacb9bdfd3a7..64baf77bbb676 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/class.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/class.rs @@ -10,7 +10,7 @@ use oxc_syntax::{ scope::ScopeFlags, symbol::{SymbolFlags, SymbolId}, }; -use oxc_traverse::{BoundIdentifier, TraverseCtx}; +use oxc_traverse::{Ancestor, BoundIdentifier, TraverseCtx}; use crate::{common::helper_loader::Helper, TransformCtx}; @@ -20,177 +20,330 @@ use super::{ ClassBindings, ClassDetails, ClassProperties, FxIndexMap, PrivateProp, }; +// TODO(improve-on-babel): If outer scope is sloppy mode, all code which is moved to outside +// the class should be wrapped in an IIFE with `'use strict'` directive. Babel doesn't do this. + +// TODO: If static blocks transform is disabled, it's possible to get incorrect execution order. +// ```js +// class C { +// static x = console.log('x'); +// static { +// console.log('block'); +// } +// static y = console.log('y'); +// } +// ``` +// This logs "x", "block", "y". But in transformed output it'd be "block", "x", "y". +// Maybe force transform of static blocks if any static properties? +// Or alternatively could insert static property initializers into static blocks. + impl<'a, 'ctx> ClassProperties<'a, 'ctx> { - /// Transform class expression. - // `#[inline]` so that compiler sees that `expr` is an `Expression::ClassExpression`. - // Main guts of transform is broken out into `transform_class_expression_start` and - // `transform_class_expression_finish` to keep this function as small as possible. - // Want it to be inlined into `enter_expression` and for `enter_expression` to be inlined into parent. - #[inline] - pub(super) fn transform_class_expression( + /// Perform first phase of transformation of class. + /// + /// This is the only entry point into the transform upon entering class body. + /// + /// First we check if any transforms are necessary, and exit if not. + /// + /// If transform is required: + /// * Build a hashmap of private property keys. + /// * Push `ClassDetails` containing info about the class to `classes_stack`. + /// * Extract instance property initializers (public or private) from class body and insert into + /// class constructor. + /// * Temporarily replace computed keys of instance properties with assignments to temp vars. + /// `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` + pub(super) fn transform_class_body_on_entry( &mut self, - expr: &mut Expression<'a>, + body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>, ) { - let Expression::ClassExpression(class) = expr else { unreachable!() }; - - let class_address = class.address(); - let expr_count = self.transform_class_expression_start(class, class_address, ctx); - if expr_count > 0 { - self.transform_class_expression_finish(expr, expr_count, ctx); + // Ignore TS class declarations + // TODO: Is this correct? + let Ancestor::ClassBody(class) = ctx.parent() else { unreachable!() }; + if *class.declare() { + return; } - } - fn transform_class_expression_start( - &mut self, - class: &mut Class<'a>, - class_address: Address, - ctx: &mut TraverseCtx<'a>, - ) -> usize { - // Check this class isn't being visited twice - if *self.class_expression_addresses_stack.last() == class_address { - // This class has already been transformed, and we're now encountering it again - // in the sequence expression which was substituted for it. So don't transform it again! - // Returning 0 tells `enter_expression` not to call `transform_class_expression_finish` either. - self.class_expression_addresses_stack.pop(); - return 0; - } + // Get basic details about class + let is_declaration = match ctx.ancestor(1) { + Ancestor::ExportDefaultDeclarationDeclaration(_) + | Ancestor::ExportNamedDeclarationDeclaration(_) => true, + grandparent => grandparent.is_parent_of_statement(), + }; - self.transform_class(class, false, ctx); + let mut class_name_binding = class.id().as_ref().map(BoundIdentifier::from_binding_ident); + let class_scope_id = class.scope_id().get().unwrap(); + let has_super_class = class.super_class().is_some(); - // Return number of expressions to be inserted before/after the class - let mut expr_count = self.insert_before.len() + self.insert_after_exprs.len(); - if let Some(private_props) = &self.current_class().private_props { - expr_count += private_props.len(); + // Check if class has any properties or statick blocks, and locate constructor (if class has one) + let mut instance_prop_count = 0; + let mut has_static_prop = false; + let mut has_static_block = false; + // TODO: Store `FxIndexMap`s in a pool and re-use them + let mut private_props = FxIndexMap::default(); + let mut constructor = None; + for element in body.body.iter_mut() { + match element { + ClassElement::PropertyDefinition(prop) => { + // TODO: Throw error if property has decorators + + // Create binding for private property key + if let PropertyKey::PrivateIdentifier(ident) = &prop.key { + // Note: Current scope is outside class. + let binding = ctx.generate_uid_in_current_hoist_scope(&ident.name); + private_props.insert( + ident.name.clone(), + PrivateProp { binding, is_static: prop.r#static }, + ); + } + + if prop.r#static { + has_static_prop = true; + } else { + instance_prop_count += 1; + } + + continue; + } + ClassElement::StaticBlock(_) => { + // Static block only necessitates transforming class if it's being transformed + if self.transform_static_blocks { + has_static_block = true; + continue; + } + } + ClassElement::MethodDefinition(method) => { + if method.kind == MethodDefinitionKind::Constructor + && method.value.body.is_some() + { + constructor = Some(method); + } + } + ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { + // TODO: Need to handle these? + } + } } - if expr_count > 0 { - // We're going to replace class expression with a sequence expression - // `(..., _C = class C {}, ..., _C)`, so this class will be visited again. - // Store the `Address` of class in stack. This will cause bail-out when we re-visit it. - self.class_expression_addresses_stack.push(class_address); + // Exit if nothing to transform + if instance_prop_count == 0 && !has_static_prop && !has_static_block { + self.classes_stack.push(ClassDetails::empty(is_declaration)); + return; } - expr_count - } + // Initialize class binding vars. + // Static prop in class expression or anonymous `export default class {}` always require + // temp var for class. Static prop in class declaration doesn't. + let need_temp_var = has_static_prop && (!is_declaration || class_name_binding.is_none()); - /// Insert expressions before/after the class. - /// `C = class { [x()] = 1; static y = 2 };` - /// -> `C = (_x = x(), _Class = class C { constructor() { this[_x] = 1; } }, _Class.y = 2, _Class)` - fn transform_class_expression_finish( - &mut self, - expr: &mut Expression<'a>, - mut expr_count: usize, - ctx: &mut TraverseCtx<'a>, - ) { - // TODO: Name class if had no name, and name is statically knowable (as in example above). - // If class name shadows var which is referenced within class, rename that var. - // `var C = class { prop = C }; var C2 = C;` - // -> `var _C = class C { constructor() { this.prop = _C; } }; var C2 = _C;` - // This is really difficult as need to rename all references too to that binding too, - // which can be very far above the class in AST, when it's a `var`. - // Maybe for now only add class name if it doesn't shadow a var used within class? + let outer_hoist_scope_id = ctx.current_hoist_scope_id(); + let class_temp_binding = if need_temp_var { + let temp_binding = ClassBindings::create_temp_binding( + class_name_binding.as_ref(), + outer_hoist_scope_id, + ctx, + ); + if is_declaration { + // Anonymous `export default class {}`. Set class name binding to temp var. + // Actual class name will be set to this later. + class_name_binding = Some(temp_binding.clone()); + } else { + // Create temp var `var _Class;` statement. + // TODO(improve-on-babel): Inserting the temp var `var _Class` statement here is only + // to match Babel's output. It'd be simpler just to insert it at the end and get rid of + // `temp_var_is_created` that tracks whether it's done already or not. + self.ctx.var_declarations.insert_var(&temp_binding, ctx); + } + Some(temp_binding) + } else { + None + }; - // TODO: Deduct static private props from `expr_count`. - // Or maybe should store count and increment it when create private static props? - // They're probably pretty rare, so it'll be rarely used. - let class_details = self.classes_stack.last(); - expr_count += 1 + usize::from(class_details.bindings.temp.is_some()); + let static_private_fields_use_temp = !is_declaration; + let class_bindings = ClassBindings::new( + class_name_binding, + class_temp_binding, + outer_hoist_scope_id, + static_private_fields_use_temp, + need_temp_var, + ); - let mut exprs = ctx.ast.vec_with_capacity(expr_count); + // Add entry to `classes_stack` + self.classes_stack.push(ClassDetails { + is_declaration, + is_transform_required: true, + private_props: if private_props.is_empty() { None } else { Some(private_props) }, + bindings: class_bindings, + }); - // Insert `_prop = new WeakMap()` expressions for private instance props - // (or `_prop = _classPrivateFieldLooseKey("prop")` if loose mode). - // Babel has these always go first, regardless of order of class elements. - // Also insert `var _prop;` temp var declarations for private static props. - if let Some(private_props) = &class_details.private_props { - // Insert `var _prop;` declarations here rather than when binding was created to maintain - // same order of `var` declarations as Babel. - // `c = class C { #x = 1; static y = 2; }` -> `var _C, _x;` - // TODO(improve-on-babel): Simplify this. - if self.private_fields_as_properties { - exprs.extend(private_props.iter().map(|(name, prop)| { - // Insert `var _prop;` declaration - self.ctx.var_declarations.insert_var(&prop.binding, ctx); + // Exit if no instance properties (public or private) + if instance_prop_count == 0 { + return; + } - // `_prop = _classPrivateFieldLooseKey("prop")` - let value = Self::create_private_prop_key_loose(name, self.ctx, ctx); - create_assignment(&prop.binding, value, ctx) - })); + // Determine where to insert instance property initializers in constructor + let instance_inits_insert_location = if let Some(constructor) = constructor { + // Existing constructor + let constructor = constructor.value.as_mut(); + if has_super_class { + let (insert_scopes, insert_location) = + Self::replace_super_in_constructor(constructor, ctx); + self.instance_inits_scope_id = insert_scopes.insert_in_scope_id; + self.instance_inits_constructor_scope_id = insert_scopes.constructor_scope_id; + insert_location } else { - let mut weakmap_symbol_id = None; - exprs.extend(private_props.values().filter_map(|prop| { - // Insert `var _prop;` declaration - self.ctx.var_declarations.insert_var(&prop.binding, ctx); + let constructor_scope_id = constructor.scope_id(); + self.instance_inits_scope_id = constructor_scope_id; + // Only record `constructor_scope_id` if constructor's scope has some bindings. + // If it doesn't, no need to check for shadowed symbols in instance prop initializers, + // because no bindings to clash with. + self.instance_inits_constructor_scope_id = + if ctx.scopes().get_bindings(constructor_scope_id).is_empty() { + None + } else { + Some(constructor_scope_id) + }; + InstanceInitsInsertLocation::ExistingConstructor(0) + } + } else { + // No existing constructor - create scope for one + let constructor_scope_id = ctx.scopes_mut().add_scope( + Some(class_scope_id), + NodeId::DUMMY, + ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode, + ); + self.instance_inits_scope_id = constructor_scope_id; + self.instance_inits_constructor_scope_id = None; + InstanceInitsInsertLocation::NewConstructor + }; - if prop.is_static { - return None; + // Extract instance properties initializers. + // + // We leave the properties themselves in place, but take the initializers. + // `class C { prop = 123; }` -> `class { prop; }` + // Leave them in place to avoid shifting up all the elements twice. + // We already have to do that in exit phase, so better to do it all at once. + // + // Also replace any instance property computed keys with an assignment to temp var. + // `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` + // Those assignments will be moved to before class in exit phase of the transform. + // -> `_foo = foo(); class C {}` + let mut instance_inits = Vec::with_capacity(instance_prop_count); + let mut constructor = None; + for element in body.body.iter_mut() { + #[expect(clippy::match_same_arms)] + match element { + ClassElement::PropertyDefinition(prop) => { + if !prop.r#static { + self.convert_instance_property(prop, &mut instance_inits, ctx); } - - // `_prop = new WeakMap()` - let value = create_new_weakmap(&mut weakmap_symbol_id, ctx); - Some(create_assignment(&prop.binding, value, ctx)) - })); + } + ClassElement::MethodDefinition(method) => { + if method.kind == MethodDefinitionKind::Constructor + && method.value.body.is_some() + { + constructor = Some(method.value.as_mut()); + } + } + ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { + // TODO: Need to handle these? + } + ClassElement::StaticBlock(_) => {} } } - // Insert computed key initializers - exprs.extend(self.insert_before.drain(..)); - - // Insert class + static property assignments + static blocks - let class_expr = ctx.ast.move_expression(expr); - if let Some(binding) = &class_details.bindings.temp { - // Insert `var _Class` statement, if it wasn't already in `transform_class` - if !class_details.bindings.temp_var_is_created { - self.ctx.var_declarations.insert_var(binding, ctx); + // Insert instance initializers into constructor. + // Create a constructor if there isn't one. + match instance_inits_insert_location { + InstanceInitsInsertLocation::NewConstructor => { + self.insert_constructor(body, instance_inits, has_super_class, ctx); + } + InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => { + self.insert_inits_into_constructor_as_statements( + constructor.as_mut().unwrap(), + instance_inits, + stmt_index, + ctx, + ); + } + InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => { + self.create_super_function_inside_constructor( + constructor.as_mut().unwrap(), + instance_inits, + &super_binding, + ctx, + ); + } + InstanceInitsInsertLocation::SuperFnOutsideClass(super_binding) => { + self.create_super_function_outside_constructor(instance_inits, &super_binding, ctx); } - - // `_Class = class {}` - let assignment = create_assignment(binding, class_expr, ctx); - exprs.push(assignment); - // Add static property assignments + static blocks - exprs.extend(self.insert_after_exprs.drain(..)); - // `_Class` - exprs.push(binding.create_read_expression(ctx)); - } else { - // Add static blocks (which didn't reference class name) - // TODO: If class has `extends` clause, and it may have side effects, then static block contents - // goes after class expression, and temp var is called `_temp` not `_Class`. - // `let C = class extends Unbound { static { x = 1; } };` - // -> `var _temp; let C = ((_temp = class C extends Unbound {}), (x = 1), _temp);` - // `let C = class extends Bound { static { x = 1; } };` - // -> `let C = ((x = 1), class C extends Bound {});` - exprs.extend(self.insert_after_exprs.drain(..)); - - exprs.push(class_expr); } - - *expr = ctx.ast.expression_sequence(SPAN, exprs); } - /// Transform class declaration. - pub(super) fn transform_class_declaration( + /// Transform class declaration on exit. + /// + /// This is the exit phase of the transform. Only applies to class *declarations*. + /// Class *expressions* are handled in [`ClassProperties::transform_class_expression_on_exit`] below. + /// Both functions do much the same, `transform_class_expression_on_exit` inserts items as expressions, + /// whereas here we insert as statements. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties and static blocks from class body. + /// + /// Items needing insertion before/after class are inserted as statements. + /// + /// `class C { static [foo()] = 123; static { bar(); } }` + /// -> `_foo = foo(); class C {}; C[_foo] = 123; bar();` + pub(super) fn transform_class_declaration_on_exit( &mut self, class: &mut Class<'a>, - stmt_address: Address, ctx: &mut TraverseCtx<'a>, ) { // Ignore TS class declarations // TODO: Is this correct? - // TODO: If remove this check, remove from `transform_class_on_exit` too. if class.declare { return; } - self.transform_class(class, true, ctx); + // Leave class expressions to `transform_class_expression_on_exit` + let class_details = self.current_class_mut(); + if !class_details.is_declaration { + return; + } + + // Finish transform + if class_details.is_transform_required { + self.transform_class_declaration_on_exit_impl(class, ctx); + } else { + debug_assert!(class_details.bindings.temp.is_none()); + } + + // Pop off stack. We're done! + self.classes_stack.pop(); + } + + fn transform_class_declaration_on_exit_impl( + &mut self, + class: &mut Class<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + // Static properties and static blocks + self.current_class_mut().bindings.static_private_fields_use_temp = true; - // TODO: Run other transforms on inserted statements. How? + // Transform static properties, remove static and instance properties, and move computed keys + // to before class + self.transform_class_elements(class, ctx); + // Insert temp var for class if required. Name class if required. let class_details = self.classes_stack.last_mut(); if let Some(temp_binding) = &class_details.bindings.temp { // Binding for class name is required if let Some(ident) = &class.id { - // Insert `var _Class` statement, if it wasn't already in `transform_class` + // Insert `var _Class` statement, if it wasn't already in entry phase if !class_details.bindings.temp_var_is_created { self.ctx.var_declarations.insert_var(temp_binding, ctx); } @@ -211,7 +364,14 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } } - // Insert expressions before/after class + // Insert statements before/after class + let stmt_address = match ctx.parent() { + parent @ (Ancestor::ExportDefaultDeclarationDeclaration(_) + | Ancestor::ExportNamedDeclarationDeclaration(_)) => parent.address(), + // `Class` is always stored in a `Box`, so has a stable memory location + _ => Address::from_ptr(class), + }; + if !self.insert_before.is_empty() { self.ctx.statement_injector.insert_many_before( &stmt_address, @@ -252,206 +412,187 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { .statement_injector .insert_many_after(&stmt_address, self.insert_after_stmts.drain(..)); } - - // Flag that static private fields should be transpiled using name binding, - // while traversing class body. - // - // Static private fields reference class name (not temp var) in class declarations. - // `class Class { static #prop; method() { return obj.#prop; } }` - // -> `method() { return _assertClassBrand(Class, obj, _prop)._; }` - // (note `Class` in `_assertClassBrand(Class, ...)`, not `_Class`) - // - // Also see comments on `ClassBindings`. - // - // Note: If declaration is `export default class {}` with no name, and class has static props, - // then class has had name binding created already in `transform_class`. - // So name binding is always `Some`. - class_details.bindings.static_private_fields_use_temp = false; } - /// `_classPrivateFieldLooseKey("prop")` - fn create_private_prop_key_loose( - name: &Atom<'a>, - transform_ctx: &TransformCtx<'a>, + /// Transform class expression on exit. + /// + /// This is the exit phase of the transform. Only applies to class *expressions*. + /// Class *expressions* are handled in [`ClassProperties::transform_class_declaration_on_exit`] above. + /// Both functions do much the same, `transform_class_declaration_on_exit` inserts items as statements, + /// whereas here we insert as expressions. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties and static blocks from class body. + /// + /// Items needing insertion before/after class are inserted as expressions around, surrounding class + /// in a [`SequenceExpression`]. + /// + /// `let C = class { static [foo()] = 123; static { bar(); } };` + /// -> `let C = (_foo = foo(), _Class = class {}, _Class[_foo] = 123, bar(), _Class);` + pub(super) fn transform_class_expression_on_exit( + &mut self, + expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - transform_ctx.helper_call_expr( - Helper::ClassPrivateFieldLooseKey, - SPAN, - ctx.ast.vec1(Argument::from(ctx.ast.expression_string_literal( - SPAN, - name.clone(), - None, - ))), - ctx, - ) + ) { + let Expression::ClassExpression(class) = expr else { unreachable!() }; + + // Ignore TS class declarations + // TODO: Is this correct? + if class.declare { + return; + } + + // Finish transform + let class_details = self.current_class(); + if class_details.is_transform_required { + self.transform_class_expression_on_exit_impl(expr, ctx); + } else { + debug_assert!(class_details.bindings.temp.is_none()); + } + + // Pop off stack. We're done! + self.classes_stack.pop(); } - /// Main guts of the transform. - fn transform_class( + fn transform_class_expression_on_exit_impl( &mut self, - class: &mut Class<'a>, - is_declaration: bool, + expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>, ) { - // TODO(improve-on-babel): If outer scope is sloppy mode, all code which is moved to outside - // the class should be wrapped in an IIFE with `'use strict'` directive. Babel doesn't do this. - - // TODO: If static blocks transform is disabled, it's possible to get incorrect execution order. - // ```js - // class C { - // static x = console.log('x'); - // static { - // console.log('block'); - // } - // static y = console.log('y'); - // } - // ``` - // This logs "x", "block", "y". But in transformed output it'd be "block", "x", "y". - // Maybe force transform of static blocks if any static properties? - // Or alternatively could insert static property initializers into static blocks. - - // Check if class has any properties and get index of constructor (if class has one) - let mut instance_prop_count = 0; - let mut has_static_prop = false; - let mut has_static_block = false; - // TODO: Store `FxIndexMap`s in a pool and re-use them - let mut private_props = FxIndexMap::default(); - let mut constructor = None; - for element in class.body.body.iter_mut() { - match element { - ClassElement::PropertyDefinition(prop) => { - // TODO: Throw error if property has decorators + let Expression::ClassExpression(class) = expr else { unreachable!() }; - // Create binding for private property key - if let PropertyKey::PrivateIdentifier(ident) = &prop.key { - // Note: Current scope is outside class. - let binding = ctx.generate_uid_in_current_hoist_scope(&ident.name); - private_props.insert( - ident.name.clone(), - PrivateProp { binding, is_static: prop.r#static }, - ); - } + // Transform static properties, remove static and instance properties, and move computed keys + // to before class + self.transform_class_elements(class, ctx); - if prop.r#static { - has_static_prop = true; - } else { - instance_prop_count += 1; - } + // Insert expressions before / after class. + // `C = class { [x()] = 1; static y = 2 };` + // -> `C = (_x = x(), _Class = class C { constructor() { this[_x] = 1; } }, _Class.y = 2, _Class)` - continue; - } - ClassElement::StaticBlock(_) => { - // Static block only necessitates transforming class if it's being transformed - if self.transform_static_blocks { - has_static_block = true; - continue; - } - } - ClassElement::MethodDefinition(method) => { - if method.kind == MethodDefinitionKind::Constructor - && method.value.body.is_some() - { - constructor = Some(method); - } - } - ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { - // TODO: Need to handle these? - } - } + // TODO: Name class if had no name, and name is statically knowable (as in example above). + // If class name shadows var which is referenced within class, rename that var. + // `var C = class { prop = C }; var C2 = C;` + // -> `var _C = class C { constructor() { this.prop = _C; } }; var C2 = _C;` + // This is really difficult as need to rename all references to that binding too, + // which can be very far above the class in AST, when it's a `var`. + // Maybe for now only add class name if it doesn't shadow a var used within class? + + // TODO: Deduct static private props from `expr_count`. + // Or maybe should store count and increment it when create private static props? + // They're probably pretty rare, so it'll be rarely used. + let class_details = self.classes_stack.last(); + + let mut expr_count = self.insert_before.len() + self.insert_after_exprs.len(); + if let Some(private_props) = &class_details.private_props { + expr_count += private_props.len(); } - // Exit if nothing to transform - if instance_prop_count == 0 && !has_static_prop && !has_static_block { - self.classes_stack.push(ClassDetails::default()); + // Exit if no expressions to insert before or after class + if expr_count == 0 { return; } - // Initialize class binding vars. - // Static prop in class expression or anonymous `export default class {}` always require - // temp var for class. Static prop in class declaration doesn't. - let mut class_name_binding = class.id.as_ref().map(BoundIdentifier::from_binding_ident); + expr_count += 1 + usize::from(class_details.bindings.temp.is_some()); - let need_temp_var = has_static_prop && (!is_declaration || class.id.is_none()); + let mut exprs = ctx.ast.vec_with_capacity(expr_count); - let class_temp_binding = if need_temp_var { - let temp_binding = ClassBindings::create_temp_binding(class_name_binding.as_ref(), ctx); - if is_declaration { - // Anonymous `export default class {}`. Set class name binding to temp var. - // Actual class name will be set to this later. - class_name_binding = Some(temp_binding.clone()); + // Insert `_prop = new WeakMap()` expressions for private instance props + // (or `_prop = _classPrivateFieldLooseKey("prop")` if loose mode). + // Babel has these always go first, regardless of order of class elements. + // Also insert `var _prop;` temp var declarations for private static props. + if let Some(private_props) = &class_details.private_props { + // Insert `var _prop;` declarations here rather than when binding was created to maintain + // same order of `var` declarations as Babel. + // `c = class C { #x = 1; static y = 2; }` -> `var _C, _x;` + // TODO(improve-on-babel): Simplify this. + if self.private_fields_as_properties { + exprs.extend(private_props.iter().map(|(name, prop)| { + // Insert `var _prop;` declaration + self.ctx.var_declarations.insert_var(&prop.binding, ctx); + + // `_prop = _classPrivateFieldLooseKey("prop")` + let value = Self::create_private_prop_key_loose(name, self.ctx, ctx); + create_assignment(&prop.binding, value, ctx) + })); } else { - // Create temp var `var _Class;` statement. - // TODO(improve-on-babel): Inserting the temp var `var _Class` statement here is only - // to match Babel's output. It'd be simpler just to insert it at the end and get rid of - // `temp_var_is_created` that tracks whether it's done already or not. - self.ctx.var_declarations.insert_var(&temp_binding, ctx); - } - Some(temp_binding) - } else { - None - }; + let mut weakmap_symbol_id = None; + exprs.extend(private_props.values().filter_map(|prop| { + // Insert `var _prop;` declaration + self.ctx.var_declarations.insert_var(&prop.binding, ctx); - let class_bindings = - ClassBindings::new(class_name_binding, class_temp_binding, need_temp_var); + if prop.is_static { + return None; + } - // Add entry to `classes_stack` - self.classes_stack.push(ClassDetails { - is_declaration, - private_props: if private_props.is_empty() { None } else { Some(private_props) }, - bindings: class_bindings, - }); + // `_prop = new WeakMap()` + let value = create_new_weakmap(&mut weakmap_symbol_id, ctx); + Some(create_assignment(&prop.binding, value, ctx)) + })); + } + } - // Determine where to insert instance property initializers in constructor - let instance_inits_insert_location = if instance_prop_count == 0 { - // No instance prop initializers to insert - None - } else if let Some(constructor) = constructor { - // Existing constructor - let constructor = constructor.value.as_mut(); - if class.super_class.is_some() { - let (insert_scopes, insert_location) = - Self::replace_super_in_constructor(constructor, ctx); - self.instance_inits_scope_id = insert_scopes.insert_in_scope_id; - self.instance_inits_constructor_scope_id = insert_scopes.constructor_scope_id; - Some(insert_location) - } else { - let constructor_scope_id = constructor.scope_id(); - self.instance_inits_scope_id = constructor_scope_id; - // Only record `constructor_scope_id` if constructor's scope has some bindings. - // If it doesn't, no need to check for shadowed symbols in instance prop initializers, - // because no bindings to clash with. - self.instance_inits_constructor_scope_id = - if ctx.scopes().get_bindings(constructor_scope_id).is_empty() { - None - } else { - Some(constructor_scope_id) - }; - Some(InstanceInitsInsertLocation::ExistingConstructor(0)) + // Insert computed key initializers + exprs.extend(self.insert_before.drain(..)); + + // Insert class + static property assignments + static blocks + let class_expr = ctx.ast.move_expression(expr); + if let Some(binding) = &class_details.bindings.temp { + // Insert `var _Class` statement, if it wasn't already in entry phase + if !class_details.bindings.temp_var_is_created { + self.ctx.var_declarations.insert_var(binding, ctx); } + + // `_Class = class {}` + let assignment = create_assignment(binding, class_expr, ctx); + exprs.push(assignment); + // Add static property assignments + static blocks + exprs.extend(self.insert_after_exprs.drain(..)); + // `_Class` + exprs.push(binding.create_read_expression(ctx)); } else { - // No existing constructor - create scope for one - let constructor_scope_id = ctx.scopes_mut().add_scope( - Some(class.scope_id()), - NodeId::DUMMY, - ScopeFlags::Function | ScopeFlags::Constructor | ScopeFlags::StrictMode, - ); - self.instance_inits_scope_id = constructor_scope_id; - self.instance_inits_constructor_scope_id = None; - Some(InstanceInitsInsertLocation::NewConstructor) - }; + // Add static blocks (which didn't reference class name) + // TODO: If class has `extends` clause, and it may have side effects, then static block contents + // goes after class expression, and temp var is called `_temp` not `_Class`. + // `let C = class extends Unbound { static { x = 1; } };` + // -> `var _temp; let C = ((_temp = class C extends Unbound {}), (x = 1), _temp);` + // `let C = class extends Bound { static { x = 1; } };` + // -> `let C = ((x = 1), class C extends Bound {});` + exprs.extend(self.insert_after_exprs.drain(..)); - // Extract properties and static blocks from class body + substitute computed method keys - let mut instance_inits = Vec::with_capacity(instance_prop_count); - let mut constructor_index = 0; - let mut index_not_including_removed = 0; + exprs.push(class_expr); + } + + debug_assert!(exprs.len() > 1); + debug_assert!(exprs.len() <= expr_count); + + *expr = ctx.ast.expression_sequence(SPAN, exprs); + } + + /// Transform class elements. + /// + /// This is part of the exit phase of transform, performed on both class declarations + /// and class expressions. + /// + /// At this point, other transforms have had a chance to run on the class body, so we can move + /// parts of the code out to before/after the class now. + /// + /// * Transform static properties and insert after class. + /// * Transform static blocks and insert after class. + /// * Extract computed key assignments and insert them before class. + /// * Remove all properties and static blocks from class body. + fn transform_class_elements(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { class.body.body.retain_mut(|element| { match element { ClassElement::PropertyDefinition(prop) => { if prop.r#static { self.convert_static_property(prop, ctx); - } else { - self.convert_instance_property(prop, &mut instance_inits, ctx); + } else if prop.computed { + self.extract_instance_prop_computed_key(prop, ctx); } return false; } @@ -462,47 +603,70 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } } ClassElement::MethodDefinition(method) => { - if method.kind == MethodDefinitionKind::Constructor { - if method.value.body.is_some() { - constructor_index = index_not_including_removed; - } - } else { - self.substitute_temp_var_for_method_computed_key(method, ctx); - } + self.substitute_temp_var_for_method_computed_key(method, ctx); } ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => { // TODO: Need to handle these? } } - index_not_including_removed += 1; - true }); + } - // Insert instance initializers into constructor, or create constructor if there is none - if let Some(instance_inits_insert_location) = instance_inits_insert_location { - self.insert_instance_inits( - class, - instance_inits, - &instance_inits_insert_location, - constructor_index, - ctx, - ); - } + /// Flag that static private fields should be transpiled using temp binding, + /// while in this static property or static block. + /// + /// We turn `static_private_fields_use_temp` on and off when entering exiting static context. + /// + /// Static private fields reference class name (not temp var) in class declarations. + /// `class Class { static #privateProp; method() { return obj.#privateProp; } }` + /// -> `method() { return _assertClassBrand(Class, obj, _privateProp)._; }` + /// ^^^^^ + /// + /// But in static properties and static blocks, they use the temp var. + /// `class Class { static #privateProp; static publicProp = obj.#privateProp; }` + /// -> `Class.publicProp = _assertClassBrand(_Class, obj, _privateProp)._` + /// ^^^^^^ + /// + /// Also see comments on `ClassBindings`. + /// + /// Note: If declaration is `export default class {}` with no name, and class has static props, + /// then class has had name binding created already in `transform_class`. + /// So name binding is always `Some`. + pub(super) fn flag_entering_static_property_or_block(&mut self) { + // No need to check if class is a declaration, because `static_private_fields_use_temp` + // is always `true` for class expressions anyway + self.current_class_mut().bindings.static_private_fields_use_temp = true; } - /// Pop from private props stack. - // `#[inline]` because this is function is so small - #[inline] - pub(super) fn transform_class_on_exit(&mut self, class: &Class) { - // Ignore TS class declarations - // TODO: Is this correct? - if class.declare { - return; + /// Flag that static private fields should be transpiled using name binding again + /// as we're exiting this static property or static block. + /// (see [`ClassProperties::flag_entering_static_property_or_block`]) + pub(super) fn flag_exiting_static_property_or_block(&mut self) { + // Flag that transpiled static private props use name binding in class declarations + let class_details = self.current_class_mut(); + if class_details.is_declaration { + class_details.bindings.static_private_fields_use_temp = false; } + } - self.classes_stack.pop(); + /// `_classPrivateFieldLooseKey("prop")` + fn create_private_prop_key_loose( + name: &Atom<'a>, + transform_ctx: &TransformCtx<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + transform_ctx.helper_call_expr( + Helper::ClassPrivateFieldLooseKey, + SPAN, + ctx.ast.vec1(Argument::from(ctx.ast.expression_string_literal( + SPAN, + name.clone(), + None, + ))), + ctx, + ) } /// Insert an expression after the class. @@ -522,6 +686,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Create `new WeakMap()` expression. /// /// Takes an `&mut Option>` which is updated after looking up the binding for `WeakMap`. +/// /// * `None` = Not looked up yet. /// * `Some(None)` = Has been looked up, and `WeakMap` is unbound. /// * `Some(Some(symbol_id))` = Has been looked up, and `WeakMap` has a local binding. diff --git a/crates/oxc_transformer/src/es2022/class_properties/class_bindings.rs b/crates/oxc_transformer/src/es2022/class_properties/class_bindings.rs index ad3e52bbefc42..b5bf61be74493 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/class_bindings.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/class_bindings.rs @@ -1,4 +1,7 @@ -use oxc_syntax::symbol::SymbolId; +use oxc_syntax::{ + scope::ScopeId, + symbol::{SymbolFlags, SymbolId}, +}; use oxc_traverse::{BoundIdentifier, TraverseCtx}; /// Store for bindings for class. @@ -9,12 +12,12 @@ use oxc_traverse::{BoundIdentifier, TraverseCtx}; /// Temp var is required in the following circumstances: /// /// * Class expression has static properties. -/// e.g. `C = class { x = 1; }` +/// e.g. `C = class { static x = 1; }` /// * Class declaration has static properties and one of the static prop's initializers contains: /// a. `this` -/// e.g. `class C { x = this; }` +/// e.g. `class C { static x = this; }` /// b. Reference to class name -/// e.g. `class C { x = C; }` +/// e.g. `class C { static x = C; }` /// c. A private field referring to one of the class's static private props. /// e.g. `class C { static #x; static y = obj.#x; }` /// @@ -35,13 +38,14 @@ use oxc_traverse::{BoundIdentifier, TraverseCtx}; /// /// `static_private_fields_use_temp` is updated as transform moves through the class, /// to indicate which binding to use. -#[derive(Default, Clone)] pub(super) struct ClassBindings<'a> { /// Binding for class name, if class has name pub name: Option>, /// Temp var for class. /// e.g. `_Class` in `_Class = class {}, _Class.x = 1, _Class` pub temp: Option>, + /// `ScopeId` of hoist scope outside class (which temp `var` binding would be created in) + pub outer_hoist_scope_id: ScopeId, /// `true` if should use temp binding for references to class in transpiled static private fields, /// `false` if can use name binding pub static_private_fields_use_temp: bool, @@ -50,20 +54,30 @@ pub(super) struct ClassBindings<'a> { } impl<'a> ClassBindings<'a> { - /// Create `ClassBindings`. + /// Create new `ClassBindings`. pub fn new( name_binding: Option>, temp_binding: Option>, + outer_scope_id: ScopeId, + static_private_fields_use_temp: bool, temp_var_is_created: bool, ) -> Self { Self { name: name_binding, temp: temp_binding, - static_private_fields_use_temp: true, + outer_hoist_scope_id: outer_scope_id, + static_private_fields_use_temp, temp_var_is_created, } } + /// Create dummy `ClassBindings`. + /// + /// Used when class needs no transform, and for dummy entry at top of `ClassesStack`. + pub fn dummy() -> Self { + Self::new(None, None, ScopeId::new(0), false, false) + } + /// Get `SymbolId` of name binding. pub fn name_symbol_id(&self) -> Option { self.name.as_ref().map(|binding| binding.symbol_id) @@ -88,7 +102,9 @@ impl<'a> ClassBindings<'a> { ) -> &BoundIdentifier<'a> { if self.static_private_fields_use_temp { // Create temp binding if doesn't already exist - self.temp.get_or_insert_with(|| Self::create_temp_binding(self.name.as_ref(), ctx)) + self.temp.get_or_insert_with(|| { + Self::create_temp_binding(self.name.as_ref(), self.outer_hoist_scope_id, ctx) + }) } else { // `static_private_fields_use_temp` is always `true` for class expressions. // Class declarations always have a name binding if they have any static props. @@ -100,12 +116,13 @@ impl<'a> ClassBindings<'a> { /// Generate binding for temp var. pub fn create_temp_binding( name_binding: Option<&BoundIdentifier<'a>>, + outer_hoist_scope_id: ScopeId, ctx: &mut TraverseCtx<'a>, ) -> BoundIdentifier<'a> { // Base temp binding name on class name, or "Class" if no name. // TODO(improve-on-babel): If class name var isn't mutated, no need for temp var for // class declaration. Can just use class binding. let name = name_binding.map_or("Class", |binding| binding.name.as_str()); - ctx.generate_uid_in_current_hoist_scope(name) + ctx.generate_uid(name, outer_hoist_scope_id, SymbolFlags::FunctionScopedVariable) } } diff --git a/crates/oxc_transformer/src/es2022/class_properties/class_details.rs b/crates/oxc_transformer/src/es2022/class_properties/class_details.rs index e77f85f031277..6aa42fae1f258 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/class_details.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/class_details.rs @@ -8,10 +8,11 @@ use super::{ClassBindings, ClassProperties, FxIndexMap}; /// Details of a class. /// /// These are stored in `ClassesStack`. -#[derive(Default)] pub(super) struct ClassDetails<'a> { /// `true` for class declaration, `false` for class expression pub is_declaration: bool, + /// `true` if class requires no transformation + pub is_transform_required: bool, /// Private properties. /// Mapping private prop name to binding for temp var. /// This is then used as lookup when transforming e.g. `this.#x`. @@ -21,6 +22,20 @@ pub(super) struct ClassDetails<'a> { pub bindings: ClassBindings<'a>, } +impl<'a> ClassDetails<'a> { + /// Create empty `ClassDetails`. + /// + /// Used when class needs no transform, and for dummy entry at top of `ClassesStack`. + pub fn empty(is_declaration: bool) -> Self { + Self { + is_declaration, + is_transform_required: false, + private_props: None, + bindings: ClassBindings::dummy(), + } + } +} + /// Details of a private property. pub(super) struct PrivateProp<'a> { pub binding: BoundIdentifier<'a>, @@ -28,6 +43,7 @@ pub(super) struct PrivateProp<'a> { } /// Stack of `ClassDetails`. +/// /// Pushed to when entering a class, popped when exiting. /// /// We use a `NonEmptyStack` to make `last` and `last_mut` cheap (these are used a lot). @@ -37,12 +53,17 @@ pub(super) struct PrivateProp<'a> { /// to work around borrow-checker. You can call `find_private_prop` and retain the return value /// without holding a mut borrow of the whole of `&mut ClassProperties`. This allows accessing other /// properties of `ClassProperties` while that borrow is held. -#[derive(Default)] pub(super) struct ClassesStack<'a> { stack: NonEmptyStack>, } impl<'a> ClassesStack<'a> { + /// Create new `ClassesStack`. + pub fn new() -> Self { + // Default stack capacity is 4. That's is probably good. More than 4 nested classes is rare. + Self { stack: NonEmptyStack::new(ClassDetails::empty(false)) } + } + /// Push an entry to stack. #[inline] pub fn push(&mut self, class: ClassDetails<'a>) { diff --git a/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs b/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs index 8c5d8a86986c6..da0e84cfbfe7c 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/computed_key.rs @@ -34,8 +34,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // 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) - let key = ctx.ast.move_expression(key); - let temp_var = self.create_computed_key_temp_var(key, ctx); + let original_key = ctx.ast.move_expression(key); + let (assignment, temp_var) = self.create_computed_key_temp_var(original_key, ctx); + self.insert_before.push(assignment); method.key = PropertyKey::from(temp_var); } @@ -52,43 +53,89 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// This function: /// * Creates the `let _x;` statement and inserts it. /// * Creates the `_x = x()` assignment. - /// * Inserts assignment before class. + /// * If static prop, inserts assignment before class. + /// * If instance prop, replaces existing key with assignment (it'll be moved to before class later). /// * Returns `_x`. pub(super) fn create_computed_key_temp_var_if_required( &mut self, key: &mut Expression<'a>, + is_static: bool, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { - let key = ctx.ast.move_expression(key); - if key_needs_temp_var(&key, ctx) { - self.create_computed_key_temp_var(key, ctx) + let original_key = ctx.ast.move_expression(key); + if key_needs_temp_var(&original_key, ctx) { + let (assignment, ident) = self.create_computed_key_temp_var(original_key, ctx); + if is_static { + self.insert_before.push(assignment); + } else { + *key = assignment; + } + ident } else { - key + original_key } } - /// * Create `let _x;` statement and insert it. - /// * Create `_x = x()` assignment. - /// * Insert assignment before class. - /// * Return `_x`. + /// Create `let _x;` statement and insert it. + /// Return `_x = x()` assignment, and `_x` identifier referencing same temp var. fn create_computed_key_temp_var( &mut self, key: Expression<'a>, ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - // 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(); + ) -> (/* assignment */ Expression<'a>, /* identifier */ Expression<'a>) { + let outer_scope_id = ctx.current_block_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); + ctx.generate_uid_based_on_node(&key, outer_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); + let ident = binding.create_read_expression(ctx); + + (assignment, ident) + } + + /// Extract computed key if it's an assignment, and replace with identifier. + /// + /// In entry phase, computed keys for instance properties are converted to assignments to temp vars. + /// `class C { [foo()] = 123 }` + /// -> `class C { [_foo = foo()]; constructor() { this[_foo] = 123; } }` + /// + /// Now in exit phase, extract this assignment and move it to before class. + /// + /// `class C { [_foo = foo()]; constructor() { this[_foo] = 123; } }` + /// -> `_foo = foo(); class C { [null]; constructor() { this[_foo] = 123; } }` + /// (`[null]` property will be removed too by caller) + /// + /// We do this process in 2 passes so that the computed key is still present within the class during + /// traversal of the class body, so any other transforms can run on it. + /// Now that we're exiting the class, we can move the assignment `_foo = foo()` out of the class + /// to where it needs to be. + pub(super) fn extract_instance_prop_computed_key( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &TraverseCtx<'a>, + ) { + // Exit if computed key is not an assignment (wasn't processed in 1st pass). + let PropertyKey::AssignmentExpression(assign_expr) = &prop.key else { return }; + + // Debug checks that we're removing what we think we are + #[cfg(debug_assertions)] + { + assert!(assign_expr.span.is_empty()); + let AssignmentTarget::AssignmentTargetIdentifier(ident) = &assign_expr.left else { + unreachable!(); + }; + assert!(ident.name.starts_with('_')); + assert!(ctx.symbols().get_reference(ident.reference_id()).symbol_id().is_some()); + assert!(ident.span.is_empty()); + assert!(prop.value.is_none()); + } - binding.create_read_expression(ctx) + // Extract assignment from computed key and insert before class + let assignment = ctx.ast.move_property_key(&mut prop.key).into_expression(); + self.insert_before.push(assignment); } } diff --git a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs index 307b527c271fc..f577e6f9140e3 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/constructor.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/constructor.rs @@ -207,54 +207,17 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { (insert_scopes, insert_location) } - /// Insert instance property initializers. - pub(super) fn insert_instance_inits( - &mut self, - class: &mut Class<'a>, - inits: Vec>, - insertion_location: &InstanceInitsInsertLocation<'a>, - constructor_index: usize, - ctx: &mut TraverseCtx<'a>, - ) { - // TODO: Handle private props in constructor params `class C { #x; constructor(x = this.#x) {} }`. - - match insertion_location { - InstanceInitsInsertLocation::NewConstructor => { - self.insert_constructor(class, inits, ctx); - } - InstanceInitsInsertLocation::ExistingConstructor(stmt_index) => { - self.insert_inits_into_constructor_as_statements( - class, - inits, - constructor_index, - *stmt_index, - ctx, - ); - } - InstanceInitsInsertLocation::SuperFnInsideConstructor(super_binding) => { - self.create_super_function_inside_constructor( - class, - inits, - super_binding, - constructor_index, - ctx, - ); - } - InstanceInitsInsertLocation::SuperFnOutsideClass(super_binding) => { - self.create_super_function_outside_constructor(inits, super_binding, ctx); - } - } - } + // TODO: Handle private props in constructor params `class C { #x; constructor(x = this.#x) {} }`. /// Add a constructor to class containing property initializers. - fn insert_constructor( + pub(super) fn insert_constructor( &self, - class: &mut Class<'a>, + body: &mut ClassBody<'a>, inits: Vec>, + has_super_class: bool, ctx: &mut TraverseCtx<'a>, ) { - // Create statements to go in function body. - let has_super_class = class.super_class.is_some(); + // Create statements to go in function body let mut stmts = ctx.ast.vec_with_capacity(inits.len() + usize::from(has_super_class)); // Add `super(..._args);` statement and `..._args` param if class has a super class. @@ -307,20 +270,18 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { )); // TODO(improve-on-babel): Could push constructor onto end of elements, instead of inserting as first - class.body.body.insert(0, ctor); + body.body.insert(0, ctor); } /// Insert instance property initializers into constructor body at `insertion_index`. - fn insert_inits_into_constructor_as_statements( + pub(super) fn insert_inits_into_constructor_as_statements( &mut self, - class: &mut Class<'a>, + constructor: &mut Function<'a>, inits: Vec>, - constructor_index: usize, insertion_index: usize, ctx: &mut TraverseCtx<'a>, ) { // Rename any symbols in constructor which clash with references in inits - let constructor = Self::get_constructor(class, constructor_index); self.rename_clashing_symbols(constructor, ctx); // Insert inits into constructor body @@ -331,16 +292,14 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Create `_super` function containing instance property initializers, /// and insert at top of constructor body. /// `var _super = (..._args) => (super(..._args), , this);` - fn create_super_function_inside_constructor( + pub(super) fn create_super_function_inside_constructor( &mut self, - class: &mut Class<'a>, + constructor: &mut Function<'a>, inits: Vec>, super_binding: &BoundIdentifier<'a>, - constructor_index: usize, ctx: &mut TraverseCtx<'a>, ) { // Rename any symbols in constructor which clash with references in inits - let constructor = Self::get_constructor(class, constructor_index); self.rename_clashing_symbols(constructor, ctx); // `(super(..._args), , this)` @@ -405,14 +364,16 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Create `_super` function containing instance property initializers, /// and insert it outside class. /// `let _super = function() { ; return this; }` - fn create_super_function_outside_constructor( + pub(super) fn create_super_function_outside_constructor( &mut self, inits: Vec>, super_binding: &BoundIdentifier<'a>, ctx: &mut TraverseCtx<'a>, ) { // Add `"use strict"` directive if outer scope is not strict mode - let directives = if ctx.current_scope_flags().is_strict_mode() { + // TODO: This should be parent scope if insert `_super` function as expression before class expression. + let outer_scope_id = ctx.current_block_scope_id(); + let directives = if ctx.scopes().get_flags(outer_scope_id).is_strict_mode() { ctx.ast.vec() } else { ctx.ast.vec1(ctx.ast.use_strict_directive()) @@ -445,8 +406,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { )); // Insert `_super` function after class. - // Note: Inserting it after class not before, so that other transforms run on it. - // TODO: That doesn't work - other transforms do not run on it. + // TODO: Need to add `_super` function to class as a static method, and then remove it again + // in exit phase - so other transforms run on it in between. + // TODO: Need to transform `super` and references to class name in initializers. // TODO: If static block transform is not enabled, it's possible to construct the class // within the static block `class C { static { new C() } }` and that'd run before `_super` // is defined. So it needs to go before the class, not after, in that case. @@ -455,6 +417,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } else { let assignment = create_assignment(super_binding, super_func, ctx); // TODO: Why does this end up before class, not after? + // TODO: This isn't right. Should not be adding to `insert_after_exprs` in entry phase. self.insert_after_exprs.push(assignment); None }; @@ -490,20 +453,6 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // Empty `clashing_constructor_symbols` hashmap for reuse on next class clashing_symbols.clear(); } - - /// Get `Function` for constructor, given constructor's index within class elements. - fn get_constructor<'b>( - class: &'b mut Class<'a>, - constructor_index: usize, - ) -> &'b mut Function<'a> { - let Some(ClassElement::MethodDefinition(method)) = - class.body.body.get_mut(constructor_index) - else { - unreachable!() - }; - debug_assert!(method.kind == MethodDefinitionKind::Constructor); - &mut method.value - } } /// Visitor for transforming `super()` in class constructor params. @@ -550,7 +499,7 @@ impl<'a, 'c> ConstructorParamsSuperReplacer<'a, 'c> { let insert_location = InstanceInitsInsertLocation::SuperFnOutsideClass(super_binding); // Create scope for `_super` function - let outer_scope_id = self.ctx.current_scope_id(); + let outer_scope_id = self.ctx.current_block_scope_id(); let super_func_scope_id = self.ctx.scopes_mut().add_scope( Some(outer_scope_id), NodeId::DUMMY, @@ -631,7 +580,7 @@ impl<'a, 'c> ConstructorParamsSuperReplacer<'a, 'c> { let super_binding = self.super_binding.get_or_insert_with(|| { self.ctx.generate_uid( "super", - self.ctx.current_scope_id(), + self.ctx.current_block_scope_id(), SymbolFlags::BlockScopedVariable, ) }); diff --git a/crates/oxc_transformer/src/es2022/class_properties/mod.rs b/crates/oxc_transformer/src/es2022/class_properties/mod.rs index d6c396269b6d9..12072f565ba98 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/mod.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/mod.rs @@ -97,8 +97,6 @@ //! //! ## Implementation //! -//! WORK IN PROGRESS. INCOMPLETE. -//! //! ### Reference implementation //! //! Implementation based on [@babel/plugin-transform-class-properties](https://babel.dev/docs/babel-plugin-transform-class-properties). @@ -115,13 +113,65 @@ //! //! Transform happens in 3 phases: //! -//! 1. Check if class contains properties or static blocks, to determine if any transform is necessary -//! (in [`ClassProperties::transform_class`]). -//! 2. Extract class property declarations and static blocks from class and insert in class constructor -//! (instance properties) or before/after the class (static properties + static blocks) -//! (in [`ClassProperties::transform_class`]). -//! 3. Transform private property usages (`this.#prop`) -//! (in [`ClassProperties::transform_private_field_expression`] and other visitors). +//! 1. On entering class body: +//! ([`ClassProperties::transform_class_body_on_entry`]) +//! * Check if class contains properties or static blocks, to determine if any transform is necessary. +//! Exit if nothing to do. +//! * Build a hashmap of private property keys. +//! * Extract instance property initializers (public or private) from class body and insert into +//! class constructor. +//! * Temporarily replace computed keys of instance properties with assignments to temp vars. +//! `class C { [foo()] = 123; }` -> `class C { [_foo = foo()]; }` +//! +//! 2. During traversal of class body: +//! ([`ClassProperties::transform_private_field_expression`] and other visitors) +//! * Transform private fields (`this.#foo`). +//! +//! 3. On exiting class: +//! ([`ClassProperties::transform_class_declaration_on_exit`] and [`ClassProperties::transform_class_expression_on_exit`]) +//! * Transform static properties, and static blocks. +//! * Move assignments to temp vars which were inserted in computed keys for in phase 1 to before class. +//! * Create temp vars for computed method keys if required. +//! * Insert statements before/after class declaration / expressions before/after class expression. +//! +//! The reason for doing transform in 3 phases is that everything needs to stay within the class body +//! while main traverse executes, so that other transforms have a chance to run on that code. +//! +//! Static property initializers, static blocks, and computed keys move to outside the class eventually, +//! but we move them in the final exit phase, so they get transformed first. +//! Additionally, any private fields (`this.#prop`) in these parts are also transformed in the main traverse +//! by this transform. +//! +//! However, we can't leave *everything* until the exit phase because: +//! +//! 1. We need to compile a list of private properties before main traversal. +//! 2. Instance property initializers need to move into the class constructor, and if we don't do that +//! before the main traversal of class body, then other transforms running on instance property +//! initializers will create temp vars outside the class, when they should be in constructor. +//! +//! Note: We execute the entry phase on entering class *body*, not class, because private properties +//! defined in a class only affect the class body, and not the `extends` clause. +//! By only pushing details of the class to the stack when entering class *body*, we avoid any class +//! fields in the `extends` clause being incorrectly resolved to private properties defined in that class, +//! as `extends` clause is visited before class body. +//! +//! ### Structures +//! +//! Transform stores 2 sets of state: +//! +//! 1. Details about classes in a stack of `ClassDetails` - `classes_stack`. +//! This stack is pushed to when entering class body, and popped when exiting class. +//! This contains data which is used in both the enter and exit phases. +//! 2. A set of properties - `insert_before` etc. +//! These properties are only used in *either* enter or exit phase. +//! State cannot be shared between enter and exit phases in these properties, as they'll get clobbered +//! if there's a nested class within this one. +//! +//! We don't store all state in `ClassDetails` as a performance optimization. +//! It reduces the size of `ClassDetails` which has be repeatedly pushed and popped from stack, +//! and allows reusing same `Vec`s and `FxHashMap`s for each class, rather than creating new each time. +//! +//! ### Files //! //! Implementation is split into several files: //! @@ -151,9 +201,7 @@ use indexmap::IndexMap; use rustc_hash::{FxBuildHasher, FxHashMap}; use serde::Deserialize; -use oxc_allocator::{Address, GetAddress}; use oxc_ast::ast::*; -use oxc_data_structures::stack::NonEmptyStack; use oxc_span::Atom; use oxc_syntax::{scope::ScopeId, symbol::SymbolId}; use oxc_traverse::{Traverse, TraverseCtx}; @@ -189,43 +237,49 @@ pub struct ClassPropertiesOptions { /// /// [module docs]: self pub struct ClassProperties<'a, 'ctx> { - // Options + // ----- Options ----- // - /// If `true`, set properties with `=`, instead of `_defineProperty` helper. + /// If `true`, set properties with `=`, instead of `_defineProperty` helper (loose option). set_public_class_fields: bool, - /// If `true`, record private properties as string keys + /// If `true`, store private properties as normal properties as string keys (loose option). private_fields_as_properties: bool, /// If `true`, transform static blocks. transform_static_blocks: bool, ctx: &'ctx TransformCtx<'a>, - // State during whole AST + // ----- State used during all phases of transform ----- // /// Stack of classes. /// Pushed to when entering a class, popped when exiting. - // TODO: The way stack is used is not perfect, because pushing to/popping from it in - // `enter_expression` / `exit_expression`. If another transform replaces the class, - // then stack will get out of sync. - // TODO: Should push to the stack only when entering class body, because `#x` in class `extends` - // clause resolves to `#x` in *outer* class, not the current class. + /// + /// The way stack is used is not perfect, because pushing to/popping from it in + /// `enter_class_body` / `exit_expression`. If another transform replaces/removes the class + /// in an earlier `exit_expression` visitor, then stack will get out of sync. + /// I (@overlookmotel) don't think there's a solution to this, and I don't think any other + /// transforms will remove a class expression in this way, so should be OK. + /// This problem only affects class expressions. Class declarations aren't affected, + /// as their exit-phase transform happens in `exit_class`. classes_stack: ClassesStack<'a>, - /// Addresses of class expressions being processed, to prevent same class being visited twice. - /// Have to use a stack because the revisit doesn't necessarily happen straight after the first visit. - /// e.g. `c = class C { [class D {}] = 1; }` -> `c = (_D = class D {}, class C { ... })` - class_expression_addresses_stack: NonEmptyStack
, - - // State during transform of class + // ----- State used only during enter phase ----- // - /// Scope that instance init initializers will be inserted into + /// Scope that instance property initializers will be inserted into. + /// This is usually class constructor, but can also be a `_super` function which is created. instance_inits_scope_id: ScopeId, - /// `ScopeId` of class constructor, if instance init initializers will be inserted into constructor. + /// Scope of class constructor, if instance property initializers will be inserted into constructor. /// Used for checking for variable name clashes. - /// e.g. `let x; class C { prop = x; constructor(x) {} }` - `x` in constructor needs to be renamed + /// e.g. `class C { prop = x(); constructor(x) {} }` + /// - `x` in constructor needs to be renamed when `x()` is moved into constructor body. + /// `None` if class has no existing constructor, as then there can't be any clashes. instance_inits_constructor_scope_id: Option, - /// `SymbolId`s in constructor which clash with instance prop initializers + /// Symbols in constructor which clash with instance prop initializers. + /// Keys are symbols' IDs. + /// Values are initially the original name of binding, later on the name of new UID name. clashing_constructor_symbols: FxHashMap>, + + // ----- State used only during exit phase ----- + // /// Expressions to insert before class insert_before: Vec>, /// Expressions to insert after class expression @@ -235,6 +289,7 @@ pub struct ClassProperties<'a, 'ctx> { } impl<'a, 'ctx> ClassProperties<'a, 'ctx> { + /// Create `ClassProperties` transformer pub fn new( options: ClassPropertiesOptions, transform_static_blocks: bool, @@ -251,8 +306,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { private_fields_as_properties, transform_static_blocks, ctx, - classes_stack: ClassesStack::default(), - class_expression_addresses_stack: NonEmptyStack::new(Address::DUMMY), + classes_stack: ClassesStack::new(), // Temporary values - overwritten when entering class instance_inits_scope_id: ScopeId::new(0), instance_inits_constructor_scope_id: None, @@ -266,16 +320,26 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> { - // `#[inline]` because this is a hot path + fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_class_body_on_entry(body, ctx); + } + + fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.transform_class_declaration_on_exit(class, ctx); + } + + // `#[inline]` for fast exit for expressions which are not `Class`es + #[inline] + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if matches!(expr, Expression::ClassExpression(_)) { + self.transform_class_expression_on_exit(expr, ctx); + } + } + + // `#[inline]` for fast exit for expressions which are not any of the transformed types #[inline] fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { - // IMPORTANT: If add any other visitors here to handle private fields, - // also need to add them to visitor in `static_prop.rs`. match expr { - // `class {}` - Expression::ClassExpression(_) => { - self.transform_class_expression(expr, ctx); - } // `object.#prop` Expression::PrivateFieldExpression(_) => { self.transform_private_field_expression(expr, ctx); @@ -308,7 +372,7 @@ impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> { } } - // `#[inline]` because this is a hot path + // `#[inline]` for fast exit for assignment targets which are not private fields (rare case) #[inline] fn enter_assignment_target( &mut self, @@ -318,37 +382,31 @@ impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> { self.transform_assignment_target(target, ctx); } - // `#[inline]` because this is a hot path - #[inline] - fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - match stmt { - // `class C {}` - Statement::ClassDeclaration(class) => { - let stmt_address = class.address(); - self.transform_class_declaration(class, stmt_address, ctx); - } - // `export class C {}` - Statement::ExportNamedDeclaration(decl) => { - let stmt_address = decl.address(); - if let Some(Declaration::ClassDeclaration(class)) = &mut decl.declaration { - self.transform_class_declaration(class, stmt_address, ctx); - } - } - // `export default class {}` - Statement::ExportDefaultDeclaration(decl) => { - let stmt_address = decl.address(); - if let ExportDefaultDeclarationKind::ClassDeclaration(class) = &mut decl.declaration - { - self.transform_class_declaration(class, stmt_address, ctx); - } - } - _ => {} + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if prop.r#static { + self.flag_entering_static_property_or_block(); } } - // `#[inline]` because `transform_class_on_exit` is so small - #[inline] - fn exit_class(&mut self, class: &mut Class<'a>, _ctx: &mut TraverseCtx<'a>) { - self.transform_class_on_exit(class); + fn exit_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if prop.r#static { + self.flag_exiting_static_property_or_block(); + } + } + + fn enter_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) { + self.flag_entering_static_property_or_block(); + } + + fn exit_static_block(&mut self, _block: &mut StaticBlock<'a>, _ctx: &mut TraverseCtx<'a>) { + self.flag_exiting_static_property_or_block(); } } diff --git a/crates/oxc_transformer/src/es2022/class_properties/private_field.rs b/crates/oxc_transformer/src/es2022/class_properties/private_field.rs index 0e7c5ae86b8d5..2dd69c564e5c9 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/private_field.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/private_field.rs @@ -1439,8 +1439,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { } } - // Note: This is also called by visitor in `static_prop.rs` - pub(super) fn transform_unary_expression_impl( + fn transform_unary_expression_impl( &mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>, diff --git a/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs b/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs index 6c8049b2213bd..a358fac4f2755 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/prop_decl.rs @@ -218,7 +218,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { self.create_init_assignment_loose(prop, value, assignee, is_static, ctx) } else { // `_defineProperty(assignee, "prop", value)` - self.create_init_assignment_not_loose(prop, value, assignee, ctx) + self.create_init_assignment_not_loose(prop, value, assignee, is_static, ctx) } } @@ -237,12 +237,14 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { 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); + return self + .create_init_assignment_not_loose(prop, value, assignee, is_static, 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); + return self + .create_init_assignment_not_loose(prop, value, assignee, is_static, ctx); } key @ match_expression!(PropertyKey) => { let key = key.to_expression_mut(); @@ -250,7 +252,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // `class C { 'x' = true; 123 = false; }` // No temp var is created for these. // TODO: Any other possible static key types? - let key = self.create_computed_key_temp_var_if_required(key, ctx); + let key = self.create_computed_key_temp_var_if_required(key, is_static, ctx); ctx.ast.member_expression_computed(SPAN, assignee, key, false) } PropertyKey::PrivateIdentifier(_) => { @@ -274,6 +276,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { prop: &mut PropertyDefinition<'a>, value: Expression<'a>, assignee: Expression<'a>, + is_static: bool, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { let key = match &mut prop.key { @@ -286,7 +289,7 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // `class C { 'x' = true; 123 = false; }` // No temp var is created for these. // TODO: Any other possible static key types? - self.create_computed_key_temp_var_if_required(key, ctx) + self.create_computed_key_temp_var_if_required(key, is_static, ctx) } PropertyKey::PrivateIdentifier(_) => { // Handled in `convert_instance_property` and `convert_static_property` diff --git a/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs b/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs index e49c8b1256b2c..6705dfc4b8d6f 100644 --- a/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs +++ b/crates/oxc_transformer/src/es2022/class_properties/static_prop_init.rs @@ -15,7 +15,7 @@ use super::ClassProperties; impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// Transform static property initializer. /// - /// Replace `this`, and references to class name, with temp var for class. Transform private fields. + /// Replace `this`, and references to class name, with temp var for class. /// See below for full details of transforms. pub(super) fn transform_static_initializer( &mut self, @@ -39,22 +39,16 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// -> `var _C; class C {}; _C = C; C.x = _C.y;` /// * Class expression: `x = class C { static x = C.y; }` /// -> `var _C; x = (_C = class C {}, _C.x = _C.y, _C)` -/// 3. Private fields which refer to private props of this class. -/// * Class declaration: `class C { static #x = 123; static y = this.#x; }` -/// -> `var _C; class C {}; _C = C; var _x = { _: 123 }; C.y = _assertClassBrand(_C, _C, _x)._;` -/// * Class expression: `x = class C { static #x = 123; static y = this.#x; }` -/// -> `var _C, _x; x = (_C = class C {}, _x = { _: 123 }, _C.y = _assertClassBrand(_C, _C, _x)._), _C)` /// /// Also: -/// * Updates parent `ScopeId` of first level of scopes in initializer. -/// * Sets `ScopeFlags` of scopes to sloppy mode if code outside the class is sloppy mode. +/// * Update parent `ScopeId` of first level of scopes in initializer. +/// * Set `ScopeFlags` of scopes to sloppy mode if code outside the class is sloppy mode. /// /// Reason we need to transform `this` is because the initializer is being moved from inside the class -/// to outside. `this` outside the class refers to a different `this`, and private fields are only valid -/// within the class body. So we need to transform them. +/// to outside. `this` outside the class refers to a different `this`. So we need to transform it. /// /// Note that for class declarations, assignments are made to properties of original class name `C`, -/// but temp var `_C` is used in replacements for `this` or class name, and private fields. +/// but temp var `_C` is used in replacements for `this` or class name. /// This is because class binding `C` could be mutated, and the initializer may contain functions which /// are not executed immediately, so the mutation occurs before that initializer code runs. /// @@ -69,12 +63,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { /// assert(C2.getSelf2() === C); // Would fail if `C` in `getSelf2` was not replaced with temp var /// ``` /// -/// If this class defines no private properties, class has no name, and no `ScopeFlags` need updating, -/// then we only need to transform `this`, and re-parent first-level scopes. So can skip traversing -/// into functions and other contexts which have their own `this`. -/// -/// Note: Those functions could contain private fields referring to a *parent* class's private props, -/// but we don't need to transform them here as they remain in same class scope. +/// If this class has no name, and no `ScopeFlags` need updating, then we only need to transform `this`, +/// and re-parent first-level scopes. So can skip traversing into functions and other contexts which have +/// their own `this`. // // TODO(improve-on-babel): Unnecessary to create temp var for class declarations if either: // 1. Class name binding is not mutated. @@ -85,9 +76,9 @@ impl<'a, 'ctx> ClassProperties<'a, 'ctx> { // but actually the transform isn't right. Should wrap initializer in a strict mode IIFE so that // initializer code runs in strict mode, as it was before within class body. struct StaticInitializerVisitor<'a, 'ctx, 'v> { - /// `true` if class has name, or class has private properties, or `ScopeFlags` need updating. - /// Any of these neccesitates walking the whole tree. If none of those apply, we only need to - /// walk as far as functions and other constructs which define a `this`. + /// `true` if class has name, or `ScopeFlags` need updating. + /// Either of these neccesitates walking the whole tree. If neither applies, we only need to walk + /// as far as functions and other constructs which define a `this`. walk_deep: bool, /// `true` if should make scopes sloppy mode make_sloppy_mode: bool, @@ -114,42 +105,14 @@ impl<'a, 'ctx, 'v> StaticInitializerVisitor<'a, 'ctx, 'v> { ctx: &'v mut TraverseCtx<'a>, ) -> Self { let make_sloppy_mode = !ctx.current_scope_flags().is_strict_mode(); - let walk_deep = if make_sloppy_mode { - true - } else { - let class_details = class_properties.current_class(); - class_details.bindings.name.is_some() || class_details.private_props.is_some() - }; + let walk_deep = + make_sloppy_mode || class_properties.current_class().bindings.name.is_some(); Self { walk_deep, make_sloppy_mode, this_depth: 0, scope_depth: 0, class_properties, ctx } } } impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { - // TODO: Also need to call class visitors so private props stack is in correct state. - // Otherwise, in this example, `#x` in `getInnerX` is resolved incorrectly - // and `getInnerX()` will return 1 instead of 2. - // We have to visit the inner class now rather than later after exiting outer class so that - // `#y` in `getOuterY` resolves correctly too. - // ```js - // class Outer { - // #x = 1; - // #y = 1; - // static inner = class Inner { - // #x = 2; - // getInnerX() { - // return this.#x; // Should equal 2 - // } - // getOuterY() { - // return this.#y; // Should equal 1 - // } - // }; - // } - // ``` - // - // Need to save all per-class state (`insert_before` etc), and restore it again after. - // Using a stack would be overkill because nested classes in static blocks will be rare. - #[inline] fn visit_expression(&mut self, expr: &mut Expression<'a>) { match expr { @@ -159,23 +122,14 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { self.replace_this_with_temp_var(expr, span); return; } - // `delete this` / `delete object?.#prop.xyz` + // `delete this` Expression::UnaryExpression(unary_expr) => { - if unary_expr.operator == UnaryOperator::Delete { - match &unary_expr.argument { - Expression::ThisExpression(_) => { - let span = unary_expr.span; - self.replace_delete_this_with_true(expr, span); - return; - } - Expression::ChainExpression(_) => { - // Call directly into `transform_unary_expression_impl` rather than - // main entry point `transform_unary_expression`. We already checked that - // `expr` is `delete `, so can avoid checking that again. - self.class_properties.transform_unary_expression_impl(expr, self.ctx); - } - _ => {} - } + if unary_expr.operator == UnaryOperator::Delete + && matches!(&unary_expr.argument, Expression::ThisExpression(_)) + { + let span = unary_expr.span; + self.replace_delete_this_with_true(expr, span); + return; } } // `super.prop` @@ -186,41 +140,17 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { Expression::ComputedMemberExpression(_) => { self.transform_computed_member_expression_if_super(expr); } - // `object.#prop` - Expression::PrivateFieldExpression(_) => { - self.class_properties.transform_private_field_expression(expr, self.ctx); - } - // `super.prop()` or `object.#prop()` + // `super.prop()` Expression::CallExpression(call_expr) => { self.transform_call_expression_if_super_member_expression(call_expr); - self.class_properties.transform_call_expression(expr, self.ctx); } - // `super.prop = value`, `super.prop += value`, `super.prop ??= value` or - // `object.#prop = value`, `object.#prop += value`, `object.#prop ??= value` etc + // `super.prop = value`, `super.prop += value`, `super.prop ??= value` Expression::AssignmentExpression(_) => { self.transform_assignment_expression_if_super_member_assignment_target(expr); - // Check again if it's an assignment expression, because it could have been transformed - // to other expression. - if matches!(expr, Expression::AssignmentExpression(_)) { - self.class_properties.transform_assignment_expression(expr, self.ctx); - } } - // `object.#prop++`, `--object.#prop` + // `super.prop++`, `--super.prop` Expression::UpdateExpression(_) => { self.transform_update_expression_if_super_member_assignment_target(expr); - // Check again if it's an update expression, because it could have been transformed - // to other expression. - if matches!(expr, Expression::UpdateExpression(_)) { - self.class_properties.transform_update_expression(expr, self.ctx); - } - } - // `object?.#prop` - Expression::ChainExpression(_) => { - self.class_properties.transform_chain_expression(expr, self.ctx); - } - // "object.#prop`xyz`" - Expression::TaggedTemplateExpression(_) => { - self.class_properties.transform_tagged_template_expression(expr, self.ctx); } _ => {} } @@ -228,13 +158,6 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { walk_mut::walk_expression(self, expr); } - #[inline] - fn visit_assignment_target(&mut self, target: &mut AssignmentTarget<'a>) { - // `[object.#prop] = []` - self.class_properties.transform_assignment_target(target, self.ctx); - walk_mut::walk_assignment_target(self, target); - } - /// Transform reference to class name to temp var fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) { self.replace_class_name_with_temp_var(ident); @@ -254,9 +177,8 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { // from `this` within this class, and decrement it when exiting. // Therefore `this_depth == 0` when `this` refers to the `this` which needs to be transformed. // - // Or, if class has no name, class has no private properties, and `ScopeFlags` don't need updating, - // stop traversing entirely. No private field accesses need to be transformed, and no scopes need - // flags updating, so no point searching for them. + // Or, if class has no name, and `ScopeFlags` don't need updating, stop traversing entirely. + // No scopes need flags updating, so no point searching for them. // // Also set `make_sloppy_mode = false` while traversing a construct which is strict mode. @@ -357,7 +279,7 @@ impl<'a, 'ctx, 'v> VisitMut<'a> for StaticInitializerVisitor<'a, 'ctx, 'v> { // static prop = class Inner { [this] = 1; }; // } // ``` - // Don't visit `type_annotation` field because can't contain `this` or private props. + // Don't visit `type_annotation` field because can't contain `this`. // Not possible that `self.scope_depth == 0` here, because a `PropertyDefinition` // can only be in a class, and that class would be the first-level scope. diff --git a/crates/oxc_transformer/src/es2022/mod.rs b/crates/oxc_transformer/src/es2022/mod.rs index 43d1ce5061032..662e63d22b5ad 100644 --- a/crates/oxc_transformer/src/es2022/mod.rs +++ b/crates/oxc_transformer/src/es2022/mod.rs @@ -43,14 +43,16 @@ impl<'a, 'ctx> Traverse<'a> for ES2022<'a, 'ctx> { } } - fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { if let Some(class_properties) = &mut self.class_properties { - class_properties.enter_statement(stmt, ctx); + class_properties.exit_expression(expr, ctx); } } fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) { - if let Some(class_static_block) = &mut self.class_static_block { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_class_body(body, ctx); + } else if let Some(class_static_block) = &mut self.class_static_block { class_static_block.enter_class_body(body, ctx); } } @@ -70,4 +72,36 @@ impl<'a, 'ctx> Traverse<'a> for ES2022<'a, 'ctx> { class_properties.enter_assignment_target(target, ctx); } } + + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_property_definition(prop, ctx); + } + } + + fn exit_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_property_definition(prop, ctx); + } + } + + fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.enter_static_block(block, ctx); + } + } + + fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { + if let Some(class_properties) = &mut self.class_properties { + class_properties.exit_static_block(block, ctx); + } + } } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 9c8a75bb6d908..b0a200c167499 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -262,10 +262,12 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { fn enter_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { self.common.enter_static_block(block, ctx); + self.x2_es2022.enter_static_block(block, ctx); } fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { self.common.exit_static_block(block, ctx); + self.x2_es2022.exit_static_block(block, ctx); } fn enter_ts_module_declaration( @@ -294,6 +296,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { self.x1_jsx.exit_expression(expr, ctx); + self.x2_es2022.exit_expression(expr, ctx); self.x2_es2018.exit_expression(expr, ctx); self.x2_es2017.exit_expression(expr, ctx); self.common.exit_expression(expr, ctx); @@ -438,6 +441,15 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_property_definition(def, ctx); } + self.x2_es2022.enter_property_definition(def, ctx); + } + + fn exit_property_definition( + &mut self, + def: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.x2_es2022.exit_property_definition(def, ctx); } fn enter_accessor_property( @@ -518,7 +530,6 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_statement(stmt, ctx); } - self.x2_es2022.enter_statement(stmt, ctx); self.x2_es2018.enter_statement(stmt, ctx); } diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed-redeclared/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed-redeclared/output.js index 75f00d33b8dcd..e47809507f7e4 100644 --- a/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed-redeclared/output.js +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed-redeclared/output.js @@ -7,12 +7,12 @@ class Foo { }); } test() { - var _foo3; + var _foo2; let _this$foo; - var _foo2 = babelHelpers.classPrivateFieldLooseKey("foo"); - class Nested extends (_foo3 = babelHelpers.classPrivateFieldLooseKey("foo"), _this$foo = babelHelpers.classPrivateFieldLooseBase(this, _foo3)[_foo3], class { + var _foo3 = babelHelpers.classPrivateFieldLooseKey("foo"); + class Nested extends (_foo2 = babelHelpers.classPrivateFieldLooseKey("foo"), _this$foo = babelHelpers.classPrivateFieldLooseBase(this, _foo2)[_foo2], class { constructor() { - Object.defineProperty(this, _foo3, { + Object.defineProperty(this, _foo2, { writable: true, value: 2 }); @@ -21,7 +21,7 @@ class Foo { }) { constructor(..._args) { super(..._args); - Object.defineProperty(this, _foo2, { + Object.defineProperty(this, _foo3, { writable: true, value: 3 }); diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed/output.js new file mode 100644 index 0000000000000..2cce838f120e3 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed/output.js @@ -0,0 +1,26 @@ +var _foo = babelHelpers.classPrivateFieldLooseKey("foo"); +class Foo { + constructor() { + Object.defineProperty(this, _foo, { + writable: true, + value: 1 + }); + } + test() { + let _this$foo; + var _foo2 = babelHelpers.classPrivateFieldLooseKey("foo"); + class Nested extends (_this$foo = babelHelpers.classPrivateFieldLooseBase(this, _foo)[_foo], class { + constructor() { + this[_this$foo] = 2; + } + }) { + constructor(..._args) { + super(..._args); + Object.defineProperty(this, _foo2, { + writable: true, + value: 3 + }); + } + } + } +} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed-redeclared/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed-redeclared/output.js new file mode 100644 index 0000000000000..7fd23841708c9 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed-redeclared/output.js @@ -0,0 +1,22 @@ +var _foo = new WeakMap(); +class Foo { + constructor() { + babelHelpers.classPrivateFieldInitSpec(this, _foo, 1); + } + test() { + var _foo2; + let _this$foo; + var _foo3 = new WeakMap(); + class Nested extends (_foo2 = new WeakMap(), _this$foo = babelHelpers.classPrivateFieldGet2(_foo2, this), class { + constructor() { + babelHelpers.classPrivateFieldInitSpec(this, _foo2, 2); + babelHelpers.defineProperty(this, _this$foo, 2); + } + }) { + constructor(..._args) { + super(..._args); + babelHelpers.classPrivateFieldInitSpec(this, _foo3, 3); + } + } + } +} diff --git a/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed/output.js b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed/output.js new file mode 100644 index 0000000000000..2fd5d30327c99 --- /dev/null +++ b/tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed/output.js @@ -0,0 +1,20 @@ +var _foo = new WeakMap(); +class Foo { + constructor() { + babelHelpers.classPrivateFieldInitSpec(this, _foo, 1); + } + test() { + let _this$foo; + var _foo2 = new WeakMap(); + class Nested extends (_this$foo = babelHelpers.classPrivateFieldGet2(_foo, this), class { + constructor() { + babelHelpers.defineProperty(this, _this$foo, 2); + } + }) { + constructor(..._args) { + super(..._args); + babelHelpers.classPrivateFieldInitSpec(this, _foo2, 3); + } + } + } +} diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 3d1033c40d1d5..ad00408a59ded 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 593/927 +Passed: 599/927 # All Passed: * babel-plugin-transform-class-static-block @@ -276,7 +276,7 @@ x Output mismatch x Output mismatch -# babel-plugin-transform-class-properties (205/264) +# babel-plugin-transform-class-properties (211/264) * assumption-constantSuper/complex-super-class/input.js x Output mismatch @@ -335,15 +335,6 @@ x Output mismatch * private/class-shadow-builtins/input.mjs x Output mismatch -* private/nested-class-computed-redeclared/input.js -x Output mismatch - -* private/nested-class-extends-computed/input.js -x Output mismatch - -* private/nested-class-extends-computed-redeclared/input.js -x Output mismatch - * private/optional-chain-cast-to-boolean/input.js x Output mismatch @@ -374,23 +365,6 @@ x Output mismatch * private-loose/class-shadow-builtins/input.mjs x Output mismatch -* private-loose/nested-class-computed-redeclared/input.js -x Output mismatch - -* private-loose/nested-class-extends-computed/input.js -x Output mismatch - -* private-loose/nested-class-extends-computed-redeclared/input.js -Bindings mismatch: -after transform: ScopeId(2): ["Nested", "_foo2", "_foo3"] -rebuilt : ScopeId(3): ["Nested", "_foo2", "_foo3", "_this$foo"] -Bindings mismatch: -after transform: ScopeId(3): ["_this$foo"] -rebuilt : ScopeId(4): [] -Symbol scope ID mismatch for "_this$foo": -after transform: SymbolId(6): ScopeId(3) -rebuilt : SymbolId(3): ScopeId(3) - * private-loose/optional-chain-before-member-call/input.js x Output mismatch diff --git a/tasks/transform_conformance/snapshots/babel_exec.snap.md b/tasks/transform_conformance/snapshots/babel_exec.snap.md index afa017ab6f152..aaceafc4927f3 100644 --- a/tasks/transform_conformance/snapshots/babel_exec.snap.md +++ b/tasks/transform_conformance/snapshots/babel_exec.snap.md @@ -2,7 +2,7 @@ commit: 54a8389f node: v22.12.0 -Passed: 191 of 215 (88.84%) +Passed: 195 of 215 (90.70%) Failures: @@ -24,16 +24,6 @@ Unexpected token `[`. Expected * for generator, private key, identifier or async AssertionError: expected undefined to be 'hello' // Object.is equality at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-nested-class-super-property-in-decorator-exec.test.js:22:28 -./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-nested-class-computed-redeclared-exec.test.js -Private field '#foo' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-nested-class-extends-computed-exec.test.js -AssertionError: expected [Function] to not throw an error but 'TypeError: attempted to use private f…' was thrown - at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:1438:21) - at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:923:17) - at Proxy.methodWrapper (./node_modules/.pnpm/chai@5.1.2/node_modules/chai/chai.js:1610:25) - at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-nested-class-extends-computed-exec.test.js:36:9 - ./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-optional-chain-in-function-param-with-transform-exec.test.js TypeError: Cannot convert undefined or null to object at hasOwnProperty () @@ -62,16 +52,6 @@ TypeError: Cannot read properties of undefined (reading 'bind') at Foo.test (./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-parenthesized-optional-member-call-with-transform-exec.test.js:20:59) at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-loose-parenthesized-optional-member-call-with-transform-exec.test.js:78:12 -./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-nested-class-computed-redeclared-exec.test.js -Private field '#foo' must be declared in an enclosing class - -./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-nested-class-extends-computed-exec.test.js -AssertionError: expected [Function] to not throw an error but 'TypeError: Private element is not pre…' was thrown - at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:1438:21) - at Proxy. (./node_modules/.pnpm/@vitest+expect@2.1.2/node_modules/@vitest/expect/dist/index.js:923:17) - at Proxy.methodWrapper (./node_modules/.pnpm/chai@5.1.2/node_modules/chai/chai.js:1610:25) - at ./tasks/transform_conformance/fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-nested-class-extends-computed-exec.test.js:31:9 - ./fixtures/babel/babel-plugin-transform-class-properties-test-fixtures-private-static-shadow-exec.test.js TypeError: e.has is not a function at _assertClassBrand (./node_modules/.pnpm/@babel+runtime@7.26.0/node_modules/@babel/runtime/helpers/assertClassBrand.js:2:44) diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index c0be81644c67f..f61419d052e26 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 116/132 +Passed: 116/133 # All Passed: * babel-plugin-transform-class-static-block @@ -16,7 +16,36 @@ Passed: 116/132 * regexp -# babel-plugin-transform-class-properties (17/21) +# babel-plugin-transform-class-properties (17/22) +* interaction-with-other-transforms/input.js +Bindings mismatch: +after transform: ScopeId(0): ["C", "C2", "_ref", "_ref2"] +rebuilt : ScopeId(0): ["C", "C2", "_a", "_e", "_g", "_ref", "_ref2"] +Scope children mismatch: +after transform: ScopeId(0): [ScopeId(1), ScopeId(3)] +rebuilt : ScopeId(0): [ScopeId(1), ScopeId(3), ScopeId(4)] +Bindings mismatch: +after transform: ScopeId(1): ["_a", "_e", "_g"] +rebuilt : ScopeId(1): [] +Scope children mismatch: +after transform: ScopeId(1): [ScopeId(2), ScopeId(6)] +rebuilt : ScopeId(1): [ScopeId(2)] +Scope flags mismatch: +after transform: ScopeId(2): ScopeFlags(StrictMode | Function | Arrow) +rebuilt : ScopeId(3): ScopeFlags(Function | Arrow) +Scope parent mismatch: +after transform: ScopeId(2): Some(ScopeId(1)) +rebuilt : ScopeId(3): Some(ScopeId(0)) +Symbol scope ID mismatch for "_a": +after transform: SymbolId(4): ScopeId(1) +rebuilt : SymbolId(0): ScopeId(0) +Symbol scope ID mismatch for "_e": +after transform: SymbolId(5): ScopeId(1) +rebuilt : SymbolId(1): ScopeId(0) +Symbol scope ID mismatch for "_g": +after transform: SymbolId(6): ScopeId(1) +rebuilt : SymbolId(2): ScopeId(0) + * static-super-assignment-target/input.js x Output mismatch diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/input.js new file mode 100644 index 0000000000000..bd37de5083d5e --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/input.js @@ -0,0 +1,16 @@ +class C { + [a ?? b] = c ?? d; + static [e ?? f] = g ?? h; + static { + i ?? j; + } +} + +class C2 extends S { + prop = k ?? l; + constructor() { + if (true) { + super(); + } + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/options.json new file mode 100644 index 0000000000000..2d7df537d10d7 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + "transform-class-properties", + "transform-class-static-block", + "transform-nullish-coalescing-operator" + ] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/output.js new file mode 100644 index 0000000000000..b62c3ccfbb69c --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/output.js @@ -0,0 +1,28 @@ +var _a, _e, _g; +let _ref, _ref2; + +_ref = (_a = a) !== null && _a !== void 0 ? _a : b; +_ref2 = (_e = e) !== null && _e !== void 0 ? _e : f; +class C { + constructor() { + var _c; + babelHelpers.defineProperty(this, _ref, (_c = c) !== null && _c !== void 0 ? _c : d); + } +} +babelHelpers.defineProperty(C, _ref2, (_g = g) !== null && _g !== void 0 ? _g : h); +(() => { + var _i; + (_i = i) !== null && _i !== void 0 ? _i : j; +})(); + +class C2 extends S { + constructor() { + var _super = (..._args) => { + var _k; + return super(..._args), babelHelpers.defineProperty(this, "prop", (_k = k) !== null && _k !== void 0 ? _k : l), this; + }; + if (true) { + _super(); + } + } +}