diff --git a/crates/oxc_transformer/src/es2020/mod.rs b/crates/oxc_transformer/src/es2020/mod.rs new file mode 100644 index 000000000..bcf94cfd6 --- /dev/null +++ b/crates/oxc_transformer/src/es2020/mod.rs @@ -0,0 +1,56 @@ +mod nullish_coalescing_operator; +mod options; + +pub use nullish_coalescing_operator::NullishCoalescingOperator; +pub use options::ES2020Options; +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 ES2020<'a> { + ctx: Ctx<'a>, + options: ES2020Options, + + // Plugins + nullish_coalescing_operator: NullishCoalescingOperator<'a>, +} + +impl<'a> ES2020<'a> { + pub fn new(options: ES2020Options, ctx: Ctx<'a>) -> Self { + Self { + nullish_coalescing_operator: NullishCoalescingOperator::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.nullish_coalescing_operator { + self.nullish_coalescing_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.nullish_coalescing_operator { + self.nullish_coalescing_operator.transform_statements_on_exit(statements, ctx); + } + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.nullish_coalescing_operator { + self.nullish_coalescing_operator.transform_expression(expr, ctx); + } + } +} diff --git a/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs b/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs new file mode 100644 index 000000000..f97372836 --- /dev/null +++ b/crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs @@ -0,0 +1,146 @@ +use std::cell::Cell; + +use oxc_semantic::{ReferenceFlag, SymbolFlags}; +use oxc_traverse::TraverseCtx; + +use oxc_allocator::{CloneIn, Vec}; +use oxc_ast::ast::*; +use oxc_span::SPAN; +use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator}; + +use crate::context::Ctx; + +/// ES2020: Nullish Coalescing Operator +/// +/// References: +/// * +/// * +pub struct NullishCoalescingOperator<'a> { + _ctx: Ctx<'a>, + var_declarations: std::vec::Vec>>, +} + +impl<'a> NullishCoalescingOperator<'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)); + } + } + + fn create_new_var_with_expression( + &mut self, + expr: &Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> IdentifierReference<'a> { + // Add `var name` to scope + let name = match expr { + Expression::Identifier(ident) => ident.name.as_str(), + // TODO: needs to port generateUidIdentifierBasedOnNode + // https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L543-L545 + _ => "nullish_coalescing_operator", + }; + 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)); + }; + + ctx.create_reference_id(SPAN, symbol_name, Some(symbol_id), ReferenceFlag::Read) + } + + pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + // left ?? right + if !matches!(expr, Expression::LogicalExpression(logical_expr) if logical_expr.operator == LogicalOperator::Coalesce) + { + return; + } + + // Take ownership of the `LogicalExpression` + let logical_expr = match ctx.ast.move_expression(expr) { + Expression::LogicalExpression(logical_expr) => logical_expr.unbox(), + _ => unreachable!(), + }; + + // skip creating extra reference when `left` is static + let (reference, assignment) = if ctx.symbols().is_static(&logical_expr.left) { + (Self::clone_expression(&logical_expr.left, ctx), logical_expr.left) + } else { + let ident = self.create_new_var_with_expression(&logical_expr.left, ctx); + let left = + AssignmentTarget::from(ctx.ast.simple_assignment_target_from_identifier_reference( + Self::clone_identifier_reference(&ident, ctx), + )); + let right = logical_expr.left; + ( + ctx.ast.expression_from_identifier_reference(ident), + ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right), + ) + }; + + let op = BinaryOperator::StrictInequality; + let null = ctx.ast.expression_null_literal(SPAN); + let left = ctx.ast.expression_binary(SPAN, ctx.ast.copy(&assignment), op, null); + let right = ctx.ast.expression_binary(SPAN, ctx.ast.copy(&reference), op, ctx.ast.void_0()); + let test = ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, right); + + *expr = ctx.ast.expression_conditional(SPAN, test, reference, logical_expr.right); + } +} diff --git a/crates/oxc_transformer/src/es2020/options.rs b/crates/oxc_transformer/src/es2020/options.rs new file mode 100644 index 000000000..1101637e6 --- /dev/null +++ b/crates/oxc_transformer/src/es2020/options.rs @@ -0,0 +1,16 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct ES2020Options { + #[serde(skip)] + pub nullish_coalescing_operator: bool, +} + +impl ES2020Options { + #[must_use] + pub fn with_nullish_coalescing_operator(mut self, enable: bool) -> Self { + self.nullish_coalescing_operator = enable; + self + } +} diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 59bb65142..c0f9251fe 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -16,6 +16,7 @@ mod options; mod env; mod es2015; mod es2016; +mod es2020; mod react; mod typescript; @@ -27,6 +28,7 @@ mod helpers { use std::{path::Path, rc::Rc}; use es2016::ES2016; +use es2020::ES2020; use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ast::*, AstBuilder, Trivias}; use oxc_diagnostics::OxcDiagnostic; @@ -60,6 +62,7 @@ pub struct Transformer<'a> { // NOTE: all callbacks must run in order. x0_typescript: TypeScript<'a>, x1_react: React<'a>, + x2_es2020: ES2020<'a>, x2_es2016: ES2016<'a>, x3_es2015: ES2015<'a>, } @@ -86,6 +89,7 @@ impl<'a> Transformer<'a> { 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)), + x2_es2020: ES2020::new(options.es2020, Rc::clone(&ctx)), x3_es2015: ES2015::new(options.es2015, ctx), } } @@ -164,6 +168,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_es2020.transform_expression(expr, ctx); self.x2_es2016.transform_expression(expr, ctx); self.x3_es2015.transform_expression(expr); } @@ -251,12 +256,14 @@ impl<'a> Traverse<'a> for Transformer<'a> { fn enter_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.x0_typescript.transform_statements(stmts); + self.x2_es2020.transform_statements(stmts, ctx); 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_es2020.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 445ada7d0..88cd6a331 100644 --- a/crates/oxc_transformer/src/options/transformer.rs +++ b/crates/oxc_transformer/src/options/transformer.rs @@ -8,6 +8,7 @@ use crate::{ env::{can_enable_plugin, EnvOptions, Versions}, es2015::{ArrowFunctionsOptions, ES2015Options}, es2016::ES2016Options, + es2020::ES2020Options, options::babel::BabelOptions, react::ReactOptions, typescript::TypeScriptOptions, @@ -37,6 +38,8 @@ pub struct TransformOptions { pub es2015: ES2015Options, pub es2016: ES2016Options, + + pub es2020: ES2020Options, } impl TransformOptions { @@ -112,6 +115,11 @@ impl TransformOptions { enable_plugin(plugin_name, options, &env_options, &targets).is_some() }); + let es2020 = ES2020Options::default().with_nullish_coalescing_operator({ + let plugin_name = "transform-nullish-coalescing-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)) @@ -144,6 +152,7 @@ impl TransformOptions { react, es2015, es2016, + es2020, }) } } diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index eb67b4b0a..f1c2a0f7e 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,6 +1,6 @@ commit: 12619ffe -Passed: 456/931 +Passed: 458/943 # All Passed: * babel-preset-react @@ -433,6 +433,18 @@ Passed: 456/931 * shipped-proposals/new-class-features-chrome-94/input.js * shipped-proposals/new-class-features-firefox-70/input.js +# babel-plugin-transform-nullish-coalescing-operator (2/12) +* assumption-noDocumentAll/transform/input.js +* assumption-noDocumentAll/transform-in-default-destructuring/input.js +* assumption-noDocumentAll/transform-in-default-param/input.js +* assumption-noDocumentAll/transform-in-function/input.js +* assumption-noDocumentAll/transform-static-refs-in-default/input.js +* assumption-noDocumentAll/transform-static-refs-in-function/input.js +* nullish-coalescing/transform-in-default-destructuring/input.js +* nullish-coalescing/transform-in-default-param/input.js +* nullish-coalescing/transform-in-function/input.js +* nullish-coalescing/transform-loose/input.js + # babel-plugin-transform-exponentiation-operator (3/4) * regression/4349/input.js diff --git a/tasks/transform_conformance/babel_exec.snap.md b/tasks/transform_conformance/babel_exec.snap.md index 8e328b349..1ee4d2cf1 100644 --- a/tasks/transform_conformance/babel_exec.snap.md +++ b/tasks/transform_conformance/babel_exec.snap.md @@ -1,8 +1,9 @@ commit: 12619ffe -Passed: 10/16 +Passed: 12/18 # All Passed: +* babel-plugin-transform-nullish-coalescing-operator * 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 f713a321b..1fe595215 100644 --- a/tasks/transform_conformance/src/constants.rs +++ b/tasks/transform_conformance/src/constants.rs @@ -14,7 +14,7 @@ pub(crate) const PLUGINS: &[&str] = &[ // // ES2020 // "babel-plugin-transform-export-namespace-from", // "babel-plugin-transform-dynamic-import", - // "babel-plugin-transform-nullish-coalescing-operator", + "babel-plugin-transform-nullish-coalescing-operator", // "babel-plugin-transform-optional-chaining", // // [Syntax] "babel-plugin-transform-syntax-bigint", // // [Syntax] "babel-plugin-transform-syntax-dynamic-import",