From f58b6279c80a0384e7cee2e59346f7595049e46c Mon Sep 17 00:00:00 2001 From: Dunqing Date: Wed, 13 Dec 2023 20:46:28 +0800 Subject: [PATCH] feat(transformer): add arrow_functions plugin (#1663) --- .../src/es2015/arrow_functions.rs | 143 ++++++++++++++++++ crates/oxc_transformer/src/es2015/mod.rs | 2 + crates/oxc_transformer/src/lib.rs | 5 + crates/oxc_transformer/src/options.rs | 6 +- tasks/transform_conformance/babel.snap.md | 25 ++- .../transform_conformance/babel_exec.snap.md | 5 +- tasks/transform_conformance/src/lib.rs | 1 + tasks/transform_conformance/src/test_case.rs | 7 +- 8 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 crates/oxc_transformer/src/es2015/arrow_functions.rs diff --git a/crates/oxc_transformer/src/es2015/arrow_functions.rs b/crates/oxc_transformer/src/es2015/arrow_functions.rs new file mode 100644 index 0000000000000..d8b528f01e730 --- /dev/null +++ b/crates/oxc_transformer/src/es2015/arrow_functions.rs @@ -0,0 +1,143 @@ +use std::rc::Rc; + +use oxc_allocator::Vec; +use oxc_ast::{ast::*, AstBuilder, AstKind, VisitMut}; +use oxc_span::{Atom, SPAN}; +use serde::Deserialize; + +use crate::context::TransformerCtx; +use crate::options::TransformOptions; +use crate::TransformTarget; + +/// ES2015 Arrow Functions +/// +/// References: +/// * +/// * +pub struct ArrowFunctions<'a> { + ast: Rc>, + nodes: Vec<'a, AstKind<'a>>, + uid: usize, + has_this: bool, + /// Insert a variable declaration at the top of the BlockStatement + insert: bool, +} + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct ArrowFunctionsOptions { + /// This option enables the following: + /// * Wrap the generated function in .bind(this) and keeps uses of this inside the function as-is, instead of using a renamed this. + /// * Add a runtime check to ensure the functions are not instantiated. + /// * Add names to arrow functions. + pub spec: bool, +} + +impl<'a> VisitMut<'a> for ArrowFunctions<'a> { + fn enter_node(&mut self, kind: AstKind<'a>) { + self.nodes.push(kind); + } + + fn leave_node(&mut self, _kind: AstKind<'a>) { + self.nodes.pop(); + } + + fn visit_jsx_identifier(&mut self, ident: &mut JSXIdentifier) { + let parent_kind = self.nodes.last().unwrap(); + let parent_parent_kind = self.nodes[self.nodes.len() - 2]; + if ident.name == "this" + && (matches!(parent_kind, AstKind::JSXElementName(_)) + || matches!(parent_parent_kind, AstKind::JSXMemberExpression(_))) + { + if !self.has_this { + self.has_this = true; + self.uid += 1; + } + *ident = self.ast.jsx_identifier(SPAN, self.get_this_name()); + } + } +} + +impl<'a> ArrowFunctions<'a> { + pub fn new( + ast: Rc>, + _: TransformerCtx<'a>, + options: &TransformOptions, + ) -> Option { + (options.target < TransformTarget::ES2015 || options.arrow_functions.is_some()).then(|| { + let nodes = ast.new_vec(); + Self { ast, uid: 0, nodes, has_this: false, insert: false } + }) + } + + fn get_this_name(&self) -> Atom { + let uid = if self.uid == 1 { String::new() } else { self.uid.to_string() }; + format!("_this{uid}",).into() + } + + pub fn transform_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + if self.insert { + let binding_pattern = self.ast.binding_pattern( + self.ast + .binding_pattern_identifier(BindingIdentifier::new(SPAN, self.get_this_name())), + None, + false, + ); + + let variable_declarator = self.ast.variable_declarator( + SPAN, + VariableDeclarationKind::Var, + binding_pattern, + Some(self.ast.this_expression(SPAN)), + false, + ); + + let stmt = self.ast.variable_declaration( + SPAN, + VariableDeclarationKind::Var, + self.ast.new_vec_single(variable_declarator), + Modifiers::empty(), + ); + stmts.insert(0, Statement::Declaration(Declaration::VariableDeclaration(stmt))); + self.insert = false; + } + + // Insert to parent block + if self.has_this { + self.insert = true; + self.has_this = false; + } + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { + if let Expression::ArrowExpression(arrow_expr) = expr { + let mut body = self.ast.copy(&arrow_expr.body); + + if arrow_expr.expression { + let first_stmt = body.statements.remove(0); + if let Statement::ExpressionStatement(stmt) = first_stmt { + let return_statement = + self.ast.return_statement(SPAN, Some(self.ast.copy(&stmt.expression))); + body.statements.push(return_statement); + } + } + + self.visit_function_body(&mut body); + + let new_function = self.ast.function( + FunctionType::FunctionExpression, + SPAN, + None, + false, + arrow_expr.generator, + arrow_expr.r#async, + self.ast.copy(&arrow_expr.params), + Some(body), + self.ast.copy(&arrow_expr.type_parameters), + self.ast.copy(&arrow_expr.return_type), + Modifiers::empty(), + ); + + *expr = Expression::FunctionExpression(new_function); + } + } +} diff --git a/crates/oxc_transformer/src/es2015/mod.rs b/crates/oxc_transformer/src/es2015/mod.rs index 1afd234a71198..ea89390f329be 100644 --- a/crates/oxc_transformer/src/es2015/mod.rs +++ b/crates/oxc_transformer/src/es2015/mod.rs @@ -1,8 +1,10 @@ +mod arrow_functions; mod duplicate_keys; mod function_name; mod shorthand_properties; mod template_literals; +pub use arrow_functions::{ArrowFunctions, ArrowFunctionsOptions}; pub use duplicate_keys::DuplicateKeys; pub use function_name::FunctionName; pub use shorthand_properties::ShorthandProperties; diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index fbe44b7c5b09c..68d70fa84bed2 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -40,6 +40,7 @@ use crate::{ }; pub use crate::{ + es2015::ArrowFunctionsOptions, es2020::NullishCoalescingOperatorOptions, options::{TransformOptions, TransformTarget}, react_jsx::{ReactJsxOptions, ReactJsxRuntime, ReactJsxRuntimeOption}, @@ -63,6 +64,7 @@ pub struct Transformer<'a> { es2016_exponentiation_operator: Option>, // es2015 es2015_function_name: Option>, + es2015_arrow_functions: Option>, es2015_shorthand_properties: Option>, es2015_template_literals: Option>, es2015_duplicate_keys: Option>, @@ -100,6 +102,7 @@ impl<'a> Transformer<'a> { es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), ctx.clone(), &options), // es2015 es2015_function_name: FunctionName::new(Rc::clone(&ast), ctx.clone(), &options), + es2015_arrow_functions: ArrowFunctions::new(Rc::clone(&ast), ctx.clone(), &options), es2015_shorthand_properties: ShorthandProperties::new(Rc::clone(&ast), &options), es2015_template_literals: TemplateLiterals::new(Rc::clone(&ast), &options), es2015_duplicate_keys: DuplicateKeys::new(Rc::clone(&ast), &options), @@ -155,6 +158,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { self.es2021_logical_assignment_operators.as_mut().map(|t| t.add_vars_to_statements(stmts)); self.es2020_nullish_coalescing_operators.as_mut().map(|t| t.add_vars_to_statements(stmts)); self.es2016_exponentiation_operator.as_mut().map(|t| t.add_vars_to_statements(stmts)); + self.es2015_arrow_functions.as_mut().map(|t| t.transform_statements(stmts)); } fn visit_statement(&mut self, stmt: &mut Statement<'a>) { @@ -174,6 +178,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> { self.es2021_logical_assignment_operators.as_mut().map(|t| t.transform_expression(expr)); self.es2020_nullish_coalescing_operators.as_mut().map(|t| t.transform_expression(expr)); + self.es2015_arrow_functions.as_mut().map(|t| t.transform_expression(expr)); self.es2016_exponentiation_operator.as_mut().map(|t| t.transform_expression(expr)); self.es2015_template_literals.as_mut().map(|t| t.transform_expression(expr)); diff --git a/crates/oxc_transformer/src/options.rs b/crates/oxc_transformer/src/options.rs index 7279851c3fb40..eaef7e82c1364 100644 --- a/crates/oxc_transformer/src/options.rs +++ b/crates/oxc_transformer/src/options.rs @@ -1,6 +1,9 @@ use oxc_syntax::assumptions::CompilerAssumptions; -use crate::{es2020::NullishCoalescingOperatorOptions, react_jsx::ReactJsxOptions}; +use crate::{ + es2015::ArrowFunctionsOptions, es2020::NullishCoalescingOperatorOptions, + react_jsx::ReactJsxOptions, +}; #[derive(Debug, Default, Clone)] pub struct TransformOptions { @@ -22,6 +25,7 @@ pub struct TransformOptions { // es2015 pub duplicate_keys: bool, pub function_name: bool, + pub arrow_functions: Option, pub shorthand_properties: bool, pub sticky_regex: bool, pub template_literals: bool, diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index e21167a27b1a7..629ba00df410f 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 277/1111 +Passed: 288/1137 # All Passed: * babel-plugin-transform-numeric-separator @@ -746,6 +746,25 @@ Passed: 277/1111 # babel-plugin-transform-exponentiation-operator (3/4) * regression/4349/input.js +# babel-plugin-transform-arrow-functions (9/26) +* arrow-functions/arguments/input.js +* arrow-functions/arguments-global-undeclared/input.js +* arrow-functions/arguments-global-var/input.js +* arrow-functions/default-parameters/input.js +* arrow-functions/destructuring-parameters/input.js +* arrow-functions/implicit-var-arguments/input.js +* arrow-functions/nested/input.js +* arrow-functions/self-referential/input.js +* arrow-functions/spec/input.js +* arrow-functions/super-call/input.js +* arrow-functions/super-prop/input.js +* arrow-functions/this/input.js +* assumption-newableArrowFunctions-false/basic/input.js +* assumption-newableArrowFunctions-false/naming/input.js +* assumption-newableArrowFunctions-false/self-referential/input.js +* spec/newableArrowFunction-default/input.js +* spec/newableArrowFunction-vs-spec-false/input.js + # babel-plugin-transform-unicode-regex (1/4) * unicode-regex/basic/input.js * unicode-regex/negated-set/input.js @@ -869,12 +888,10 @@ Passed: 277/1111 * regression/15768/input.ts * variable-declaration/non-null-in-optional-chain/input.ts -# babel-plugin-transform-react-jsx (144/156) +# babel-plugin-transform-react-jsx (146/156) * autoImport/after-polyfills-compiled-to-cjs/input.mjs * autoImport/complicated-scope-module/input.js -* react/arrow-functions/input.js * react/optimisation.react.constant-elements/input.js -* react-automatic/arrow-functions/input.js * react-automatic/optimisation.react.constant-elements/input.js * react-automatic/should-handle-attributed-elements/input.js * react-automatic/should-throw-when-filter-is-specified/input.js diff --git a/tasks/transform_conformance/babel_exec.snap.md b/tasks/transform_conformance/babel_exec.snap.md index b4d4ca6b5c8e1..a65fc66bdeb96 100644 --- a/tasks/transform_conformance/babel_exec.snap.md +++ b/tasks/transform_conformance/babel_exec.snap.md @@ -1,4 +1,4 @@ -Passed: 375/441 +Passed: 377/444 # All Passed: * babel-plugin-transform-class-static-block @@ -94,3 +94,6 @@ Passed: 375/441 * object-spread-loose-builtins/no-object-assign-exec/exec.js * object-spread-loose-builtins/side-effect/exec.js +# babel-plugin-transform-arrow-functions (2/3) +* arrow-functions/implicit-var-arguments/exec.js + diff --git a/tasks/transform_conformance/src/lib.rs b/tasks/transform_conformance/src/lib.rs index 7522ca47f47db..9907a801d05bf 100644 --- a/tasks/transform_conformance/src/lib.rs +++ b/tasks/transform_conformance/src/lib.rs @@ -74,6 +74,7 @@ const CASES: &[&str] = &[ // ES2016 "babel-plugin-transform-exponentiation-operator", // ES2015 + "babel-plugin-transform-arrow-functions", "babel-plugin-transform-shorthand-properties", "babel-plugin-transform-sticky-regex", "babel-plugin-transform-unicode-regex", diff --git a/tasks/transform_conformance/src/test_case.rs b/tasks/transform_conformance/src/test_case.rs index 14a766a83db10..1fc8ce67a3956 100644 --- a/tasks/transform_conformance/src/test_case.rs +++ b/tasks/transform_conformance/src/test_case.rs @@ -11,8 +11,8 @@ use oxc_semantic::SemanticBuilder; use oxc_span::{SourceType, VALID_EXTENSIONS}; use oxc_tasks_common::{normalize_path, print_diff_in_terminal, BabelOptions}; use oxc_transformer::{ - NullishCoalescingOperatorOptions, ReactJsxOptions, TransformOptions, TransformTarget, - Transformer, + ArrowFunctionsOptions, NullishCoalescingOperatorOptions, ReactJsxOptions, TransformOptions, + TransformTarget, Transformer, }; use serde::de::DeserializeOwned; use serde_json::Value; @@ -95,6 +95,9 @@ pub trait TestCase { assumptions: options.assumptions, class_static_block: options.get_plugin("transform-class-static-block").is_some(), function_name: options.get_plugin("transform-function-name").is_some(), + arrow_functions: options + .get_plugin("transform-arrow-functions") + .map(get_options::), logical_assignment_operators: options .get_plugin("transform-logical-assignment-operators") .is_some(),