diff --git a/crates/oxc_transformer/src/es2016/exponentiation_operator.rs b/crates/oxc_transformer/src/es2016/exponentiation_operator.rs new file mode 100644 index 0000000000000..aa64e3951669c --- /dev/null +++ b/crates/oxc_transformer/src/es2016/exponentiation_operator.rs @@ -0,0 +1,322 @@ +use std::cell::Cell; + +use oxc_allocator::{CloneIn, Vec}; +use oxc_ast::ast::*; +use oxc_semantic::{ReferenceFlag, SymbolFlags}; +use oxc_span::SPAN; +use oxc_syntax::operator::{AssignmentOperator, BinaryOperator}; +use oxc_traverse::TraverseCtx; + +use crate::context::Ctx; + +/// ES2016: Exponentiation Operator +/// +/// References: +/// * +/// * +/// * +pub struct ExponentiationOperator<'a> { + _ctx: Ctx<'a>, + var_declarations: std::vec::Vec>>, +} + +#[derive(Debug)] +struct Exploded<'a> { + reference: AssignmentTarget<'a>, + uid: Expression<'a>, +} + +impl<'a> ExponentiationOperator<'a> { + pub fn new(ctx: Ctx<'a>) -> Self { + Self { _ctx: ctx, var_declarations: vec![] } + } + + fn clone_identifier_reference( + ident: &IdentifierReference<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> IdentifierReference<'a> { + let reference = ctx.symbols().get_reference(ident.reference_id.get().unwrap()); + let symbol_id = reference.symbol_id(); + let flag = reference.flag(); + ctx.create_reference_id(ident.span, ident.name.clone(), symbol_id, *flag) + } + + fn clone_expression(expr: &Expression<'a>, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + match expr { + Expression::Identifier(ident) => ctx + .ast + .expression_from_identifier_reference(Self::clone_identifier_reference(ident, ctx)), + _ => expr.clone_in(ctx.ast.allocator), + } + } + + pub fn transform_statements( + &mut self, + _statements: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + self.var_declarations.push(ctx.ast.vec()); + } + + pub fn transform_statements_on_exit( + &mut self, + statements: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if let Some(declarations) = self.var_declarations.pop() { + if declarations.is_empty() { + return; + } + let variable = ctx.ast.alloc_variable_declaration( + SPAN, + VariableDeclarationKind::Var, + declarations, + false, + ); + statements.insert(0, Statement::VariableDeclaration(variable)); + } + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + // left ** right + if let Expression::BinaryExpression(binary_expr) = expr { + if binary_expr.operator == BinaryOperator::Exponential { + let left = ctx.ast.move_expression(&mut binary_expr.left); + let right = ctx.ast.move_expression(&mut binary_expr.right); + *expr = Self::math_pow(left, right, ctx); + } + } + + // left **= right + if let Expression::AssignmentExpression(assign_expr) = expr { + if assign_expr.operator == AssignmentOperator::Exponential { + let mut nodes = ctx.ast.vec(); + let Some(Exploded { reference, uid }) = + self.explode(&mut assign_expr.left, &mut nodes, ctx) + else { + return; + }; + let right = ctx.ast.move_expression(&mut assign_expr.right); + let right = Self::math_pow(uid, right, ctx); + let assign_expr = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + reference, + right, + ); + nodes.push(assign_expr); + *expr = ctx.ast.expression_sequence(SPAN, nodes); + } + } + } + + /// `left ** right` -> `Math.pow(left, right)` + fn math_pow( + left: Expression<'a>, + right: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let ident_math = + ctx.create_reference_id(SPAN, ctx.ast.atom("Math"), None, ReferenceFlag::Read); + let object = ctx.ast.expression_from_identifier_reference(ident_math); + let property = ctx.ast.identifier_name(SPAN, "pow"); + let callee = + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)); + let mut arguments = ctx.ast.vec_with_capacity(2); + arguments.push(Argument::from(left)); + arguments.push(Argument::from(right)); + ctx.ast.expression_call( + SPAN, + arguments, + callee, + None::>, + false, + ) + } + + /// Change `lhs **= 2` to `var temp; temp = lhs, lhs = Math.pow(temp, 2);`. + /// If the lhs is a member expression `obj.ref` or `obj[ref]`, assign them to a temporary variable so side-effects are not computed twice. + /// For `obj.ref`, change it to `var _obj; _obj = obj, _obj["ref"] = Math.pow(_obj["ref"], 2)`. + /// For `obj[ref]`, change it to `var _obj, _ref; _obj = obj, _ref = ref, _obj[_ref] = Math.pow(_obj[_ref], 2);`. + fn explode( + &mut self, + node: &mut AssignmentTarget<'a>, + nodes: &mut Vec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let node = node.as_simple_assignment_target_mut()?; + let obj = self.get_obj_ref(node, nodes, ctx)?; + let (reference, uid) = match node { + SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => { + let reference = AssignmentTarget::AssignmentTargetIdentifier( + ctx.ast.alloc(Self::clone_identifier_reference(ident.as_ref(), ctx)), + ); + (reference, obj) + } + match_member_expression!(SimpleAssignmentTarget) => { + let member_expr = node.to_member_expression_mut(); + let computed = member_expr.is_computed(); + let prop = self.get_prop_ref(member_expr, nodes, ctx)?; + let optional = false; + let obj_clone = Self::clone_expression(&obj, ctx); + let (reference, uid) = match &prop { + Expression::Identifier(ident) if !computed => { + let ident = IdentifierName::new(SPAN, ident.name.clone()); + ( + // TODO: + // Both of these are the same, but it's in order to avoid after cloning without reference_id. + // Related: https://github.com/oxc-project/oxc/issues/4804 + ctx.ast.member_expression_static( + SPAN, + obj_clone, + ident.clone(), + optional, + ), + ctx.ast.member_expression_static(SPAN, obj, ident, optional), + ) + } + _ => { + let prop_clone = Self::clone_expression(&prop, ctx); + ( + ctx.ast + .member_expression_computed(SPAN, obj_clone, prop_clone, optional), + ctx.ast.member_expression_computed(SPAN, obj, prop, optional), + ) + } + }; + ( + AssignmentTarget::from( + ctx.ast.simple_assignment_target_member_expression(reference), + ), + Expression::from(uid), + ) + } + _ => return None, + }; + Some(Exploded { reference, uid }) + } + + /// Make sure side-effects of evaluating `obj` of `obj.ref` and `obj[ref]` only happen once. + fn get_obj_ref( + &mut self, + node: &mut SimpleAssignmentTarget<'a>, + nodes: &mut Vec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let reference = match node { + SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) => { + if ident + .reference_id + .get() + .is_some_and(|reference_id| ctx.symbols().has_binding(reference_id)) + { + // this variable is declared in scope so we can be 100% sure + // that evaluating it multiple times won't trigger a getter + // or something else + return Some(ctx.ast.expression_from_identifier_reference( + Self::clone_identifier_reference(ident, ctx), + )); + } + // could possibly trigger a getter so we need to only evaluate it once + ctx.ast.expression_from_identifier_reference(Self::clone_identifier_reference( + ident, ctx, + )) + } + match_member_expression!(SimpleAssignmentTarget) => { + let expr = match node { + SimpleAssignmentTarget::ComputedMemberExpression(e) => &mut e.object, + SimpleAssignmentTarget::StaticMemberExpression(e) => &mut e.object, + SimpleAssignmentTarget::PrivateFieldExpression(e) => &mut e.object, + _ => unreachable!(), + }; + let expr = ctx.ast.move_expression(expr); + // the object reference that we need to save is locally declared + // so as per the previous comment we can be 100% sure evaluating + // it multiple times will be safe + // Super cannot be directly assigned so lets return it also + if matches!(expr, Expression::Super(_)) + || matches!(&expr, Expression::Identifier(ident) if ident + .reference_id + .get() + .is_some_and(|reference_id| ctx.symbols().has_binding(reference_id))) + { + return Some(expr); + } + + expr + } + _ => return None, + }; + Some(self.add_new_reference(reference, nodes, ctx)) + } + + /// Make sure side-effects of evaluating `ref` of `obj.ref` and `obj[ref]` only happen once. + fn get_prop_ref( + &mut self, + node: &mut MemberExpression<'a>, + nodes: &mut Vec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + let expr = match node { + MemberExpression::ComputedMemberExpression(expr) => { + let expr = ctx.ast.move_expression(&mut expr.expression); + if expr.is_literal() { + return Some(expr); + } + expr + } + MemberExpression::StaticMemberExpression(expr) => { + return Some(ctx.ast.expression_string_literal(SPAN, expr.property.name.clone())); + } + MemberExpression::PrivateFieldExpression(_) => { + // From babel: "We can't generate property ref for private name, please install `@babel/plugin-transform-class-properties`" + return None; + } + }; + Some(self.add_new_reference(expr, nodes, ctx)) + } + + fn add_new_reference( + &mut self, + expr: Expression<'a>, + nodes: &mut Vec<'a, Expression<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let name = match expr { + Expression::Identifier(ref ident) => ident.name.clone().as_str(), + _ => "ref", + }; + + let symbol_id = + ctx.generate_uid_in_current_scope(name, SymbolFlags::FunctionScopedVariable); + let symbol_name = ctx.ast.atom(ctx.symbols().get_name(symbol_id)); + + { + // var _name; + let binding_identifier = BindingIdentifier { + span: SPAN, + name: symbol_name.clone(), + symbol_id: Cell::new(Some(symbol_id)), + }; + let kind = VariableDeclarationKind::Var; + let id = ctx.ast.binding_pattern_kind_from_binding_identifier(binding_identifier); + let id = ctx.ast.binding_pattern(id, None::>, false); + self.var_declarations + .last_mut() + .unwrap() + .push(ctx.ast.variable_declarator(SPAN, kind, id, None, false)); + } + + let ident = + ctx.create_reference_id(SPAN, symbol_name, Some(symbol_id), ReferenceFlag::Read); + + // let ident = self.create_new_var_with_expression(&expr); + // Add new reference `_name = name` to nodes + let left = ctx.ast.simple_assignment_target_from_identifier_reference( + Self::clone_identifier_reference(&ident, ctx), + ); + let op = AssignmentOperator::Assign; + nodes.push(ctx.ast.expression_assignment(SPAN, op, AssignmentTarget::from(left), expr)); + ctx.ast.expression_from_identifier_reference(ident) + } +} diff --git a/crates/oxc_transformer/src/es2016/mod.rs b/crates/oxc_transformer/src/es2016/mod.rs new file mode 100644 index 0000000000000..a19d894546779 --- /dev/null +++ b/crates/oxc_transformer/src/es2016/mod.rs @@ -0,0 +1,52 @@ +mod exponentiation_operator; +mod options; + +pub use exponentiation_operator::ExponentiationOperator; +pub use options::ES2016Options; +use oxc_allocator::Vec; +use oxc_ast::ast::*; +use oxc_traverse::TraverseCtx; +use std::rc::Rc; + +use crate::context::Ctx; + +#[allow(dead_code)] +pub struct ES2016<'a> { + ctx: Ctx<'a>, + options: ES2016Options, + + // Plugins + exponentiation_operator: ExponentiationOperator<'a>, +} + +impl<'a> ES2016<'a> { + pub fn new(options: ES2016Options, ctx: Ctx<'a>) -> Self { + Self { exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ctx)), ctx, options } + } + + pub fn transform_statements( + &mut self, + statements: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.exponentiation_operator { + self.exponentiation_operator.transform_statements(statements, ctx); + } + } + + pub fn transform_statements_on_exit( + &mut self, + statements: &mut Vec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.exponentiation_operator { + self.exponentiation_operator.transform_statements_on_exit(statements, ctx); + } + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.exponentiation_operator { + self.exponentiation_operator.transform_expression(expr, ctx); + } + } +} diff --git a/crates/oxc_transformer/src/es2016/options.rs b/crates/oxc_transformer/src/es2016/options.rs new file mode 100644 index 0000000000000..802f750e3e135 --- /dev/null +++ b/crates/oxc_transformer/src/es2016/options.rs @@ -0,0 +1,16 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2016Options { + #[serde(skip)] + pub exponentiation_operator: bool, +} + +impl ES2016Options { + #[must_use] + pub fn with_exponentiation_operator(mut self, enable: bool) -> Self { + self.exponentiation_operator = enable; + self + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 88e5ca75ccef0..59bb65142a742 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -15,6 +15,7 @@ mod options; // Presets: mod env; mod es2015; +mod es2016; mod react; mod typescript; @@ -25,6 +26,7 @@ mod helpers { use std::{path::Path, rc::Rc}; +use es2016::ES2016; use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ast::*, AstBuilder, Trivias}; use oxc_diagnostics::OxcDiagnostic; @@ -58,6 +60,7 @@ pub struct Transformer<'a> { // NOTE: all callbacks must run in order. x0_typescript: TypeScript<'a>, x1_react: React<'a>, + x2_es2016: ES2016<'a>, x3_es2015: ES2015<'a>, } @@ -82,6 +85,7 @@ impl<'a> Transformer<'a> { ctx: Rc::clone(&ctx), x0_typescript: TypeScript::new(options.typescript, Rc::clone(&ctx)), x1_react: React::new(options.react, Rc::clone(&ctx)), + x2_es2016: ES2016::new(options.es2016, Rc::clone(&ctx)), x3_es2015: ES2015::new(options.es2015, ctx), } } @@ -160,6 +164,7 @@ impl<'a> Traverse<'a> for Transformer<'a> { fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_expression(expr); self.x1_react.transform_expression(expr, ctx); + self.x2_es2016.transform_expression(expr, ctx); self.x3_es2015.transform_expression(expr); } @@ -244,13 +249,15 @@ impl<'a> Traverse<'a> for Transformer<'a> { self.x0_typescript.transform_property_definition(def); } - fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) { + fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_statements(stmts); + self.x2_es2016.transform_statements(stmts, ctx); self.x3_es2015.enter_statements(stmts); } fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_statements_on_exit(stmts, ctx); + self.x2_es2016.transform_statements_on_exit(stmts, ctx); self.x3_es2015.exit_statements(stmts); } diff --git a/crates/oxc_transformer/src/options/transformer.rs b/crates/oxc_transformer/src/options/transformer.rs index d100bdc3a464e..445ada7d04992 100644 --- a/crates/oxc_transformer/src/options/transformer.rs +++ b/crates/oxc_transformer/src/options/transformer.rs @@ -7,6 +7,7 @@ use crate::{ compiler_assumptions::CompilerAssumptions, env::{can_enable_plugin, EnvOptions, Versions}, es2015::{ArrowFunctionsOptions, ES2015Options}, + es2016::ES2016Options, options::babel::BabelOptions, react::ReactOptions, typescript::TypeScriptOptions, @@ -34,6 +35,8 @@ pub struct TransformOptions { pub react: ReactOptions, pub es2015: ES2015Options, + + pub es2016: ES2016Options, } impl TransformOptions { @@ -104,6 +107,11 @@ impl TransformOptions { }) }); + let es2016 = ES2016Options::default().with_exponentiation_operator({ + let plugin_name = "transform-exponentiation-operator"; + enable_plugin(plugin_name, options, &env_options, &targets).is_some() + }); + let typescript = { let plugin_name = "transform-typescript"; from_value::(get_plugin_options(plugin_name, options)) @@ -135,6 +143,7 @@ impl TransformOptions { typescript, react, es2015, + es2016, }) } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index ae66627ffeb70..eb67b4b0ad850 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,6 +1,6 @@ commit: 12619ffe -Passed: 453/927 +Passed: 456/931 # All Passed: * babel-preset-react @@ -433,6 +433,9 @@ Passed: 453/927 * shipped-proposals/new-class-features-chrome-94/input.js * shipped-proposals/new-class-features-firefox-70/input.js +# babel-plugin-transform-exponentiation-operator (3/4) +* regression/4349/input.js + # babel-plugin-transform-arrow-functions (1/6) * assumption-newableArrowFunctions-false/basic/input.js * assumption-newableArrowFunctions-false/naming/input.js diff --git a/tasks/transform_conformance/babel_exec.snap.md b/tasks/transform_conformance/babel_exec.snap.md index 21032bec752ec..8e328b349eb34 100644 --- a/tasks/transform_conformance/babel_exec.snap.md +++ b/tasks/transform_conformance/babel_exec.snap.md @@ -1,8 +1,9 @@ commit: 12619ffe -Passed: 8/14 +Passed: 10/16 # All Passed: +* babel-plugin-transform-exponentiation-operator * babel-plugin-transform-arrow-functions diff --git a/tasks/transform_conformance/src/constants.rs b/tasks/transform_conformance/src/constants.rs index 766669094d549..f713a321bbed2 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -30,8 +30,8 @@ pub(crate) const PLUGINS: &[&str] = &[ // // [Regex] "babel-plugin-transform-named-capturing-groups-regex", // // ES2017 // "babel-plugin-transform-async-to-generator", - // // ES2016 - // "babel-plugin-transform-exponentiation-operator", + // ES2016 + "babel-plugin-transform-exponentiation-operator", // // ES2015 "babel-plugin-transform-arrow-functions", // "babel-plugin-transform-function-name",