Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transformer): ES2020 Nullish Coalescing Operator #1004

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/oxc_ast/src/ast_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ impl<'a> AstBuilder<'a> {
Program { span, source_type, directives, hashbang, body }
}

/* ---------- Constructors ---------- */

/// `void 0`
pub fn void_0(&self) -> Expression<'a> {
let left = self.number_literal(Span::default(), 0.0, "0", NumberBase::Decimal);
let num = self.literal_number_expression(left);
self.unary_expression(Span::default(), UnaryOperator::Void, num)
}

/* ---------- Literals ---------- */

pub fn number_literal(
Expand Down
13 changes: 8 additions & 5 deletions crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use oxc_allocator::{Box, Vec};
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;
use oxc_syntax::{
operator::{BinaryOperator, LogicalOperator, UnaryOperator},
operator::{BinaryOperator, UnaryOperator},
precedence::{GetPrecedence, Precedence},
NumberBase,
};
Expand Down Expand Up @@ -1347,6 +1347,7 @@ impl<'a, const MINIFY: bool> GenExpr<MINIFY> for BinaryExpression<'a> {
p.print_space_before_identifier();
}
self.operator.gen(p, ctx);
p.print_soft_space();
self.right.gen_expr(p, self.precedence(), ctx.union_in_if(wrap));
});
}
Expand Down Expand Up @@ -1385,11 +1386,9 @@ impl<'a, const MINIFY: bool> GenExpr<MINIFY> for LogicalExpression<'a> {
);
p.wrap(mixed || (precedence > self.precedence()), |p| {
self.left.gen_expr(p, self.precedence(), ctx);
p.print_soft_space();
p.print_str(self.operator.as_str().as_bytes());
let _precedence = match self.operator {
LogicalOperator::And | LogicalOperator::Coalesce => Precedence::BitwiseOr,
LogicalOperator::Or => Precedence::LogicalAnd,
};
p.print_soft_space();
self.right.gen_expr(p, self.precedence(), ctx);
});
}
Expand All @@ -1400,9 +1399,13 @@ impl<'a, const MINIFY: bool> GenExpr<MINIFY> for ConditionalExpression<'a> {
let wrap = precedence > self.precedence();
p.wrap(wrap, |p| {
self.test.gen_expr(p, self.precedence(), ctx);
p.print_soft_space();
p.print(b'?');
p.print_soft_space();
self.consequent.gen_expr(p, Precedence::Assign, ctx.and_in(true));
p.print_soft_space();
p.print(b':');
p.print_soft_space();
self.alternate.gen_expr(p, Precedence::Assign, ctx.union_in_if(wrap));
});
}
Expand Down
17 changes: 5 additions & 12 deletions crates/oxc_minifier/src/compressor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ impl<'a> Compressor<'a> {

/* Utilities */

/// `void 0`
fn create_void_0(&mut self) -> Expression<'a> {
let left = self.ast.number_literal(SPAN, 0.0, "0", NumberBase::Decimal);
let num = self.ast.literal_number_expression(left);
self.ast.unary_expression(SPAN, UnaryOperator::Void, num)
}

/// `1/0`
#[allow(unused)]
fn create_one_div_zero(&mut self) -> Expression<'a> {
Expand Down Expand Up @@ -88,7 +81,7 @@ impl<'a> Compressor<'a> {

fn compress_console(&mut self, expr: &mut Expression<'a>) -> bool {
if self.options.drop_console && util::is_console(expr) {
*expr = self.create_void_0();
*expr = self.ast.void_0();
true
} else {
false
Expand Down Expand Up @@ -163,12 +156,12 @@ impl<'a> Compressor<'a> {
/* Expressions */

/// Transforms `undefined` => `void 0`
fn compress_undefined(&mut self, expr: &mut Expression<'a>) -> bool {
fn compress_undefined(&self, expr: &mut Expression<'a>) -> bool {
let Expression::Identifier(ident) = expr else { return false };
if ident.name == "undefined" {
// if let Some(reference_id) = ident.reference_id.get() {
// && self.semantic.symbols().is_global_reference(reference_id)
*expr = self.create_void_0();
*expr = self.ast.void_0();
return true;
// }
}
Expand Down Expand Up @@ -209,14 +202,14 @@ impl<'a> Compressor<'a> {

/// Transforms `typeof foo == "undefined"` into `foo === void 0`
/// Enabled by `compress.typeofs`
fn compress_typeof_undefined(&mut self, expr: &mut BinaryExpression<'a>) {
fn compress_typeof_undefined(&self, expr: &mut BinaryExpression<'a>) {
if expr.operator.is_equality() && self.options.typeofs {
if let Expression::UnaryExpression(unary_expr) = &expr.left {
if unary_expr.operator == UnaryOperator::Typeof {
if let Expression::Identifier(ident) = &unary_expr.argument {
if expr.right.is_specific_string_literal("undefined") {
let left = self.ast.identifier_reference_expression((*ident).clone());
let right = self.create_void_0();
let right = self.ast.void_0();
let operator = BinaryOperator::StrictEquality;
*expr = BinaryExpression { span: SPAN, left, operator, right };
}
Expand Down
23 changes: 23 additions & 0 deletions crates/oxc_semantic/src/symbol.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use oxc_ast::ast::Expression;
use oxc_index::IndexVec;
use oxc_span::{Atom, Span};
pub use oxc_syntax::{
Expand Down Expand Up @@ -112,4 +113,26 @@ impl SymbolTable {
.iter()
.map(|reference_id| &self.references[*reference_id])
}

/// Determine whether evaluating the specific input `node` is a consequenceless reference. ie.
/// evaluating it won't result in potentially arbitrary code from being ran. The following are
/// allowed and determined not to cause side effects:
///
/// - `this` expressions
/// - `super` expressions
/// - Bound identifiers
///
/// Reference:
/// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L557>
pub fn is_static(&self, expr: &Expression) -> bool {
match expr {
Expression::ThisExpression(_) | Expression::Super(_) => true,
Expression::Identifier(ident)
if ident.reference_id.get().is_some_and(|id| self.has_binding(id)) =>
{
true
}
_ => false,
}
}
}
5 changes: 4 additions & 1 deletion crates/oxc_transformer/examples/transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{TransformOptions, TransformReactOptions, TransformTarget, Transformer};
use oxc_transformer::{
Assumptions, TransformOptions, TransformReactOptions, TransformTarget, Transformer,
};

// Instruction:
// create a `test.js`,
Expand Down Expand Up @@ -41,6 +43,7 @@ fn main() {
let transform_options = TransformOptions {
target: TransformTarget::ES2015,
react: Some(TransformReactOptions::default()),
assumptions: Assumptions::default(),
};
Transformer::new(&allocator, source_type, &symbols, transform_options).build(program);
let printed = Codegen::<false>::new(source_text.len(), codegen_options).build(program);
Expand Down
54 changes: 14 additions & 40 deletions crates/oxc_transformer/src/es2016/exponentiation_operator.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::{cell::RefCell, rc::Rc};

use oxc_allocator::Vec;
use oxc_ast::{ast::*, AstBuilder};
use oxc_semantic::SymbolTable;
use oxc_span::{Atom, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator};

use std::{cell::RefCell, mem, rc::Rc};
use crate::utils::CreateVars;

/// ES2016: Exponentiation Operator
///
Expand All @@ -23,23 +25,22 @@ struct Exploded<'a> {
uid: Expression<'a>,
}

impl<'a> CreateVars<'a> for ExponentiationOperator<'a> {
fn ast(&self) -> &AstBuilder<'a> {
&self.ast
}

fn vars_mut(&mut self) -> &mut Vec<'a, VariableDeclarator<'a>> {
&mut self.vars
}
}

impl<'a> ExponentiationOperator<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, symbols: Rc<RefCell<SymbolTable>>) -> Self {
let vars = ast.new_vec();
Self { ast, symbols, vars }
}

pub fn leave_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
if self.vars.is_empty() {
return;
}
let decls = mem::replace(&mut self.vars, self.ast.new_vec());
let kind = VariableDeclarationKind::Var;
let decl = self.ast.variable_declaration(Span::default(), kind, decls, Modifiers::empty());
let stmt = Statement::Declaration(Declaration::VariableDeclaration(decl));
stmts.insert(0, stmt);
}

pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
// left ** right
if let Expression::BinaryExpression(binary_expr) = expr {
Expand Down Expand Up @@ -218,17 +219,7 @@ impl<'a> ExponentiationOperator<'a> {
expr: Expression<'a>,
nodes: &mut Vec<'a, Expression<'a>>,
) -> Expression<'a> {
let name = generate_uid_identifier_based_on_node(&expr);
// TODO: scope.push({ id: temp });

// Add `var name` to scope
let binding_identifier = BindingIdentifier::new(Span::default(), name.clone());
let binding_pattern_kind = self.ast.binding_pattern_identifier(binding_identifier);
let binding = self.ast.binding_pattern(binding_pattern_kind, None, false);
let kind = VariableDeclarationKind::Var;
let decl = self.ast.variable_declarator(Span::default(), kind, binding, None, false);
self.vars.push(decl);

let name = self.create_new_var(&expr);
// Add new reference `_name = name` to nodes
let ident = IdentifierReference::new(Span::default(), name);
let target = self.ast.simple_assignment_target_identifier(ident.clone());
Expand All @@ -239,23 +230,6 @@ impl<'a> ExponentiationOperator<'a> {
}
}

// TODO:
// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L543>
fn generate_uid_identifier_based_on_node(expr: &Expression) -> Atom {
let mut parts = std::vec::Vec::with_capacity(1);
gather_node_parts(expr, &mut parts);
let name = parts.join("$");
Atom::from(format!("_{name}"))
}

// TODO: use a trait and add this to oxc_ast (syntax directed operations)
fn gather_node_parts(expr: &Expression, parts: &mut std::vec::Vec<Atom>) {
match expr {
Expression::Identifier(ident) => parts.push(ident.name.clone()),
_ => parts.push(Atom::from("ref")),
}
}

#[test]
fn test() {
use crate::{
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_transformer/src/es2020/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod nullish_coalescing_operator;

pub use nullish_coalescing_operator::NullishCoalescingOperator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use std::{cell::RefCell, rc::Rc};

use oxc_allocator::Vec;
use oxc_ast::{ast::*, AstBuilder};
use oxc_semantic::SymbolTable;
use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};

use crate::{options::Assumptions, utils::CreateVars};

/// 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> {
ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions,
vars: Vec<'a, VariableDeclarator<'a>>,
}

impl<'a> CreateVars<'a> for NullishCoalescingOperator<'a> {
fn ast(&self) -> &AstBuilder<'a> {
&self.ast
}

fn vars_mut(&mut self) -> &mut Vec<'a, VariableDeclarator<'a>> {
&mut self.vars
}
}

impl<'a> NullishCoalescingOperator<'a> {
pub fn new(
ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions,
) -> Self {
let vars = ast.new_vec();
Self { ast, symbols, assumptions, vars }
}

pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
// left ?? right
let Expression::LogicalExpression(logical_expr) = expr else { return };
if logical_expr.operator != LogicalOperator::Coalesce {
return;
}

let span = Span::default();
let reference;
let assignment;

// skip creating extra reference when `left` is static
if self.symbols.borrow().is_static(&logical_expr.left) {
reference = self.ast.copy(&logical_expr.left);
assignment = self.ast.copy(&logical_expr.left);
} else {
let name = self.create_new_var(&logical_expr.left);
let ident = IdentifierReference::new(span, name);
reference = self.ast.identifier_reference_expression(ident.clone());
let left = AssignmentTarget::SimpleAssignmentTarget(
self.ast.simple_assignment_target_identifier(ident),
);
let right = self.ast.copy(&logical_expr.left);
assignment =
self.ast.assignment_expression(span, AssignmentOperator::Assign, left, right);
};

let test = if self.assumptions.no_document_all {
let null = self.ast.literal_null_expression(NullLiteral::new(span));
self.ast.binary_expression(span, assignment, BinaryOperator::Inequality, null)
} else {
let op = BinaryOperator::StrictInequality;
let null = self.ast.literal_null_expression(NullLiteral::new(span));
let left = self.ast.binary_expression(span, self.ast.copy(&assignment), op, null);

let right =
self.ast.binary_expression(span, self.ast.copy(&reference), op, self.ast.void_0());

self.ast.logical_expression(span, left, LogicalOperator::And, right)
};

let right = self.ast.move_expression(&mut logical_expr.right);

*expr = self.ast.conditional_expression(span, test, reference, right);
}
}
Loading
Loading