Skip to content

Commit

Permalink
feat(transformer): add arrow_functions plugin (#1663)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing authored Dec 13, 2023
1 parent 67b7cc0 commit f58b627
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 8 deletions.
143 changes: 143 additions & 0 deletions crates/oxc_transformer/src/es2015/arrow_functions.rs
Original file line number Diff line number Diff line change
@@ -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:
/// * <https://babeljs.io/docs/babel-plugin-transform-arrow-functions>
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-arrow-functions>
pub struct ArrowFunctions<'a> {
ast: Rc<AstBuilder<'a>>,
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<AstBuilder<'a>>,
_: TransformerCtx<'a>,
options: &TransformOptions,
) -> Option<Self> {
(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);
}
}
}
2 changes: 2 additions & 0 deletions crates/oxc_transformer/src/es2015/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ use crate::{
};

pub use crate::{
es2015::ArrowFunctionsOptions,
es2020::NullishCoalescingOperatorOptions,
options::{TransformOptions, TransformTarget},
react_jsx::{ReactJsxOptions, ReactJsxRuntime, ReactJsxRuntimeOption},
Expand All @@ -63,6 +64,7 @@ pub struct Transformer<'a> {
es2016_exponentiation_operator: Option<ExponentiationOperator<'a>>,
// es2015
es2015_function_name: Option<FunctionName<'a>>,
es2015_arrow_functions: Option<ArrowFunctions<'a>>,
es2015_shorthand_properties: Option<ShorthandProperties<'a>>,
es2015_template_literals: Option<TemplateLiterals<'a>>,
es2015_duplicate_keys: Option<DuplicateKeys<'a>>,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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>) {
Expand All @@ -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));

Expand Down
6 changes: 5 additions & 1 deletion crates/oxc_transformer/src/options.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -22,6 +25,7 @@ pub struct TransformOptions {
// es2015
pub duplicate_keys: bool,
pub function_name: bool,
pub arrow_functions: Option<ArrowFunctionsOptions>,
pub shorthand_properties: bool,
pub sticky_regex: bool,
pub template_literals: bool,
Expand Down
25 changes: 21 additions & 4 deletions tasks/transform_conformance/babel.snap.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Passed: 277/1111
Passed: 288/1137

# All Passed:
* babel-plugin-transform-numeric-separator
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion tasks/transform_conformance/babel_exec.snap.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Passed: 375/441
Passed: 377/444

# All Passed:
* babel-plugin-transform-class-static-block
Expand Down Expand Up @@ -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

1 change: 1 addition & 0 deletions tasks/transform_conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions tasks/transform_conformance/src/test_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<ArrowFunctionsOptions>),
logical_assignment_operators: options
.get_plugin("transform-logical-assignment-operators")
.is_some(),
Expand Down

0 comments on commit f58b627

Please sign in to comment.