Skip to content

Commit

Permalink
feat(transformer): ES2020 Nullish Coalescing Operator
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Oct 16, 2023
1 parent 240260e commit 010c022
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 77 deletions.
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;
88 changes: 88 additions & 0 deletions crates/oxc_transformer/src/es2020/nullish_coalescing_operator.rs
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

0 comments on commit 010c022

Please sign in to comment.