feat(transformer): support logical-assignment-operators plugin (#4890)

part of #4754

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:36 +00:00
parent ab1d08ccfb
commit 0d7912217a
8 changed files with 373 additions and 7 deletions

View file

@ -0,0 +1,271 @@
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, LogicalOperator};
use oxc_traverse::TraverseCtx;
use crate::context::Ctx;
/// ES2021: Logical Assignment Operators
///
/// References:
/// * <https://babel.dev/docs/babel-plugin-transform-logical-assignment-operators>
/// * <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-logical-assignment-operators>
pub struct LogicalAssignmentOperators<'a> {
_ctx: Ctx<'a>,
var_declarations: std::vec::Vec<Vec<'a, VariableDeclarator<'a>>>,
}
impl<'a> LogicalAssignmentOperators<'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)
}
pub fn maybe_generate_memoised(
&mut self,
expr: &Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<IdentifierReference<'a>> {
let name = match expr {
Expression::Super(_) | Expression::ThisExpression(_) => return None,
Expression::Identifier(ident) => ident.name.clone(),
Expression::StringLiteral(str) => str.value.clone(),
_ => {
return None;
}
};
let symbol_id =
ctx.generate_uid_in_current_scope(name.as_str(), 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));
// _name = name
Some(ctx.create_reference_id(SPAN, symbol_name, Some(symbol_id), ReferenceFlag::Write))
}
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>) {
let Expression::AssignmentExpression(assignment_expr) = expr else { return };
// `&&=` `||=` `??=`
let operator = match assignment_expr.operator {
AssignmentOperator::LogicalAnd => LogicalOperator::And,
AssignmentOperator::LogicalOr => LogicalOperator::Or,
AssignmentOperator::LogicalNullish => LogicalOperator::Coalesce,
_ => return,
};
// `a &&= c` -> `a && (a = c);`
// ^ ^ assign_target
// ^ left_expr
let left_expr: Expression<'a>;
let assign_target: AssignmentTarget;
// TODO: refactor this block, add tests, cover private identifier
match &mut assignment_expr.left {
AssignmentTarget::AssignmentTargetIdentifier(ident) => {
left_expr = ctx.ast.expression_from_identifier_reference(
Self::clone_identifier_reference(ident, ctx),
);
assign_target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_from_identifier_reference(
Self::clone_identifier_reference(ident, ctx),
),
);
}
left @ match_member_expression!(AssignmentTarget) => {
let member_expr = left.to_member_expression_mut();
let op = AssignmentOperator::Assign;
// `a.b &&= c` -> `var _a; (_a = a).b && (_a.b = c)`
match member_expr {
MemberExpression::StaticMemberExpression(static_expr) => {
if let Some(ident) = self.maybe_generate_memoised(&static_expr.object, ctx)
{
// (_o = o).a
let right = ctx.ast.move_expression(&mut static_expr.object);
let target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_from_identifier_reference(
Self::clone_identifier_reference(&ident, ctx),
),
);
let object = ctx.ast.expression_assignment(SPAN, op, target, right);
left_expr = Expression::from(ctx.ast.member_expression_static(
SPAN,
object,
static_expr.property.clone_in(ctx.ast.allocator),
false,
));
// (_o.a = 1)
let assign_expr = ctx.ast.member_expression_static(
SPAN,
ctx.ast.expression_from_identifier_reference(ident),
static_expr.property.clone_in(ctx.ast.allocator),
false,
);
assign_target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_member_expression(assign_expr),
);
} else {
left_expr = Expression::from(MemberExpression::StaticMemberExpression(
static_expr.clone_in(ctx.ast.allocator),
));
assign_target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_member_expression(
member_expr.clone_in(ctx.ast.allocator),
),
);
};
}
// `a[b.y] &&= c;` ->
// `var _a, _b$y; (_a = a)[_b$y = b.y] && (_a[_b$y] = c);`
MemberExpression::ComputedMemberExpression(computed_expr) => {
if let Some(ident) =
self.maybe_generate_memoised(&computed_expr.object, ctx)
{
// (_o = object)
let right = ctx.ast.move_expression(&mut computed_expr.object);
let target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_from_identifier_reference(
Self::clone_identifier_reference(&ident, ctx),
),
);
let object = ctx.ast.expression_assignment(SPAN, op, target, right);
let mut expression =
ctx.ast.move_expression(&mut computed_expr.expression);
// _b = expression
let property = self.maybe_generate_memoised(&expression, ctx);
if let Some(ref property) = property {
let left = AssignmentTarget::from(
ctx.ast.simple_assignment_target_from_identifier_reference(
Self::clone_identifier_reference(property, ctx),
),
);
expression =
ctx.ast.expression_assignment(SPAN, op, left, expression);
}
// _o[_b]
assign_target =
AssignmentTarget::from(ctx.ast.member_expression_computed(
SPAN,
ctx.ast.expression_from_identifier_reference(
Self::clone_identifier_reference(&ident, ctx),
),
property.map_or_else(
|| expression.clone_in(ctx.ast.allocator),
|ident| ctx.ast.expression_from_identifier_reference(ident),
),
false,
));
left_expr = Expression::from(
ctx.ast.member_expression_computed(SPAN, object, expression, false),
);
} else {
let property_ident =
self.maybe_generate_memoised(&computed_expr.expression, ctx);
let mut expr = computed_expr.clone_in(ctx.ast.allocator);
if let Some(property_ident) = &property_ident {
let left = AssignmentTarget::from(
ctx.ast.simple_assignment_target_from_identifier_reference(
property_ident.clone(),
),
);
let right = computed_expr.expression.clone_in(ctx.ast.allocator);
expr.expression =
ctx.ast.expression_assignment(SPAN, op, left, right);
}
left_expr =
Expression::from(MemberExpression::ComputedMemberExpression(expr));
let mut expr = computed_expr.clone_in(ctx.ast.allocator);
if let Some(property_ident) = property_ident {
expr.expression =
ctx.ast.expression_from_identifier_reference(property_ident);
}
assign_target = AssignmentTarget::from(
ctx.ast.simple_assignment_target_member_expression(
MemberExpression::ComputedMemberExpression(expr),
),
);
};
}
MemberExpression::PrivateFieldExpression(_) => return,
}
}
// All other are TypeScript syntax.
// It is a Syntax Error if AssignmentTargetType of LeftHandSideExpression is not simple.
// So safe to return here.
_ => return,
};
let assign_op = AssignmentOperator::Assign;
let right = ctx.ast.move_expression(&mut assignment_expr.right);
let right = ctx.ast.expression_assignment(SPAN, assign_op, assign_target, right);
let logical_expr = ctx.ast.expression_logical(SPAN, left_expr, operator, right);
*expr = logical_expr;
}
}

View file

@ -0,0 +1,56 @@
mod logical_assignment_operators;
mod options;
pub use logical_assignment_operators::LogicalAssignmentOperators;
pub use options::ES2021Options;
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 ES2021<'a> {
ctx: Ctx<'a>,
options: ES2021Options,
// Plugins
logical_assignment_operators: LogicalAssignmentOperators<'a>,
}
impl<'a> ES2021<'a> {
pub fn new(options: ES2021Options, ctx: Ctx<'a>) -> Self {
Self {
logical_assignment_operators: LogicalAssignmentOperators::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.logical_assignment_operators {
self.logical_assignment_operators.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.logical_assignment_operators {
self.logical_assignment_operators.transform_statements_on_exit(statements, ctx);
}
}
pub fn transform_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if self.options.logical_assignment_operators {
self.logical_assignment_operators.transform_expression(expr, ctx);
}
}
}

View file

@ -0,0 +1,16 @@
use serde::Deserialize;
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct ES2021Options {
#[serde(skip)]
pub logical_assignment_operators: bool,
}
impl ES2021Options {
#[must_use]
pub fn with_logical_assignment_operators(mut self, enable: bool) -> Self {
self.logical_assignment_operators = enable;
self
}
}

View file

@ -18,6 +18,7 @@ mod es2015;
mod es2016;
mod es2019;
mod es2020;
mod es2021;
mod react;
mod typescript;
@ -31,6 +32,7 @@ use std::{path::Path, rc::Rc};
use es2016::ES2016;
use es2019::ES2019;
use es2020::ES2020;
use es2021::ES2021;
use oxc_allocator::{Allocator, Vec};
use oxc_ast::{ast::*, AstBuilder, Trivias};
use oxc_diagnostics::OxcDiagnostic;
@ -64,6 +66,7 @@ pub struct Transformer<'a> {
// NOTE: all callbacks must run in order.
x0_typescript: TypeScript<'a>,
x1_react: React<'a>,
x2_es2021: ES2021<'a>,
x2_es2020: ES2020<'a>,
x2_es2019: ES2019<'a>,
x2_es2016: ES2016<'a>,
@ -91,6 +94,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_es2021: ES2021::new(options.es2021, Rc::clone(&ctx)),
x2_es2020: ES2020::new(options.es2020, Rc::clone(&ctx)),
x2_es2019: ES2019::new(options.es2019, Rc::clone(&ctx)),
x2_es2016: ES2016::new(options.es2016, Rc::clone(&ctx)),
@ -172,6 +176,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_es2021.transform_expression(expr, ctx);
self.x2_es2020.transform_expression(expr, ctx);
self.x2_es2016.transform_expression(expr, ctx);
self.x3_es2015.transform_expression(expr);
@ -260,6 +265,7 @@ 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_es2021.transform_statements(stmts, ctx);
self.x2_es2020.transform_statements(stmts, ctx);
self.x2_es2016.transform_statements(stmts, ctx);
self.x3_es2015.enter_statements(stmts);
@ -267,6 +273,7 @@ impl<'a> Traverse<'a> for Transformer<'a> {
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_es2021.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

@ -10,6 +10,7 @@ use crate::{
es2016::ES2016Options,
es2019::ES2019Options,
es2020::ES2020Options,
es2021::ES2021Options,
options::babel::BabelOptions,
react::ReactOptions,
typescript::TypeScriptOptions,
@ -43,6 +44,8 @@ pub struct TransformOptions {
pub es2019: ES2019Options,
pub es2020: ES2020Options,
pub es2021: ES2021Options,
}
impl TransformOptions {
@ -128,6 +131,11 @@ impl TransformOptions {
enable_plugin(plugin_name, options, &env_options, &targets).is_some()
});
let es2021 = ES2021Options::default().with_logical_assignment_operators({
let plugin_name = "transform-logical-assignment-operators";
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))
@ -162,6 +170,7 @@ impl TransformOptions {
es2016,
es2019,
es2020,
es2021,
})
}
}

View file

@ -1,6 +1,6 @@
commit: 12619ffe
Passed: 462/947
Passed: 466/953
# All Passed:
* babel-plugin-transform-optional-catch-binding
@ -434,6 +434,10 @@ Passed: 462/947
* shipped-proposals/new-class-features-chrome-94/input.js
* shipped-proposals/new-class-features-firefox-70/input.js
# babel-plugin-transform-logical-assignment-operators (4/6)
* logical-assignment/general-semantics/input.js
* logical-assignment/null-coalescing/input.js
# babel-plugin-transform-nullish-coalescing-operator (2/12)
* assumption-noDocumentAll/transform/input.js
* assumption-noDocumentAll/transform-in-default-destructuring/input.js

View file

@ -1,6 +1,6 @@
commit: 12619ffe
Passed: 13/19
Passed: 16/23
# All Passed:
* babel-plugin-transform-nullish-coalescing-operator
@ -15,6 +15,9 @@ Passed: 13/19
* sanity/check-es2015-constants/exec.js
* sanity/regex-dot-all/exec.js
# babel-plugin-transform-logical-assignment-operators (3/4)
* logical-assignment/null-coalescing/exec.js
# babel-plugin-transform-react-jsx-source (0/2)
* react-source/basic-sample/exec.js
* react-source/with-source/exec.js

View file

@ -8,10 +8,10 @@ pub(crate) const PLUGINS: &[&str] = &[
// "babel-plugin-transform-private-methods",
// "babel-plugin-transform-private-property-in-object",
// // [Syntax] "babel-plugin-transform-syntax-top-level-await",
// // ES2021
// "babel-plugin-transform-logical-assignment-operators",
// ES2021
"babel-plugin-transform-logical-assignment-operators",
// "babel-plugin-transform-numeric-separator",
// // ES2020
// ES2020
// "babel-plugin-transform-export-namespace-from",
// "babel-plugin-transform-dynamic-import",
"babel-plugin-transform-nullish-coalescing-operator",
@ -19,7 +19,7 @@ pub(crate) const PLUGINS: &[&str] = &[
// // [Syntax] "babel-plugin-transform-syntax-bigint",
// // [Syntax] "babel-plugin-transform-syntax-dynamic-import",
// // [Syntax] "babel-plugin-transform-syntax-import-meta",
// // ES2019
// ES2019
"babel-plugin-transform-optional-catch-binding",
// "babel-plugin-transform-json-strings",
// // ES2018
@ -32,7 +32,7 @@ pub(crate) const PLUGINS: &[&str] = &[
// "babel-plugin-transform-async-to-generator",
// ES2016
"babel-plugin-transform-exponentiation-operator",
// // ES2015
// ES2015
"babel-plugin-transform-arrow-functions",
// "babel-plugin-transform-function-name",
// "babel-plugin-transform-shorthand-properties",