feat(transformer): async-to-generator plugin. (#5590)

Tests are still not passed. A lot need to do yet.

---------

Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
Ethan Goh 2024-10-14 17:16:58 +08:00 committed by GitHub
parent 3556062213
commit a9260cf6d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 406 additions and 5 deletions

View file

@ -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: <https://babeljs.io/docs/en/babel-plugin-transform-async-to-generator>
//! * Esbuild implementation: <https://github.com/evanw/esbuild/blob/main/internal/js_parser/js_parser_lower.go#L392>
//! * Babel implementation: <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-async-to-generator>
//! * Babel helper implementation: <https://github.com/babel/babel/blob/main/packages/babel-helper-remap-async-to-generator>
//! * Async / Await TC39 proposal: <https://github.com/tc39/proposal-async-await>
//!
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<SymbolId>,
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;
}
}

View file

@ -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);
}
}
}

View file

@ -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,
),
}
}
}

View file

@ -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>,

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,