From 273795d4710d7e0c83d1dcdabc058e35f140eb69 Mon Sep 17 00:00:00 2001 From: overlookmotel <557937+overlookmotel@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:07:23 +0000 Subject: [PATCH] fix(transformer/class-properties): run other transforms on static properties, static blocks, and computed keys (#7982) Large re-architecting of class properties transform. Split transform into 3 phases: 1. Transform instance properties when entering class. 2. Transform private fields during traversal of class body. 3. Transform static properties and static blocks when exiting class. This ensures that code which has to be moved outside of the class (static property initializers, static blocks, computed keys) can get transformed by other transforms before they're moved out. Also fixes a problem where we previously registered private properties too early - on entering the class, rather than the class *body*, so private fields in `extends` clause of a nested class were misinterpretted. --- .../src/es2022/class_properties/class.rs | 821 +++++++++++------- .../es2022/class_properties/class_bindings.rs | 35 +- .../es2022/class_properties/class_details.rs | 25 +- .../es2022/class_properties/computed_key.rs | 83 +- .../es2022/class_properties/constructor.rs | 91 +- .../src/es2022/class_properties/mod.rs | 196 +++-- .../es2022/class_properties/private_field.rs | 3 +- .../src/es2022/class_properties/prop_decl.rs | 13 +- .../class_properties/static_prop_init.rs | 130 +-- crates/oxc_transformer/src/es2022/mod.rs | 40 +- crates/oxc_transformer/src/lib.rs | 13 +- .../output.js | 10 +- .../nested-class-extends-computed/output.js | 26 + .../output.js | 22 + .../nested-class-extends-computed/output.js | 20 + .../snapshots/babel.snap.md | 30 +- .../snapshots/babel_exec.snap.md | 22 +- .../snapshots/oxc.snap.md | 33 +- .../input.js | 16 + .../options.json | 7 + .../output.js | 28 + 21 files changed, 996 insertions(+), 668 deletions(-) create mode 100644 tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private-loose/nested-class-extends-computed/output.js create mode 100644 tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed-redeclared/output.js create mode 100644 tasks/transform_conformance/overrides/babel-plugin-transform-class-properties/test/fixtures/private/nested-class-extends-computed/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/input.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-class-properties/test/fixtures/interaction-with-other-transforms/output.js 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(); + } + } +}