diff --git a/crates/oxc_transformer/src/common/arrow_function_converter.rs b/crates/oxc_transformer/src/common/arrow_function_converter.rs index 2cb342353..bb670756a 100644 --- a/crates/oxc_transformer/src/common/arrow_function_converter.rs +++ b/crates/oxc_transformer/src/common/arrow_function_converter.rs @@ -144,7 +144,8 @@ pub struct ArrowFunctionConverter<'a> { renamed_arguments_symbol_ids: FxHashSet, // TODO(improve-on-babel): `FxHashMap` would suffice here. Iteration order is not important. // Only using `FxIndexMap` for predictable iteration order to match Babel's output. - super_methods_stack: SparseStack, SuperMethodInfo<'a>>>, + super_methods_stack: NonEmptyStack, SuperMethodInfo<'a>>>, + super_needs_transform_stack: NonEmptyStack, } impl ArrowFunctionConverter<'_> { @@ -164,7 +165,8 @@ impl ArrowFunctionConverter<'_> { constructor_super_stack: NonEmptyStack::new(false), arguments_needs_transform_stack: NonEmptyStack::new(false), renamed_arguments_symbol_ids: FxHashSet::default(), - super_methods_stack: SparseStack::new(), + super_methods_stack: NonEmptyStack::new(FxIndexMap::default()), + super_needs_transform_stack: NonEmptyStack::new(false), } } } @@ -198,10 +200,12 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { debug_assert!(self.constructor_super_stack.len() == 1); // TODO: This assertion currently failing because we don't handle `super` in arrow functions // in class static properties correctly. - // e.g. `class C { static f = async () => super.prop; }` + // e.g. `class C { static f = () => super.prop; }` // debug_assert!(self.constructor_super_stack.last() == &false); debug_assert!(self.super_methods_stack.len() == 1); - debug_assert!(self.super_methods_stack.last().is_none()); + debug_assert!(self.super_methods_stack.last().is_empty()); + debug_assert!(self.super_needs_transform_stack.len() == 1); + debug_assert!(self.super_needs_transform_stack.last() == &false); } fn enter_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { @@ -213,32 +217,9 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { self.arguments_var_stack.push(None); self.constructor_super_stack.push(false); - if self.is_async_only() - && (func.r#async || self.super_methods_stack.len() > 1) - && Self::is_class_method_like_ancestor(ctx.parent()) - { - // `self.super_methods_stack.len() > 1` means we are in a nested class method - // - // Only `super` that inside async methods need to be transformed, if it is a - // nested class method and it is not async, we still need to push a `None` to - // `self.super_methods_stack`, because if we don't get a `FxIndexMap` from - // `self.super_methods_stack.last_mut()`, that means we don't need to transform. - // See how to transform `super` in `self.transform_member_expression_for_super` - // - // ```js - // class Outer { - // async method() { - // class Inner extends Outer { - // normal() { - // // `super.value` should not be transformed, because it is not in an async method - // super.value - // } - // } - // } - // } - // ``` - let super_methods = if func.r#async { Some(FxIndexMap::default()) } else { None }; - self.super_methods_stack.push(super_methods); + if Self::is_class_method_like_ancestor(ctx.parent()) { + self.super_methods_stack.push(FxIndexMap::default()); + self.super_needs_transform_stack.push(func.r#async); } } @@ -264,14 +245,11 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { }; let this_var = self.this_var_stack.pop(); let arguments_var = self.arguments_var_stack.pop(); - let super_methods = if self.is_async_only() - && (func.r#async || self.super_methods_stack.len() > 1) - && Self::is_class_method_like_ancestor(ctx.parent()) - { + let super_methods = Self::is_class_method_like_ancestor(ctx.parent()).then(|| { + self.super_needs_transform_stack.pop(); self.super_methods_stack.pop() - } else { - None - }; + }); + self.insert_variable_statement_at_the_top_of_statements( scope_id, &mut body.statements, @@ -286,11 +264,41 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { fn enter_arrow_function_expression( &mut self, arrow: &mut ArrowFunctionExpression<'a>, - _ctx: &mut TraverseCtx<'a>, + ctx: &mut TraverseCtx<'a>, ) { if self.is_async_only() { let previous = *self.arguments_needs_transform_stack.last(); self.arguments_needs_transform_stack.push(previous || arrow.r#async); + + if Self::if_ancestor_of_class_property_definition_value(ctx) { + self.this_var_stack.push(None); + self.super_methods_stack.push(FxIndexMap::default()); + } + self.super_needs_transform_stack + .push(arrow.r#async || *self.super_needs_transform_stack.last()); + } + } + + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.is_async_only() { + if Self::if_ancestor_of_class_property_definition_value(ctx) { + let this_var = self.this_var_stack.pop(); + let super_methods = self.super_methods_stack.pop(); + self.insert_variable_statement_at_the_top_of_statements( + arrow.scope_id(), + &mut arrow.body.statements, + this_var, + None, + Some(super_methods), + ctx, + ); + } + + self.super_needs_transform_stack.pop(); } } @@ -318,6 +326,7 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { } self.this_var_stack.push(None); + self.super_methods_stack.push(FxIndexMap::default()); } fn exit_static_block(&mut self, block: &mut StaticBlock<'a>, ctx: &mut TraverseCtx<'a>) { @@ -326,14 +335,14 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { } let this_var = self.this_var_stack.pop(); + let super_methods = self.super_methods_stack.pop(); self.insert_variable_statement_at_the_top_of_statements( block.scope_id(), &mut block.body, this_var, // `arguments` is not allowed to be used in static blocks None, - // `super()` Only allowed in class constructor - None, + Some(super_methods), ctx, ); } @@ -390,6 +399,36 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { match_member_expression!(Expression) => { self.transform_member_expression_for_super(expr, None, ctx) } + Expression::ArrowFunctionExpression(arrow) => { + // TODO: If the async arrow function without `this` or `super` usage, we can skip this step. + if self.is_async_only() + && arrow.r#async + && Self::if_ancestor_of_class_property_definition_value(ctx) + { + // Inside class property definition value, since async arrow function will be + // converted to a generator function by `AsyncToGenerator` plugin, ensure + // `_this = this` and `super` methods are inserted correctly. We need to + // wrap the async arrow function with an normal arrow function IIFE. + // + // ```js + // class A { + // prop = async () => {} + // } + // // to + // class A { + // prop = (() => { return async () => {} })(); + // } + // ``` + Some(Self::wrap_arrow_function_with_iife( + arrow.span, + arrow.scope_id(), + expr, + ctx, + )) + } else { + return; + } + } _ => return, }; @@ -404,7 +443,9 @@ impl<'a> Traverse<'a> for ArrowFunctionConverter<'a> { } if let Expression::ArrowFunctionExpression(arrow_function_expr) = expr { - if self.is_async_only() && !arrow_function_expr.r#async { + // TODO: Here should return early as long as the async-to-generator plugin is enabled, + // but currently we don't know which plugin is enabled. + if self.is_async_only() || arrow_function_expr.r#async { return; } @@ -640,6 +681,19 @@ impl<'a> ArrowFunctionConverter<'a> { } } + /// Check whether the ancestor is an [`Ancestor::PropertyDefinitionValue`], + /// return false if it's reached the statement. + fn if_ancestor_of_class_property_definition_value(ctx: &mut TraverseCtx<'a>) -> bool { + for ancestor in ctx.ancestors() { + if ancestor.is_parent_of_statement() { + return false; + } else if matches!(ancestor, Ancestor::PropertyDefinitionValue(_)) { + return true; + } + } + unreachable!() + } + /// Transforms a `MemberExpression` whose object is a `super` expression. /// /// In the [`AsyncToGenerator`](crate::es2017::AsyncToGenerator) and @@ -679,7 +733,11 @@ impl<'a> ArrowFunctionConverter<'a> { assign_value: Option<&mut Expression<'a>>, ctx: &mut TraverseCtx<'a>, ) -> Option> { - let super_methods = self.super_methods_stack.last_mut()?; + if !*self.super_needs_transform_stack.last() { + return None; + } + + let super_methods = self.super_methods_stack.last_mut(); let mut argument = None; let mut property = ""; @@ -757,7 +815,7 @@ impl<'a> ArrowFunctionConverter<'a> { call: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Option> { - if self.super_methods_stack.last().is_none() || !call.callee.is_member_expression() { + if !*self.super_needs_transform_stack.last() || !call.callee.is_member_expression() { return None; } @@ -796,7 +854,7 @@ impl<'a> ArrowFunctionConverter<'a> { ctx: &mut TraverseCtx<'a>, ) -> Option> { // Check if the left of the assignment is a `super` member expression. - if self.super_methods_stack.last().is_none() + if !*self.super_needs_transform_stack.last() || !assignment.left.as_member_expression().is_some_and(|m| m.object().is_super()) { return None; @@ -1149,6 +1207,43 @@ impl<'a> ArrowFunctionConverter<'a> { statements.insert(0, stmt); } + + /// Wrap an arrow function with IIFE + /// + /// `() => {}` -> `(() => { return () => {}; })()` + fn wrap_arrow_function_with_iife( + span: Span, + scope_id: ScopeId, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let kind = FormalParameterKind::ArrowFormalParameters; + let params = ctx.ast.formal_parameters(SPAN, kind, ctx.ast.vec(), NONE); + let statement = ctx.ast.statement_return(SPAN, Some(ctx.ast.move_expression(expr))); + let statements = ctx.ast.vec1(statement); + let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), statements); + let parent_scope_id = ctx + .create_child_scope(ctx.current_scope_id(), ScopeFlags::Arrow | ScopeFlags::Function); + ctx.scopes_mut().change_parent_id(scope_id, Some(parent_scope_id)); + let arrow = ctx.ast.alloc_arrow_function_expression_with_scope_id( + SPAN, + false, + false, + NONE, + params, + NONE, + body, + parent_scope_id, + ); + // IIFE + ctx.ast.expression_call( + span, + Expression::ArrowFunctionExpression(arrow), + NONE, + ctx.ast.vec(), + false, + ) + } } /// Visitor for inserting `this` after `super` in constructor body. diff --git a/crates/oxc_transformer/src/common/mod.rs b/crates/oxc_transformer/src/common/mod.rs index 6256264d0..35ff15670 100644 --- a/crates/oxc_transformer/src/common/mod.rs +++ b/crates/oxc_transformer/src/common/mod.rs @@ -81,6 +81,14 @@ impl<'a> Traverse<'a> for Common<'a, '_> { self.arrow_function_converter.enter_arrow_function_expression(arrow, ctx); } + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.arrow_function_converter.exit_arrow_function_expression(arrow, ctx); + } + fn enter_function_body(&mut self, body: &mut FunctionBody<'a>, ctx: &mut TraverseCtx<'a>) { self.arrow_function_converter.enter_function_body(body, ctx); } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 662b16938..27666aaf7 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -281,6 +281,7 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { #[inline] fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.enter_expression(expr, ctx); if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_expression(expr, ctx); } @@ -290,15 +291,14 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { self.x2_es2018.enter_expression(expr, ctx); self.x2_es2016.enter_expression(expr, ctx); self.x4_regexp.enter_expression(expr, ctx); - self.common.enter_expression(expr, ctx); } fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.common.exit_expression(expr, ctx); 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); } fn enter_simple_assignment_target( @@ -477,6 +477,8 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { arrow: &mut ArrowFunctionExpression<'a>, ctx: &mut TraverseCtx<'a>, ) { + self.common.exit_arrow_function_expression(arrow, ctx); + // Some plugins may add new statements to the ArrowFunctionExpression's body, // which can cause issues with the `() => x;` case, as it only allows a single statement. // To address this, we wrap the last statement in a return statement and set the expression to false. diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index ff954e232..291b9b929 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: 54a8389f -Passed: 126/148 +Passed: 129/150 # All Passed: * babel-plugin-transform-class-static-block @@ -10,6 +10,7 @@ Passed: 126/148 * babel-plugin-transform-optional-catch-binding * babel-plugin-transform-async-generator-functions * babel-plugin-transform-object-rest-spread +* babel-plugin-transform-async-to-generator * babel-plugin-transform-exponentiation-operator * babel-plugin-transform-arrow-functions * babel-preset-typescript @@ -46,11 +47,6 @@ after transform: SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), R rebuilt : SymbolId(0): [ReferenceId(0), ReferenceId(2), ReferenceId(6), ReferenceId(10)] -# babel-plugin-transform-async-to-generator (20/21) -* class/static-block/input.js -x Output mismatch - - # babel-plugin-transform-typescript (2/13) * class-property-definition/input.ts Unresolved references mismatch: diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/input.js new file mode 100644 index 000000000..9c5ac07f4 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/input.js @@ -0,0 +1,11 @@ +class Cls { + prop = async () => (this, super.prop) + static prop = async () => (this, super.prop) + + nested = () => { + async () => (this, super.prop); + } + static nested = () => { + async () => (this, super.prop); + } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/output.js new file mode 100644 index 000000000..84dd4d2c8 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/property-definition/output.js @@ -0,0 +1,27 @@ +class Cls { + prop = (() => { + var _superprop_getProp = () => super.prop, _this = this; + return babelHelpers.asyncToGenerator(function* () { + return _this, _superprop_getProp(); + }); + })(); + static prop = (() => { + var _superprop_getProp2 = () => super.prop, _this2 = this; + return babelHelpers.asyncToGenerator(function* () { + return _this2, _superprop_getProp2(); + }); + })(); + + nested = () => { + var _superprop_getProp3 = () => super.prop, _this3 = this; + /*#__PURE__*/babelHelpers.asyncToGenerator(function* () { + return _this3, _superprop_getProp3(); + }); + }; + static nested = () => { + var _superprop_getProp4 = () => super.prop, _this4 = this; + /*#__PURE__*/babelHelpers.asyncToGenerator(function* () { + return _this4, _superprop_getProp4(); + }); + }; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/input.js new file mode 100644 index 000000000..c1da584ee --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/input.js @@ -0,0 +1,4 @@ +class Cls { + prop = async () => { super.prop } + static prop = async () => { super.prop; } +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/output.js new file mode 100644 index 000000000..0ddee93b1 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/super/property/output.js @@ -0,0 +1,14 @@ +class Cls { + prop = (() => { + var _superprop_getProp = () => super.prop; + return babelHelpers.asyncToGenerator(function* () { + _superprop_getProp(); + }); + })(); + static prop = (() => { + var _superprop_getProp2 = () => super.prop; + return babelHelpers.asyncToGenerator(function* () { + _superprop_getProp2(); + }); + })(); +}