diff --git a/crates/oxc_transformer/src/es2017/async_to_generator.rs b/crates/oxc_transformer/src/es2017/async_to_generator.rs new file mode 100644 index 0000000000000..6505a42fd3a53 --- /dev/null +++ b/crates/oxc_transformer/src/es2017/async_to_generator.rs @@ -0,0 +1,233 @@ +//! ES2017: Async / Await \[WIP\] +//! +//! This plugin transforms async functions to generator functions. +//! +//! ## Example +//! +//! Input: +//! ```js +//! async function foo() { +//! await bar(); +//! } +//! const foo2 = async () => { +//! await bar(); +//! }; +//! async () => { +//! await bar(); +//! } +//! ``` +//! +//! Output (Currently): +//! ```js +//! function foo() { +//! return _asyncToGenerator(function* () { +//! yield bar(); +//! }) +//! } +//! const foo2 = () => _asyncToGenerator(function* () { +//! yield bar(); +//! } +//! ``` +//! +//! ## Implementation +//! +//! Implementation based on [@babel/plugin-transform-async-to-generator](https://babel.dev/docs/babel-plugin-transform-async-to-generator). +//! +//! +//! Reference: +//! * Babel docs: +//! * Esbuild implementation: +//! * Babel implementation: +//! * Babel helper implementation: +//! * Async / Await TC39 proposal: +//! + +use oxc_ast::ast::{ + ArrowFunctionExpression, Expression, Function, FunctionType, Statement, VariableDeclarationKind, +}; +use oxc_ast::NONE; +use oxc_span::{Atom, SPAN}; +use oxc_syntax::reference::ReferenceFlags; +use oxc_syntax::symbol::SymbolId; +use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; + +pub struct AsyncToGenerator; + +impl AsyncToGenerator { + fn get_helper_callee<'a>( + symbol_id: Option, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let ident = ctx.create_reference_id( + SPAN, + Atom::from("babelHelpers"), + symbol_id, + ReferenceFlags::Read, + ); + let object = ctx.ast.expression_from_identifier_reference(ident); + let property = ctx.ast.identifier_name(SPAN, Atom::from("asyncToGenerator")); + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)) + } + + fn transform_function<'a>(func: &mut Function<'a>, ctx: &mut TraverseCtx<'a>) -> Function<'a> { + let babel_helpers_id = ctx.scopes().find_binding(ctx.current_scope_id(), "babelHelpers"); + let callee = Self::get_helper_callee(babel_helpers_id, ctx); + 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( + SPAN, + func.params.kind, + ctx.ast.move_vec(&mut func.params.items), + func.params.rest.take(), + )), + func.return_type.take(), + func.body.take(), + ); + let parameters = + ctx.ast.vec1(ctx.ast.argument_expression(ctx.ast.expression_from_function(target))); + let call = ctx.ast.expression_call(SPAN, callee, NONE, parameters, false); + 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), + ) + } +} + +impl<'a> Traverse<'a> for AsyncToGenerator { + 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)), + ); + } + } 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); + } + } + + 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_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)); + } + } + } + + fn exit_arrow_function_expression( + &mut self, + arrow: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !arrow.r#async { + return; + } + let babel_helpers_id = ctx.scopes().find_binding(ctx.current_scope_id(), "babelHelpers"); + let callee = Self::get_helper_callee(babel_helpers_id, ctx); + let body = ctx.ast.function_body( + SPAN, + ctx.ast.move_vec(&mut arrow.body.directives), + ctx.ast.move_vec(&mut arrow.body.statements), + ); + let target = ctx.ast.function( + FunctionType::FunctionExpression, + SPAN, + None, + true, + false, + false, + arrow.type_parameters.take(), + NONE, + ctx.ast.alloc(ctx.ast.formal_parameters( + SPAN, + arrow.params.kind, + ctx.ast.move_vec(&mut arrow.params.items), + arrow.params.rest.take(), + )), + arrow.return_type.take(), + Some(body), + ); + let parameters = + ctx.ast.vec1(ctx.ast.argument_expression(ctx.ast.expression_from_function(target))); + let call = ctx.ast.expression_call(SPAN, callee, NONE, parameters, false); + let body = ctx.ast.function_body( + SPAN, + ctx.ast.vec(), + ctx.ast.vec1(ctx.ast.statement_expression(SPAN, call)), + ); + arrow.body = ctx.ast.alloc(body); + arrow.r#async = false; + arrow.expression = true; + } +} diff --git a/crates/oxc_transformer/src/es2017/mod.rs b/crates/oxc_transformer/src/es2017/mod.rs new file mode 100644 index 0000000000000..68e2b5e39eeac --- /dev/null +++ b/crates/oxc_transformer/src/es2017/mod.rs @@ -0,0 +1,45 @@ +mod async_to_generator; +pub mod options; + +use crate::es2017::async_to_generator::AsyncToGenerator; +use crate::es2017::options::ES2017Options; +use oxc_ast::ast::{ArrowFunctionExpression, Expression, Statement}; +use oxc_traverse::{Traverse, TraverseCtx}; + +#[allow(dead_code)] +pub struct ES2017 { + options: ES2017Options, + + // Plugins + async_to_generator: AsyncToGenerator, +} + +impl ES2017 { + pub fn new(options: ES2017Options) -> ES2017 { + ES2017 { async_to_generator: AsyncToGenerator, options } + } +} + +impl<'a> Traverse<'a> for ES2017 { + fn exit_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.async_to_generator { + self.async_to_generator.exit_expression(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); + } + } + + fn exit_arrow_function_expression( + &mut self, + node: &mut ArrowFunctionExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.async_to_generator { + self.async_to_generator.exit_arrow_function_expression(node, ctx); + } + } +} diff --git a/crates/oxc_transformer/src/es2017/options.rs b/crates/oxc_transformer/src/es2017/options.rs new file mode 100644 index 0000000000000..16abf9d1afebc --- /dev/null +++ b/crates/oxc_transformer/src/es2017/options.rs @@ -0,0 +1,27 @@ +use crate::env::{can_enable_plugin, Versions}; +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2017Options { + #[serde(skip)] + pub async_to_generator: bool, +} + +impl ES2017Options { + pub fn with_async_to_generator(&mut self, enable: bool) -> &mut Self { + self.async_to_generator = enable; + self + } + + #[must_use] + pub fn from_targets_and_bugfixes(targets: Option<&Versions>, bugfixes: bool) -> Self { + Self { + async_to_generator: can_enable_plugin( + "transform-async-to-generator", + targets, + bugfixes, + ), + } + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 23da2df5e52f5..05923286eb00a 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -19,6 +19,7 @@ mod options; mod env; mod es2015; mod es2016; +mod es2017; mod es2018; mod es2019; mod es2020; @@ -33,6 +34,7 @@ use std::path::Path; use common::Common; use es2016::ES2016; +use es2017::ES2017; use es2018::ES2018; use es2019::ES2019; use es2020::ES2020; @@ -95,6 +97,7 @@ impl<'a> Transformer<'a> { x2_es2019: ES2019::new(self.options.es2019), x2_es2018: ES2018::new(self.options.es2018, &self.ctx), x2_es2016: ES2016::new(self.options.es2016, &self.ctx), + x2_es2017: ES2017::new(self.options.es2017), x3_es2015: ES2015::new(self.options.es2015), x4_regexp: RegExp::new(self.options.regexp, &self.ctx), common: Common::new(&self.ctx), @@ -113,6 +116,7 @@ struct TransformerImpl<'a, 'ctx> { x2_es2020: ES2020<'a, 'ctx>, x2_es2019: ES2019, x2_es2018: ES2018<'a, 'ctx>, + x2_es2017: ES2017, x2_es2016: ES2016<'a, 'ctx>, x3_es2015: ES2015<'a>, x4_regexp: RegExp<'a, 'ctx>, @@ -196,6 +200,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_react.exit_expression(expr, ctx); + self.x2_es2017.exit_expression(expr, ctx); self.x3_es2015.exit_expression(expr, ctx); } @@ -230,6 +235,7 @@ 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); } @@ -326,6 +332,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { .push(ctx.ast.statement_return(SPAN, Some(statement.unbox().expression))); arrow.expression = false; } + self.x2_es2017.exit_arrow_function_expression(arrow, ctx); } fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { @@ -334,6 +341,10 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> { self.common.exit_statements(stmts, ctx); } + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.x2_es2017.exit_statement(stmt, ctx); + } + fn enter_tagged_template_expression( &mut self, expr: &mut TaggedTemplateExpression<'a>, diff --git a/crates/oxc_transformer/src/options/transformer.rs b/crates/oxc_transformer/src/options/transformer.rs index 12a97f1341af3..6464924e053a3 100644 --- a/crates/oxc_transformer/src/options/transformer.rs +++ b/crates/oxc_transformer/src/options/transformer.rs @@ -9,6 +9,7 @@ use crate::{ env::{can_enable_plugin, EnvOptions, Versions}, es2015::{ArrowFunctionsOptions, ES2015Options}, es2016::ES2016Options, + es2017::options::ES2017Options, es2018::{ES2018Options, ObjectRestSpreadOptions}, es2019::ES2019Options, es2020::ES2020Options, @@ -47,6 +48,8 @@ pub struct TransformOptions { pub es2016: ES2016Options, + pub es2017: ES2017Options, + pub es2018: ES2018Options, pub es2019: ES2019Options, @@ -86,6 +89,7 @@ impl TransformOptions { }, es2016: ES2016Options { exponentiation_operator: true }, es2018: ES2018Options { object_rest_spread: Some(ObjectRestSpreadOptions::default()) }, + es2017: ES2017Options { async_to_generator: true }, es2019: ES2019Options { optional_catch_binding: true }, es2020: ES2020Options { nullish_coalescing_operator: true }, es2021: ES2021Options { logical_assignment_operators: true }, @@ -100,6 +104,7 @@ impl TransformOptions { Self { es2015: ES2015Options::from_targets_and_bugfixes(targets, bugfixes), es2016: ES2016Options::from_targets_and_bugfixes(targets, bugfixes), + es2017: ES2017Options::from_targets_and_bugfixes(targets, bugfixes), es2018: ES2018Options::from_targets_and_bugfixes(targets, bugfixes), es2019: ES2019Options::from_targets_and_bugfixes(targets, bugfixes), es2020: ES2020Options::from_targets_and_bugfixes(targets, bugfixes), @@ -213,6 +218,11 @@ impl TransformOptions { get_enabled_plugin_options(plugin_name, options, targets.as_ref(), bugfixes).is_some() }); + transformer_options.es2017.with_async_to_generator({ + let plugin_name = "transform-async-to-generator"; + get_enabled_plugin_options(plugin_name, options, targets.as_ref(), bugfixes).is_some() + }); + transformer_options.es2018.with_object_rest_spread({ let plugin_name = "transform-object-rest-spread"; get_enabled_plugin_options(plugin_name, options, targets.as_ref(), bugfixes).map( diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index 03d01105d3653..8cb5d420a33f2 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: 3bcfee23 -Passed: 338/1022 +Passed: 339/1039 # All Passed: * babel-plugin-transform-logical-assignment-operators @@ -1628,6 +1628,56 @@ x Output mismatch x Output mismatch +# babel-plugin-transform-async-to-generator (1/17) +* 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 + +* bluebird-coroutines/arrow-function/input.js +x Output mismatch + +* bluebird-coroutines/class/input.js +x Output mismatch + +* bluebird-coroutines/expression/input.js +x Output mismatch + +* bluebird-coroutines/named-expression/input.js +x Output mismatch + +* bluebird-coroutines/statement/input.js +x Output mismatch + +* regression/15978/input.js +x Output mismatch + +* regression/4599/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 + +* regression/in-uncompiled-class-fields/input.js +x Output mismatch + +* regression/regression-2765/input.js +x Output mismatch + + # babel-plugin-transform-exponentiation-operator (3/4) * regression/4349/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 0eaa0432a6553..34aceed67d863 100644 --- a/tasks/transform_conformance/snapshots/babel_exec.snap.md +++ b/tasks/transform_conformance/snapshots/babel_exec.snap.md @@ -1,6 +1,6 @@ commit: 3bcfee23 -Passed: 33/54 +Passed: 34/60 # All Passed: * babel-plugin-transform-logical-assignment-operators @@ -10,13 +10,16 @@ Passed: 33/54 * babel-plugin-transform-arrow-functions -# babel-preset-env (8/11) +# babel-preset-env (7/11) * plugins-integration/class-arrow-super-tagged-expr/exec.js exec failed * plugins-integration/issue-15170/exec.js exec failed +* plugins-integration/regression-7064/exec.js +exec failed + * sanity/check-es2015-constants/exec.js exec failed @@ -71,6 +74,20 @@ exec failed exec failed +# babel-plugin-transform-async-to-generator (2/6) +* regression/15978/exec.js +exec failed + +* regression/8783/exec.js +exec failed + +* regression/T6882/exec.js +exec failed + +* regression/test262-fn-length/exec.js +exec failed + + # babel-plugin-transform-react-jsx-source (0/2) * react-source/basic-sample/exec.js exec failed diff --git a/tasks/transform_conformance/src/constants.rs b/tasks/transform_conformance/src/constants.rs index 01e619b1c4bb7..a22718f51b63b 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -29,7 +29,7 @@ pub(crate) const PLUGINS: &[&str] = &[ // "babel-plugin-transform-dotall-regex", // // [Regex] "babel-plugin-transform-named-capturing-groups-regex", // // ES2017 - // "babel-plugin-transform-async-to-generator", + "babel-plugin-transform-async-to-generator", // ES2016 "babel-plugin-transform-exponentiation-operator", // ES2015 diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index baab1fa7980a1..f310f5298187b 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -4,6 +4,7 @@ use std::{ }; use cow_utils::CowUtils; +use oxc::parser::ParseOptions; use oxc::{ allocator::Allocator, codegen::{CodeGenerator, CodegenOptions}, @@ -298,7 +299,14 @@ impl TestCase for ConformanceTestCase { String::default, |output| { // Get expected code by parsing the source text, so we can get the same code generated result. - let ret = Parser::new(&allocator, &output, source_type).parse(); + let ret = Parser::new(&allocator, &output, source_type) + .with_options(ParseOptions { + // Related: async to generator, regression + allow_return_outside_function: true, + ..Default::default() + }) + .parse(); + CodeGenerator::new() .with_options(CodegenOptions { comments: false,