feat(transformer): support nullish-coalescing-operator plugin (#4884)

The implementation copy from the original implementation which removed in https://github.com/oxc-project/oxc/pull/2865.
This commit is contained in:
Dunqing 2024-08-15 10:10:33 +00:00
parent 3a66e5843d
commit 69da9fda3a
8 changed files with 250 additions and 3 deletions

View file

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

View file

@ -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:
/// * <https://babeljs.io/docs/babel-plugin-transform-nullish-coalescing-operator>
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-nullish-coalescing-operator>
pub struct NullishCoalescingOperator<'a> {
_ctx: Ctx<'a>,
var_declarations: std::vec::Vec<Vec<'a, VariableDeclarator<'a>>>,
}
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::<TSTypeAnnotation<'_>>, 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);
}
}

View file

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

View file

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

View file

@ -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::<TypeScriptOptions>(get_plugin_options(plugin_name, options))
@ -144,6 +152,7 @@ impl TransformOptions {
react,
es2015,
es2016,
es2020,
})
}
}

View file

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

View file

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

View file

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