diff --git a/crates/oxc_ast/src/ast_builder_impl.rs b/crates/oxc_ast/src/ast_builder_impl.rs index 84c7c06fce74a..10aafab345c5e 100644 --- a/crates/oxc_ast/src/ast_builder_impl.rs +++ b/crates/oxc_ast/src/ast_builder_impl.rs @@ -146,6 +146,45 @@ impl<'a> AstBuilder<'a> { mem::replace(decl, empty_decl) } + /// Move a formal parameters out by replacing it with an empty [`FormalParameters`]. + #[inline] + pub fn move_formal_parameters(self, params: &mut FormalParameters<'a>) -> FormalParameters<'a> { + let empty_params = self.formal_parameters(Span::default(), params.kind, self.vec(), NONE); + mem::replace(params, empty_params) + } + + /// Move a function body out by replacing it with an empty [`FunctionBody`]. + #[inline] + pub fn move_function_body(self, body: &mut FunctionBody<'a>) -> FunctionBody<'a> { + let empty_body = self.function_body(Span::default(), self.vec(), self.vec()); + mem::replace(body, empty_body) + } + + /// Move a function out by replacing it with an empty [`Function`] + #[inline] + pub fn move_function(self, function: &mut Function<'a>) -> Function<'a> { + let params = self.formal_parameters( + Span::default(), + FormalParameterKind::FormalParameter, + self.vec(), + NONE, + ); + let empty_function = self.function( + FunctionType::FunctionDeclaration, + Span::default(), + None, + false, + false, + false, + NONE, + NONE, + params, + NONE, + NONE, + ); + mem::replace(function, empty_function) + } + /// Move an array element out by replacing it with an /// [elision](ArrayExpressionElement::Elision). pub fn move_array_expression_element( diff --git a/crates/oxc_transformer/src/common/statement_injector.rs b/crates/oxc_transformer/src/common/statement_injector.rs index d01c9f891f86e..4453d6cdc239c 100644 --- a/crates/oxc_transformer/src/common/statement_injector.rs +++ b/crates/oxc_transformer/src/common/statement_injector.rs @@ -78,7 +78,6 @@ impl<'a> StatementInjectorStore<'a> { } /// Add a statement to be inserted immediately after the target statement. - #[expect(dead_code)] pub fn insert_after(&self, target: &Statement<'a>, stmt: Statement<'a>) { let mut insertions = self.insertions.borrow_mut(); let adjacent_stmts = insertions.entry(target.address()).or_default(); diff --git a/crates/oxc_transformer/src/es2017/async_to_generator.rs b/crates/oxc_transformer/src/es2017/async_to_generator.rs index 0e60db75e356e..12ca8f21232e4 100644 --- a/crates/oxc_transformer/src/es2017/async_to_generator.rs +++ b/crates/oxc_transformer/src/es2017/async_to_generator.rs @@ -1,6 +1,7 @@ -//! ES2017: Async / Await \[WIP\] +//! ES2017: Async / Await //! -//! This plugin transforms async functions to generator functions. +//! This plugin transforms async functions to generator functions +//! and wraps them with `asyncToGenerator` helper function. //! //! ## Example //! @@ -17,16 +18,28 @@ //! } //! ``` //! -//! Output (Currently): +//! Output: //! ```js //! function foo() { -//! return _asyncToGenerator(function* () { -//! yield bar(); -//! }) +//! return _foo.apply(this, arguments); //! } -//! const foo2 = () => _asyncToGenerator(function* () { -//! yield bar(); +//! function _foo() { +//! _foo = babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); +//! return _foo.apply(this, arguments); //! } +//! const foo2 = function() { +//! var _ref = babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); +//! return function foo2() { +//! return _ref.apply(this, arguments); +//! }; +//! }(); +//! babelHelpers.asyncToGenerator(function* () { +//! yield bar(); +//! }); //! ``` //! //! ## Implementation @@ -35,21 +48,16 @@ //! //! Reference: //! * Babel docs: -//! * Esbuild implementation: //! * Babel implementation: -//! * Babel helper implementation: //! * Async / Await TC39 proposal: -use oxc_ast::{ - ast::{ - ArrowFunctionExpression, Expression, Function, FunctionType, Statement, - VariableDeclarationKind, - }, - NONE, -}; -use oxc_semantic::ScopeFlags; -use oxc_span::{GetSpan, SPAN}; -use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; +use std::mem; + +use oxc_allocator::Box; +use oxc_ast::{ast::*, Visit, NONE}; +use oxc_semantic::{ReferenceFlags, ScopeFlags, ScopeId, SymbolFlags}; +use oxc_span::{Atom, GetSpan, SPAN}; +use oxc_traverse::{Ancestor, BoundIdentifier, Traverse, TraverseCtx}; use crate::{common::helper_loader::Helper, TransformCtx}; @@ -65,138 +73,318 @@ impl<'a, 'ctx> AsyncToGenerator<'a, 'ctx> { impl<'a, 'ctx> Traverse<'a> for AsyncToGenerator<'a, 'ctx> { fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { - if let Expression::AwaitExpression(await_expr) = expr { - // Do not transform top-level await, or in async generator functions. - let in_async_function = ctx - .ancestry - .ancestors() - .find_map(|ance| { - // We need to check if there's async generator or async function. - // If it is async generator, we should not transform the await expression here. - if let Ancestor::FunctionBody(body) = ance { - if *body.r#async() { - Some(!body.generator()) - } else { - None - } - } else if let Ancestor::ArrowFunctionExpressionBody(_) = ance { - // Arrow function is never generator. - Some(true) - } else { - None - } - }) - .unwrap_or(false); - if in_async_function { - // Move the expression to yield. - *expr = ctx.ast.expression_yield( - SPAN, - false, - Some(ctx.ast.move_expression(&mut await_expr.argument)), - ); + let new_expr = match expr { + Expression::AwaitExpression(await_expr) => { + self.transform_await_expression(await_expr, ctx) } - } else if let Expression::FunctionExpression(func) = expr { - if !func.r#async || func.generator { - return; - } - let new_function = self.transform_function(func, ctx); - *expr = ctx.ast.expression_from_function(new_function); - } else if let Expression::ArrowFunctionExpression(arrow) = expr { - if !arrow.r#async { - return; - } - *expr = self.transform_arrow_function(arrow, ctx); + Expression::FunctionExpression(func) => self.transform_function_expression(func, ctx), + Expression::ArrowFunctionExpression(arrow) => self.transform_arrow_function(arrow, ctx), + _ => None, + }; + + if let Some(new_expr) = new_expr { + *expr = new_expr; } } fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - if let Statement::FunctionDeclaration(func) = stmt { - if !func.r#async || func.generator { - return; + let new_statement = match stmt { + Statement::FunctionDeclaration(func) => self.transform_function_declaration(func, ctx), + Statement::ExportDefaultDeclaration(decl) => { + if let ExportDefaultDeclarationKind::FunctionDeclaration(func) = + &mut decl.declaration + { + self.transform_function_declaration(func, ctx) + } else { + None + } } - let new_function = self.transform_function(func, ctx); - if let Some(id) = func.id.take() { - *stmt = ctx.ast.statement_declaration(ctx.ast.declaration_variable( - SPAN, - VariableDeclarationKind::Const, - ctx.ast.vec1(ctx.ast.variable_declarator( - SPAN, - VariableDeclarationKind::Const, - ctx.ast.binding_pattern( - ctx.ast.binding_pattern_kind_from_binding_identifier(id), - NONE, - false, - ), - Some(ctx.ast.expression_from_function(new_function)), - false, - )), - false, - )); - } else { - *stmt = - ctx.ast.statement_declaration(ctx.ast.declaration_from_function(new_function)); + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::FunctionDeclaration(func)) = &mut decl.declaration { + self.transform_function_declaration(func, ctx) + } else { + None + } } + _ => None, + }; + + if let Some(new_statement) = new_statement { + self.ctx.statement_injector.insert_after(stmt, new_statement); } } + + fn exit_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + self.transform_function_for_method_definition(&mut node.value, ctx); + } } impl<'a, 'ctx> AsyncToGenerator<'a, 'ctx> { - fn transform_function( + /// Transforms `await` expressions to `yield` expressions. + /// Ignores top-level await expressions. + #[allow(clippy::unused_self)] + fn transform_await_expression( + &self, + expr: &mut AwaitExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // We don't need to handle top-level await. + if ctx.parent().is_program() { + None + } else { + Some(ctx.ast.expression_yield( + SPAN, + false, + Some(ctx.ast.move_expression(&mut expr.argument)), + )) + } + } + + /// Transforms async method definitions to generator functions wrapped in asyncToGenerator. + /// + /// ## Example + /// + /// Input: + /// ```js + /// class A { async foo() { await bar(); } } + /// ``` + /// + /// Output: + /// ```js + /// class A { + /// foo() { + /// return babelHelpers.asyncToGenerator(function* () { + /// yield bar(); + /// })(); + /// } + /// ``` + fn transform_function_for_method_definition( &self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>, - ) -> Function<'a> { - let target = ctx.ast.function( - func.r#type, - SPAN, - None, - true, - false, - false, - func.type_parameters.take(), - func.this_param.take(), - ctx.ast.alloc(ctx.ast.formal_parameters( + ) { + if !func.r#async { + return; + } + + let Some(body) = func.body.take() else { + return; + }; + + let (generator_scope_id, wrapper_scope_id) = { + let new_scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = func.scope_id.replace(Some(new_scope_id)).unwrap(); + // We need to change the parent id to new scope id because we need to this function's body inside the wrapper function, + // and then the new scope id will be wrapper function's scope id. + ctx.scopes_mut().change_parent_id(scope_id, Some(new_scope_id)); + // We need to transform formal parameters change back to the original scope, + // because we only move out the function body. + BindingMover::new(new_scope_id, ctx).visit_formal_parameters(&func.params); + + (scope_id, new_scope_id) + }; + + let params = Self::create_empty_params(ctx); + let expression = self.create_async_to_generator_call(params, body, generator_scope_id, ctx); + // Construct the IIFE + let expression = ctx.ast.expression_call(SPAN, expression, NONE, ctx.ast.vec(), false); + let statement = ctx.ast.statement_return(SPAN, Some(expression)); + + // Modify the wrapper function + func.r#async = false; + func.body = Some(ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), ctx.ast.vec1(statement))); + func.scope_id.set(Some(wrapper_scope_id)); + } + + /// Transforms [`Function`] whose type is [`FunctionType::FunctionExpression`] to a generator function + /// and wraps it in asyncToGenerator helper function. + fn transform_function_expression( + &self, + wrapper_function: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !wrapper_function.r#async + || wrapper_function.generator + || wrapper_function.is_typescript_syntax() + { + return None; + } + + let body = wrapper_function.body.take().unwrap(); + let params = ctx.alloc(ctx.ast.move_formal_parameters(&mut wrapper_function.params)); + let id = wrapper_function.id.take(); + let has_function_id = id.is_some(); + + if !has_function_id && !Self::is_function_length_affected(¶ms) { + return Some(self.create_async_to_generator_call( + params, + body, + wrapper_function.scope_id.take().unwrap(), + ctx, + )); + } + + let (generator_scope_id, wrapper_scope_id) = { + let wrapper_scope_id = + ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = wrapper_function.scope_id.replace(Some(wrapper_scope_id)).unwrap(); + // Change the parent scope of the function scope with the current scope. + ctx.scopes_mut().change_parent_id(scope_id, Some(wrapper_scope_id)); + // If there is an id, then we will use it as the name of caller_function, + // and the caller_function is inside the wrapper function. + // so we need to move the id to the new scope. + if let Some(id) = id.as_ref() { + BindingMover::new(wrapper_scope_id, ctx).visit_binding_identifier(id); + let symbol_id = id.symbol_id.get().unwrap(); + *ctx.symbols_mut().get_flags_mut(symbol_id) = SymbolFlags::FunctionScopedVariable; + } + (scope_id, wrapper_scope_id) + }; + + let bound_ident = Self::create_bound_identifier(id.as_ref(), wrapper_scope_id, ctx); + + let caller_function = { + let scope_id = ctx.create_child_scope(wrapper_scope_id, ScopeFlags::Function); + let params = Self::create_placeholder_params(¶ms, scope_id, ctx); + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let id = id.or_else(|| { + Self::infer_function_id_from_variable_declarator(wrapper_scope_id, ctx) + }); + Self::create_function(id, params, body, scope_id, ctx) + }; + + { + // Modify the wrapper function to add new body, params, and scope_id. + let mut statements = ctx.ast.vec_with_capacity(3); + let statement = self.create_async_to_generator_declaration( + &bound_ident, + params, + body, + generator_scope_id, + ctx, + ); + statements.push(statement); + if has_function_id { + let id = caller_function.id.as_ref().unwrap(); + // If the function has an id, then we need to return the id. + // `function foo() { ... }` -> `function foo() {} return foo;` + let reference = ctx.create_bound_reference_id( + SPAN, + id.name.clone(), + id.symbol_id.get().unwrap(), + ReferenceFlags::Read, + ); + let statement = Statement::from(ctx.ast.declaration_from_function(caller_function)); + statements.push(statement); + let argument = Some(ctx.ast.expression_from_identifier_reference(reference)); + statements.push(ctx.ast.statement_return(SPAN, argument)); + } else { + // If the function doesn't have an id, then we need to return the function itself. + // `function() { ... }` -> `return function() { ... };` + let statement_return = ctx.ast.statement_return( + SPAN, + Some(ctx.ast.expression_from_function(caller_function)), + ); + statements.push(statement_return); + } + debug_assert!(wrapper_function.body.is_none()); + wrapper_function.r#async = false; + wrapper_function.body.replace(ctx.ast.alloc_function_body( SPAN, - func.params.kind, - ctx.ast.move_vec(&mut func.params.items), - func.params.rest.take(), - )), - func.return_type.take(), - func.body.take(), + ctx.ast.vec(), + statements, + )); + } + + // Construct the IIFE + let callee = ctx.ast.expression_from_function(ctx.ast.move_function(wrapper_function)); + Some(ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec(), false)) + } + + /// Transforms async function declarations into generator functions wrapped in the asyncToGenerator helper. + fn transform_function_declaration( + &self, + wrapper_function: &mut Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if !wrapper_function.r#async + || wrapper_function.generator + || wrapper_function.is_typescript_syntax() + { + return None; + } + + let (generator_scope_id, wrapper_scope_id) = { + let wrapper_scope_id = + ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + let scope_id = wrapper_function.scope_id.replace(Some(wrapper_scope_id)).unwrap(); + // Change the parent scope of the function scope with the current scope. + ctx.scopes_mut().change_parent_id(scope_id, Some(wrapper_scope_id)); + (scope_id, wrapper_scope_id) + }; + let body = wrapper_function.body.take().unwrap(); + let params = + Self::create_placeholder_params(&wrapper_function.params, wrapper_scope_id, ctx); + let params = mem::replace(&mut wrapper_function.params, params); + let bound_ident = Self::create_bound_identifier( + wrapper_function.id.as_ref(), + ctx.current_scope_id(), + ctx, ); - let parameters = - ctx.ast.vec1(ctx.ast.argument_expression(ctx.ast.expression_from_function(target))); - let call = self.ctx.helper_call_expr(Helper::AsyncToGenerator, parameters, ctx); - let returns = ctx.ast.return_statement(SPAN, Some(call)); - let body = Statement::ReturnStatement(ctx.ast.alloc(returns)); - let body = ctx.ast.function_body(SPAN, ctx.ast.vec(), ctx.ast.vec1(body)); - let body = ctx.ast.alloc(body); - let params = ctx.ast.formal_parameters(SPAN, func.params.kind, ctx.ast.vec(), NONE); - ctx.ast.function( - FunctionType::FunctionExpression, - SPAN, - None, - false, - false, - false, - func.type_parameters.take(), - func.this_param.take(), - params, - func.return_type.take(), - Some(body), - ) + + // Modify the wrapper function + { + wrapper_function.r#async = false; + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + debug_assert!(wrapper_function.body.is_none()); + wrapper_function.body.replace(ctx.ast.alloc_function_body( + SPAN, + ctx.ast.vec(), + statements, + )); + } + + // function _name() { _ref.apply(this, arguments); } + { + let mut statements = ctx.ast.vec_with_capacity(2); + statements.push(self.create_async_to_generator_assignment( + &bound_ident, + params, + body, + generator_scope_id, + ctx, + )); + statements.push(Self::create_apply_call_statement(&bound_ident, ctx)); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + + let scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + // The generator function will move to this function, so we need + // to change the parent scope of the generator function to the scope of this function. + ctx.scopes_mut().change_parent_id(generator_scope_id, Some(scope_id)); + + let params = Self::create_empty_params(ctx); + let id = Some(bound_ident.create_binding_identifier(ctx)); + let caller_function = Self::create_function(id, params, body, scope_id, ctx); + Some(Statement::from(ctx.ast.declaration_from_function(caller_function))) + } } + /// Transforms async arrow functions into generator functions wrapped in the asyncToGenerator helper. fn transform_arrow_function( &self, arrow: &mut ArrowFunctionExpression<'a>, ctx: &mut TraverseCtx<'a>, - ) -> Expression<'a> { - let mut body = ctx.ast.function_body( - SPAN, - ctx.ast.move_vec(&mut arrow.body.directives), - ctx.ast.move_vec(&mut arrow.body.statements), - ); + ) -> Option> { + if !arrow.r#async { + return None; + } + + let mut body = ctx.ast.move_function_body(&mut arrow.body); // If the arrow's expression is true, we need to wrap the only one expression with return statement. if arrow.expression { @@ -208,24 +396,300 @@ impl<'a, 'ctx> AsyncToGenerator<'a, 'ctx> { *statement = ctx.ast.statement_return(expression.span(), Some(expression)); } - let r#type = FunctionType::FunctionExpression; - let parameters = ctx.ast.alloc(ctx.ast.formal_parameters( + let params = ctx.alloc(ctx.ast.move_formal_parameters(&mut arrow.params)); + let generator_function_id = arrow.scope_id.get().unwrap(); + ctx.scopes_mut().get_flags_mut(generator_function_id).remove(ScopeFlags::Arrow); + + if !Self::is_function_length_affected(¶ms) { + return Some(self.create_async_to_generator_call( + params, + ctx.ast.alloc(body), + generator_function_id, + ctx, + )); + } + + let wrapper_scope_id = ctx.create_child_scope(ctx.current_scope_id(), ScopeFlags::Function); + // The generator function will move to inside wrapper, so we need + // to change the parent scope of the generator function to the wrapper function. + ctx.scopes_mut().change_parent_id(generator_function_id, Some(wrapper_scope_id)); + + let bound_ident = Self::create_bound_identifier(None, wrapper_scope_id, ctx); + + let caller_function = { + let scope_id = ctx.create_child_scope(wrapper_scope_id, ScopeFlags::Function); + let params = Self::create_placeholder_params(¶ms, scope_id, ctx); + let statements = ctx.ast.vec1(Self::create_apply_call_statement(&bound_ident, ctx)); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let id = Self::infer_function_id_from_variable_declarator(wrapper_scope_id, ctx); + let function = Self::create_function(id, params, body, scope_id, ctx); + let argument = Some(ctx.ast.expression_from_function(function)); + ctx.ast.statement_return(SPAN, argument) + }; + + // Wrapper function + { + let statement = self.create_async_to_generator_declaration( + &bound_ident, + params, + ctx.ast.alloc(body), + generator_function_id, + ctx, + ); + let mut statements = ctx.ast.vec_with_capacity(2); + statements.push(statement); + statements.push(caller_function); + let body = ctx.ast.alloc_function_body(SPAN, ctx.ast.vec(), statements); + let params = Self::create_empty_params(ctx); + let wrapper_function = Self::create_function(None, params, body, wrapper_scope_id, ctx); + // Construct the IIFE + let callee = ctx.ast.expression_from_function(wrapper_function); + Some(ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec(), false)) + } + } + + /// Infers the function id from [`Ancestor::VariableDeclaratorInit`]. + fn infer_function_id_from_variable_declarator( + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let Ancestor::VariableDeclaratorInit(declarator) = ctx.parent() else { + return None; + }; + let Some(id) = declarator.id().get_binding_identifier() else { unreachable!() }; + Some( + ctx.generate_binding(id.name.clone(), scope_id, SymbolFlags::FunctionScopedVariable) + .create_binding_identifier(ctx), + ) + } + + /// Creates a [`Function`] with the specified params, body and scope_id. + #[inline] + fn create_function( + id: Option>, + params: Box<'a, FormalParameters<'a>>, + body: Box<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Function<'a> { + let r#type = if id.is_some() { + FunctionType::FunctionDeclaration + } else { + FunctionType::FunctionExpression + }; + ctx.ast.function_with_scope_id( + r#type, SPAN, - arrow.params.kind, - ctx.ast.move_vec(&mut arrow.params.items), - arrow.params.rest.take(), + id, + false, + false, + false, + NONE, + NONE, + params, + NONE, + Some(body), + scope_id, + ) + } + + /// Creates a [`Statement`] that calls the `apply` method on the bound identifier. + /// + /// The generated code structure is: + /// ```js + /// bound_ident.apply(this, arguments); + /// ``` + fn create_apply_call_statement( + bound_ident: &BoundIdentifier<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let symbol_id = ctx.scopes().find_binding(ctx.current_scope_id(), "arguments"); + let arguments_ident = + ctx.create_reference_id(SPAN, Atom::from("arguments"), symbol_id, ReferenceFlags::Read); + let arguments_ident = ctx.ast.expression_from_identifier_reference(arguments_ident); + + // (this, arguments) + let mut arguments = ctx.ast.vec_with_capacity(2); + arguments.push(ctx.ast.argument_expression(ctx.ast.expression_this(SPAN))); + arguments.push(ctx.ast.argument_expression(arguments_ident)); + // _ref.apply + let callee = ctx.ast.expression_member(ctx.ast.member_expression_static( + SPAN, + bound_ident.create_read_expression(ctx), + ctx.ast.identifier_name(SPAN, "apply"), + false, )); - let body = Some(body); - let mut function = ctx - .ast - .function(r#type, SPAN, None, true, false, false, NONE, NONE, parameters, NONE, body); - function.scope_id = arrow.scope_id.clone(); - if let Some(scope_id) = function.scope_id.get() { - ctx.scopes_mut().get_flags_mut(scope_id).remove(ScopeFlags::Arrow); - } + let argument = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false); + ctx.ast.statement_return(SPAN, Some(argument)) + } - let arguments = - ctx.ast.vec1(ctx.ast.argument_expression(ctx.ast.expression_from_function(function))); + /// Creates an [`Expression`] that calls the [`Helper::AsyncToGenerator`] helper function. + /// + /// This function constructs the helper call with arguments derived from the provided + /// parameters, body, and scope_id. + /// + /// The generated code structure is: + /// ```js + /// asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_call( + &self, + params: Box<'a, FormalParameters<'a>>, + body: Box<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut function = Self::create_function(None, params, body, scope_id, ctx); + function.generator = true; + let function_expression = ctx.ast.expression_from_function(function); + let argument = ctx.ast.argument_expression(function_expression); + let arguments = ctx.ast.vec1(argument); self.ctx.helper_call_expr(Helper::AsyncToGenerator, arguments, ctx) } + + /// Creates a helper declaration statement for async-to-generator transformation. + /// + /// This function generates code that looks like: + /// ```js + /// var _ref = asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_declaration( + &self, + bound_ident: &BoundIdentifier<'a>, + params: Box<'a, FormalParameters<'a>>, + body: Box<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let init = self.create_async_to_generator_call(params, body, scope_id, ctx); + let declarations = ctx.ast.vec1(ctx.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + bound_ident.create_binding_pattern(ctx), + Some(init), + false, + )); + ctx.ast.statement_declaration(ctx.ast.declaration_variable( + SPAN, + VariableDeclarationKind::Var, + declarations, + false, + )) + } + + /// Creates a helper assignment statement for async-to-generator transformation. + /// + /// This function generates code that looks like: + /// ```js + /// _ref = asyncToGenerator(function* (PARAMS) { + /// BODY + /// }); + /// ``` + fn create_async_to_generator_assignment( + &self, + bound: &BoundIdentifier<'a>, + params: Box<'a, FormalParameters<'a>>, + body: Box<'a, FunctionBody<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Statement<'a> { + let right = self.create_async_to_generator_call(params, body, scope_id, ctx); + let expression = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + bound.create_write_target(ctx), + right, + ); + ctx.ast.statement_expression(SPAN, expression) + } + + /// Creates placeholder [`FormalParameters`] which named `_x` based on the passed-in parameters. + /// `function p(x, y, z, d = 0, ...rest) {}` -> `function* (_x, _x1, _x2) {}` + fn create_placeholder_params( + params: &FormalParameters<'a>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> Box<'a, FormalParameters<'a>> { + let mut parameters = ctx.ast.vec_with_capacity(params.items.len()); + for param in ¶ms.items { + if param.pattern.kind.is_assignment_pattern() { + break; + } + let binding = ctx.generate_uid("x", scope_id, SymbolFlags::FunctionScopedVariable); + parameters.push( + ctx.ast.plain_formal_parameter(param.span(), binding.create_binding_pattern(ctx)), + ); + } + let parameters = ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + parameters, + NONE, + ); + + parameters + } + + /// Creates an empty [FormalParameters] with [FormalParameterKind::FormalParameter]. + #[inline] + fn create_empty_params(ctx: &mut TraverseCtx<'a>) -> Box<'a, FormalParameters<'a>> { + ctx.ast.alloc_formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + ctx.ast.vec(), + NONE, + ) + } + + /// Creates a [`BoundIdentifier`] for the id of the function. + #[inline] + fn create_bound_identifier( + id: Option<&BindingIdentifier<'a>>, + scope_id: ScopeId, + ctx: &mut TraverseCtx<'a>, + ) -> BoundIdentifier<'a> { + ctx.generate_uid( + id.as_ref().map_or_else(|| "ref", |id| id.name.as_str()), + scope_id, + SymbolFlags::FunctionScopedVariable, + ) + } + + /// Checks if the function length is affected by the parameters. + /// + /// TODO: Needs to handle `ignoreFunctionLength` assumption. + // + #[inline] + fn is_function_length_affected(params: &FormalParameters<'_>) -> bool { + params.items.first().is_some_and(|param| !param.pattern.kind.is_assignment_pattern()) + } +} + +/// Moves the bindings from original scope to target scope. +struct BindingMover<'a, 'ctx> { + ctx: &'ctx mut TraverseCtx<'a>, + target_scope_id: ScopeId, +} + +impl<'a, 'ctx> BindingMover<'a, 'ctx> { + fn new(target_scope_id: ScopeId, ctx: &'ctx mut TraverseCtx<'a>) -> Self { + Self { ctx, target_scope_id } + } +} + +impl<'a, 'ctx> Visit<'a> for BindingMover<'a, 'ctx> { + /// Visits a binding identifier and moves it to the target scope. + fn visit_binding_identifier(&mut self, ident: &BindingIdentifier<'a>) { + let symbols = self.ctx.symbols(); + let symbol_id = ident.symbol_id.get().unwrap(); + let current_scope_id = symbols.get_scope_id(symbol_id); + let scopes = self.ctx.scopes_mut(); + scopes.move_binding(current_scope_id, self.target_scope_id, ident.name.as_str()); + let symbols = self.ctx.symbols_mut(); + symbols.set_scope_id(symbol_id, self.target_scope_id); + } } diff --git a/crates/oxc_transformer/src/es2017/mod.rs b/crates/oxc_transformer/src/es2017/mod.rs index 09820452930f8..b2525e161c0f7 100644 --- a/crates/oxc_transformer/src/es2017/mod.rs +++ b/crates/oxc_transformer/src/es2017/mod.rs @@ -30,6 +30,16 @@ impl<'a, 'ctx> Traverse<'a> for ES2017<'a, 'ctx> { } } + fn exit_method_definition( + &mut self, + node: &mut oxc_ast::ast::MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.async_to_generator { + self.async_to_generator.exit_method_definition(node, ctx); + } + } + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { if self.options.async_to_generator { self.async_to_generator.exit_statement(stmt, ctx); diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 2f5399b0bee2a..77388c4fc323f 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -240,7 +240,6 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { fn exit_function(&mut self, func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.exit_function(func, ctx); self.x1_react.exit_function(func, ctx); - self.x2_es2017.exit_function(func, ctx); self.x3_es2015.exit_function(func, ctx); } @@ -287,6 +286,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { ctx: &mut TraverseCtx<'a>, ) { self.x0_typescript.exit_method_definition(def, ctx); + self.x2_es2017.exit_method_definition(def, ctx); } fn enter_new_expression(&mut self, expr: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 4faa31262c0e6..7527a4064f4af 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: d20b314c -Passed: 348/1058 +Passed: 355/1058 # All Passed: * babel-plugin-transform-class-static-block @@ -1632,13 +1632,10 @@ x Output mismatch x Output mismatch -# babel-plugin-transform-async-to-generator (2/24) +# babel-plugin-transform-async-to-generator (9/24) * assumption-ignoreFunctionLength-true/basic/input.mjs x Output mismatch -* assumption-ignoreFunctionLength-true/export-default-function/input.mjs -x Output mismatch - * assumption-noNewArrows-false/basic/input.js x Output mismatch @@ -1648,21 +1645,9 @@ x Output mismatch * async-to-generator/async-iife-with-regenerator-spec/input.js x Output mismatch -* async-to-generator/function-arity/input.js -x Output mismatch - * async-to-generator/object-method-with-super/input.js x Output mismatch -* async-to-generator/shadowed-promise/input.js -x Output mismatch - -* async-to-generator/shadowed-promise-import/input.mjs -x Output mismatch - -* async-to-generator/shadowed-promise-nested/input.js -x Output mismatch - * bluebird-coroutines/arrow-function/input.js x Output mismatch @@ -1681,15 +1666,9 @@ x Output mismatch * regression/15978/input.js x Output mismatch -* regression/8783/input.js -x Output mismatch - * regression/T7108/input.js x Output mismatch -* regression/T7194/input.js -x Output mismatch - * regression/gh-6923/input.js x Output mismatch diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 956d9922446d7..894c92fe358a8 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,11 +1,12 @@ commit: d20b314c -Passed: 67/76 +Passed: 73/82 # All Passed: * babel-plugin-transform-class-static-block * babel-plugin-transform-nullish-coalescing-operator * babel-plugin-transform-optional-catch-binding +* babel-plugin-transform-async-to-generator * babel-plugin-transform-exponentiation-operator * babel-plugin-transform-arrow-functions * babel-preset-typescript diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/input.js new file mode 100644 index 0000000000000..89d618bba4a65 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/input.js @@ -0,0 +1,7 @@ +const A = async (a) => { + await Promise.resolve(); +} +setTimeout(async (p = 0) => { + await Promise.resolve(); + console.log(p) +}) \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/output.js new file mode 100644 index 0000000000000..80edd17bf322f --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/arrow/basic/output.js @@ -0,0 +1,12 @@ +const A = /*#__PURE__*/function () { + var _ref = babelHelpers.asyncToGenerator(function* (a) { + yield Promise.resolve(); + }); + return function A(_x) { + return _ref.apply(this, arguments); + }; +}(); +setTimeout(/*#__PURE__*/babelHelpers.asyncToGenerator(function* (p = 0) { + yield Promise.resolve(); + console.log(p); +})); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/input.js new file mode 100644 index 0000000000000..61f24000ae93d --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/input.js @@ -0,0 +1,9 @@ +class ClassWithAsyncMethods { + async with_parameters(p, [p1, p2]) { + return console.log(p, p1, p2); + } + + async without_parameters() { + console.log(ClassWithAsyncMethods); + } +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/output.js new file mode 100644 index 0000000000000..d0bddebb1df23 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/class/method-definition/output.js @@ -0,0 +1,12 @@ +class ClassWithAsyncMethods { + with_parameters(p, [p1, p2]) { + return babelHelpers.asyncToGenerator(function* () { + return console.log(p, p1, p2); + })(); + } + without_parameters() { + return babelHelpers.asyncToGenerator(function* () { + console.log(ClassWithAsyncMethods); + })(); + } +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/input.js new file mode 100644 index 0000000000000..d1adc5afb1d51 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/input.js @@ -0,0 +1,3 @@ +export default async function D(a, b = 0) { + await Promise.resolve(); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/output.js new file mode 100644 index 0000000000000..16939fde0fa1d --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-with-name/output.js @@ -0,0 +1,9 @@ +export default function D(_x) { + return _D.apply(this, arguments); +} +function _D() { + _D = babelHelpers.asyncToGenerator(function* (a, b = 0) { + yield Promise.resolve(); + }); + return _D.apply(this, arguments); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/input.js new file mode 100644 index 0000000000000..025209584170b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/input.js @@ -0,0 +1,3 @@ +export default async function (a, b = 0) { + await Promise.resolve(); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/output.js new file mode 100644 index 0000000000000..658aa5bce0e5c --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/default-without-name/output.js @@ -0,0 +1,9 @@ +export default function (_x) { + return _ref.apply(this, arguments); +} +function _ref() { + _ref = babelHelpers.asyncToGenerator(function* (a, b = 0) { + yield Promise.resolve(); + }); + return _ref.apply(this, arguments); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/input.js new file mode 100644 index 0000000000000..636fbd34107e9 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/input.js @@ -0,0 +1,3 @@ +export async function named(...args) { + await Promise.resolve(); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/output.js new file mode 100644 index 0000000000000..b57b5139f76f2 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/export/named/output.js @@ -0,0 +1,9 @@ +export function named() { + return _named.apply(this, arguments); +} +function _named() { + _named = babelHelpers.asyncToGenerator(function* (...args) { + yield Promise.resolve(); + }); + return _named.apply(this, arguments); +} \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/input.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/input.js new file mode 100644 index 0000000000000..dc12fb582d6a2 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/input.js @@ -0,0 +1,6 @@ +const func = async function (a, b) { + console.log(a, await Promise.resolve()); +} +setTimeout(async function (p = 0) { + await Promise.resolve(); +}) \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/output.js new file mode 100644 index 0000000000000..fd7293e93f29b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/expression/output.js @@ -0,0 +1,11 @@ +const func = /*#__PURE__*/function () { + var _ref = babelHelpers.asyncToGenerator(function* (a, b) { + console.log(a, yield Promise.resolve()); + }); + return function func(_x, _x2) { + return _ref.apply(this, arguments); + }; +}(); +setTimeout(/*#__PURE__*/babelHelpers.asyncToGenerator(function* (p = 0) { + yield Promise.resolve(); +})); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/options.json new file mode 100644 index 0000000000000..84679ca3cf181 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/function/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-async-to-generator" + ] +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/options.json new file mode 100644 index 0000000000000..84679ca3cf181 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-async-to-generator/test/fixtures/options.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "transform-async-to-generator" + ] +}