refactor(minifier): break up methods into different files (#8843)

This commit is contained in:
Boshen 2025-02-02 15:06:16 +00:00
parent baf3e4e6fb
commit 9c84c6de8e
7 changed files with 1170 additions and 1110 deletions

View file

@ -14,7 +14,7 @@ use crate::ctx::Ctx;
use super::PeepholeOptimizations;
impl<'a, 'b> PeepholeOptimizations {
impl<'a> PeepholeOptimizations {
/// Constant Folding
///
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeFoldConstants.java>
@ -37,7 +37,7 @@ impl<'a, 'b> PeepholeOptimizations {
}
#[expect(clippy::float_cmp)]
fn try_fold_unary_expr(e: &UnaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
fn try_fold_unary_expr(e: &UnaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option<Expression<'a>> {
match e.operator {
// Do not fold `void 0` back to `undefined`.
UnaryOperator::Void if e.argument.is_number_0() => None,
@ -53,7 +53,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_static_member_expr(
e: &mut StaticMemberExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
// TODO: tryFoldObjectPropAccess(n, left, name)
ctx.eval_static_member_expression(e).map(|value| ctx.value_to_expr(e.span, value))
@ -61,7 +61,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_computed_member_expr(
e: &mut ComputedMemberExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
// TODO: tryFoldObjectPropAccess(n, left, name)
ctx.eval_computed_member_expression(e).map(|value| ctx.value_to_expr(e.span, value))
@ -69,7 +69,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_logical_expr(
logical_expr: &mut LogicalExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
match logical_expr.operator {
LogicalOperator::And | LogicalOperator::Or => Self::try_fold_and_or(logical_expr, ctx),
@ -79,7 +79,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_optional_chain(
chain_expr: &mut ChainExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let member_expr = chain_expr.expression.as_member_expression()?;
if !member_expr.optional() {
@ -96,7 +96,7 @@ impl<'a, 'b> PeepholeOptimizations {
/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/09094b551915a6487a980a783831cba58b5739d1/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L587)
pub fn try_fold_and_or(
logical_expr: &mut LogicalExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let op = logical_expr.operator;
debug_assert!(matches!(op, LogicalOperator::And | LogicalOperator::Or));
@ -171,7 +171,7 @@ impl<'a, 'b> PeepholeOptimizations {
/// Try to fold a nullish coalesce `foo ?? bar`.
pub fn try_fold_coalesce(
logical_expr: &mut LogicalExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce);
let left = &logical_expr.left;
@ -214,7 +214,7 @@ impl<'a, 'b> PeepholeOptimizations {
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn try_fold_binary_expr(
e: &mut BinaryExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
// TODO: tryReduceOperandsForOp
@ -311,7 +311,7 @@ impl<'a, 'b> PeepholeOptimizations {
}
// Simplified version of `tryFoldAdd` from closure compiler.
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option<Expression<'a>> {
if let Some(v) = ctx.eval_binary_expression(e) {
return Some(ctx.value_to_expr(e.span, v));
}
@ -377,7 +377,7 @@ impl<'a, 'b> PeepholeOptimizations {
))
}
fn try_fold_comparison(e: &BinaryExpression<'a>, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
fn try_fold_comparison(e: &BinaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option<Expression<'a>> {
let left = &e.left;
let right = &e.right;
let op = e.operator;
@ -414,9 +414,9 @@ impl<'a, 'b> PeepholeOptimizations {
/// <https://tc39.es/ecma262/#sec-abstract-equality-comparison>
fn try_abstract_equality_comparison(
left_expr: &'b Expression<'a>,
right_expr: &'b Expression<'a>,
ctx: Ctx<'a, 'b>,
left_expr: &Expression<'a>,
right_expr: &Expression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<bool> {
let left = ValueType::from(left_expr);
let right = ValueType::from(right_expr);
@ -506,9 +506,9 @@ impl<'a, 'b> PeepholeOptimizations {
/// <https://tc39.es/ecma262/#sec-strict-equality-comparison>
#[expect(clippy::float_cmp)]
fn try_strict_equality_comparison(
left_expr: &'b Expression<'a>,
right_expr: &'b Expression<'a>,
ctx: Ctx<'a, 'b>,
left_expr: &Expression<'a>,
right_expr: &Expression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<bool> {
let left = ValueType::from(left_expr);
let right = ValueType::from(right_expr);
@ -557,7 +557,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_number_constructor(
e: &CallExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let Expression::Identifier(ident) = &e.callee else { return None };
if ident.name != "Number" {
@ -601,7 +601,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn try_fold_binary_typeof_comparison(
bin_expr: &mut BinaryExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
// `typeof a === typeof b` -> `typeof a == typeof b`, `typeof a != typeof b` -> `typeof a != typeof b`,
// `typeof a == typeof a` -> `true`, `typeof a != typeof a` -> `false`
@ -687,7 +687,7 @@ impl<'a, 'b> PeepholeOptimizations {
fn fold_object_spread(
&mut self,
e: &mut ObjectExpression<'a>,
ctx: Ctx<'a, 'b>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let len = e.properties.len();
e.properties.retain(|p| {

View file

@ -0,0 +1,682 @@
use oxc_ast::{ast::*, NONE};
use oxc_span::{cmp::ContentEq, GetSpan};
use oxc_syntax::es_target::ESTarget;
use crate::ctx::Ctx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn minimize_conditional(
&self,
span: Span,
test: Expression<'a>,
consequent: Expression<'a>,
alternate: Expression<'a>,
ctx: Ctx<'a, '_>,
) -> Expression<'a> {
let mut cond_expr = ctx.ast.conditional_expression(span, test, consequent, alternate);
self.try_minimize_conditional(&mut cond_expr, ctx)
.unwrap_or_else(|| Expression::ConditionalExpression(ctx.ast.alloc(cond_expr)))
}
/// `MangleIfExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2745>
pub fn try_minimize_conditional(
&self,
expr: &mut ConditionalExpression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
match &mut expr.test {
// "(a, b) ? c : d" => "a, b ? c : d"
Expression::SequenceExpression(sequence_expr) => {
if sequence_expr.expressions.len() > 1 {
let span = expr.span();
let mut sequence = ctx.ast.move_expression(&mut expr.test);
let Expression::SequenceExpression(sequence_expr) = &mut sequence else {
unreachable!()
};
let expr = self.minimize_conditional(
span,
sequence_expr.expressions.pop().unwrap(),
ctx.ast.move_expression(&mut expr.consequent),
ctx.ast.move_expression(&mut expr.alternate),
ctx,
);
sequence_expr.expressions.push(expr);
return Some(sequence);
}
}
// "!a ? b : c" => "a ? c : b"
Expression::UnaryExpression(test_expr) => {
if test_expr.operator.is_not() {
let test = ctx.ast.move_expression(&mut test_expr.argument);
let consequent = ctx.ast.move_expression(&mut expr.alternate);
let alternate = ctx.ast.move_expression(&mut expr.consequent);
return Some(
self.minimize_conditional(expr.span, test, consequent, alternate, ctx),
);
}
}
Expression::Identifier(id) => {
// "a ? a : b" => "a || b"
if let Expression::Identifier(id2) = &expr.consequent {
if id.name == id2.name {
return Some(Self::join_with_left_associative_op(
expr.span,
LogicalOperator::Or,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut expr.alternate),
ctx,
));
}
}
// "a ? b : a" => "a && b"
if let Expression::Identifier(id2) = &expr.alternate {
if id.name == id2.name {
return Some(Self::join_with_left_associative_op(
expr.span,
LogicalOperator::And,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut expr.consequent),
ctx,
));
}
}
}
// `x != y ? b : c` -> `x == y ? c : b`
Expression::BinaryExpression(test_expr) => {
if matches!(
test_expr.operator,
BinaryOperator::Inequality | BinaryOperator::StrictInequality
) {
test_expr.operator = test_expr.operator.equality_inverse_operator().unwrap();
let test = ctx.ast.move_expression(&mut expr.test);
let consequent = ctx.ast.move_expression(&mut expr.consequent);
let alternate = ctx.ast.move_expression(&mut expr.alternate);
return Some(
self.minimize_conditional(expr.span, test, alternate, consequent, ctx),
);
}
}
_ => {}
}
// "a ? true : false" => "!!a"
// "a ? false : true" => "!a"
if let (Expression::BooleanLiteral(left), Expression::BooleanLiteral(right)) =
(&expr.consequent, &expr.alternate)
{
match (left.value, right.value) {
(true, false) => {
let test = ctx.ast.move_expression(&mut expr.test);
let test = Self::minimize_not(expr.span, test, ctx);
let test = Self::minimize_not(expr.span, test, ctx);
return Some(test);
}
(false, true) => {
let test = ctx.ast.move_expression(&mut expr.test);
let test = Self::minimize_not(expr.span, test, ctx);
return Some(test);
}
_ => {}
}
}
// "a ? b ? c : d : d" => "a && b ? c : d"
if let Expression::ConditionalExpression(consequent) = &mut expr.consequent {
if ctx.expr_eq(&consequent.alternate, &expr.alternate) {
return Some(ctx.ast.expression_conditional(
expr.span,
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::And,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut consequent.test),
ctx,
),
ctx.ast.move_expression(&mut consequent.consequent),
ctx.ast.move_expression(&mut consequent.alternate),
));
}
}
// "a ? b : c ? b : d" => "a || c ? b : d"
if let Expression::ConditionalExpression(alternate) = &mut expr.alternate {
if ctx.expr_eq(&alternate.consequent, &expr.consequent) {
return Some(ctx.ast.expression_conditional(
expr.span,
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::Or,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut alternate.test),
ctx,
),
ctx.ast.move_expression(&mut expr.consequent),
ctx.ast.move_expression(&mut alternate.alternate),
));
}
}
// "a ? c : (b, c)" => "(a || b), c"
if let Expression::SequenceExpression(alternate) = &mut expr.alternate {
if alternate.expressions.len() == 2
&& ctx.expr_eq(&alternate.expressions[1], &expr.consequent)
{
return Some(ctx.ast.expression_sequence(
expr.span,
ctx.ast.vec_from_array([
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::Or,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut alternate.expressions[0]),
ctx,
),
ctx.ast.move_expression(&mut expr.consequent),
]),
));
}
}
// "a ? (b, c) : c" => "(a && b), c"
if let Expression::SequenceExpression(consequent) = &mut expr.consequent {
if consequent.expressions.len() == 2
&& ctx.expr_eq(&consequent.expressions[1], &expr.alternate)
{
return Some(ctx.ast.expression_sequence(
expr.span,
ctx.ast.vec_from_array([
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::And,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut consequent.expressions[0]),
ctx,
),
ctx.ast.move_expression(&mut expr.alternate),
]),
));
}
}
// "a ? b || c : c" => "(a && b) || c"
if let Expression::LogicalExpression(logical_expr) = &mut expr.consequent {
if logical_expr.operator == LogicalOperator::Or
&& ctx.expr_eq(&logical_expr.right, &expr.alternate)
{
return Some(ctx.ast.expression_logical(
expr.span,
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::And,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut logical_expr.left),
ctx,
),
LogicalOperator::Or,
ctx.ast.move_expression(&mut expr.alternate),
));
}
}
// "a ? c : b && c" => "(a || b) && c"
if let Expression::LogicalExpression(logical_expr) = &mut expr.alternate {
if logical_expr.operator == LogicalOperator::And
&& ctx.expr_eq(&logical_expr.right, &expr.consequent)
{
return Some(ctx.ast.expression_logical(
expr.span,
Self::join_with_left_associative_op(
expr.test.span(),
LogicalOperator::Or,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut logical_expr.left),
ctx,
),
LogicalOperator::And,
ctx.ast.move_expression(&mut expr.consequent),
));
}
}
// `a ? b(c, d) : b(e, d)` -> `b(a ? c : e, d)`
if let (
Expression::Identifier(test),
Expression::CallExpression(consequent),
Expression::CallExpression(alternate),
) = (&expr.test, &mut expr.consequent, &mut expr.alternate)
{
if consequent.arguments.len() == alternate.arguments.len()
&& !ctx.is_global_reference(test)
&& ctx.expr_eq(&consequent.callee, &alternate.callee)
&& consequent
.arguments
.iter()
.zip(&alternate.arguments)
.skip(1)
.all(|(a, b)| a.content_eq(b))
{
// `a ? b(...c) : b(...e)` -> `b(...a ? c : e)``
if matches!(consequent.arguments[0], Argument::SpreadElement(_))
&& matches!(alternate.arguments[0], Argument::SpreadElement(_))
{
let callee = ctx.ast.move_expression(&mut consequent.callee);
let consequent_first_arg = {
let Argument::SpreadElement(el) = &mut consequent.arguments[0] else {
unreachable!()
};
ctx.ast.move_expression(&mut el.argument)
};
let alternate_first_arg = {
let Argument::SpreadElement(el) = &mut alternate.arguments[0] else {
unreachable!()
};
ctx.ast.move_expression(&mut el.argument)
};
let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec());
args[0] = ctx.ast.argument_spread_element(
expr.span,
ctx.ast.expression_conditional(
expr.test.span(),
ctx.ast.move_expression(&mut expr.test),
consequent_first_arg,
alternate_first_arg,
),
);
return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false));
}
// `a ? b(c) : b(e)` -> `b(a ? c : e)`
if !matches!(consequent.arguments[0], Argument::SpreadElement(_))
&& !matches!(alternate.arguments[0], Argument::SpreadElement(_))
{
let callee = ctx.ast.move_expression(&mut consequent.callee);
let consequent_first_arg =
ctx.ast.move_expression(consequent.arguments[0].to_expression_mut());
let alternate_first_arg =
ctx.ast.move_expression(alternate.arguments[0].to_expression_mut());
let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec());
let cond_expr = self.minimize_conditional(
expr.test.span(),
ctx.ast.move_expression(&mut expr.test),
consequent_first_arg,
alternate_first_arg,
ctx,
);
args[0] = Argument::from(cond_expr);
return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false));
}
}
}
// Not part of esbuild
if let Some(e) = self.try_merge_conditional_expression_inside(expr, ctx) {
return Some(e);
}
// Try using the "??" or "?." operators
if self.target >= ESTarget::ES2020 {
if let Expression::BinaryExpression(test_binary) = &mut expr.test {
if let Some(is_negate) = match test_binary.operator {
BinaryOperator::Inequality => Some(true),
BinaryOperator::Equality => Some(false),
_ => None,
} {
// a == null / a != null / (a = foo) == null / (a = foo) != null
let value_expr_with_id_name = if test_binary.left.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.right)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.right))
} else {
None
}
} else if test_binary.right.is_null() {
if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.left)
.filter(|id| !ctx.is_global_reference(id))
{
Some((id.name, &mut test_binary.left))
} else {
None
}
} else {
None
};
if let Some((target_id_name, value_expr)) = value_expr_with_id_name {
// `a == null ? b : a` -> `a ?? b`
// `a != null ? a : b` -> `a ?? b`
// `(a = foo) == null ? b : a` -> `(a = foo) ?? b`
// `(a = foo) != null ? a : b` -> `(a = foo) ?? b`
let maybe_same_id_expr =
if is_negate { &mut expr.consequent } else { &mut expr.alternate };
if maybe_same_id_expr.is_specific_id(&target_id_name) {
return Some(ctx.ast.expression_logical(
expr.span,
ctx.ast.move_expression(value_expr),
LogicalOperator::Coalesce,
ctx.ast.move_expression(if is_negate {
&mut expr.alternate
} else {
&mut expr.consequent
}),
));
}
// "a == null ? undefined : a.b.c[d](e)" => "a?.b.c[d](e)"
// "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)"
// "(a = foo) == null ? undefined : a.b.c[d](e)" => "(a = foo)?.b.c[d](e)"
// "(a = foo) != null ? a.b.c[d](e) : undefined" => "(a = foo)?.b.c[d](e)"
let maybe_undefined_expr =
if is_negate { &expr.alternate } else { &expr.consequent };
if ctx.is_expression_undefined(maybe_undefined_expr) {
let expr_to_inject_optional_chaining =
if is_negate { &mut expr.consequent } else { &mut expr.alternate };
if Self::inject_optional_chaining_if_matched(
&target_id_name,
value_expr,
expr_to_inject_optional_chaining,
ctx,
) {
return Some(
ctx.ast.move_expression(expr_to_inject_optional_chaining),
);
}
}
}
}
}
}
if ctx.expr_eq(&expr.alternate, &expr.consequent) {
// TODO:
// "/* @__PURE__ */ a() ? b : b" => "b"
// if ctx.ExprCanBeRemovedIfUnused(test) {
// return yes
// }
// "a ? b : b" => "a, b"
let expressions = ctx.ast.vec_from_array([
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut expr.consequent),
]);
return Some(ctx.ast.expression_sequence(expr.span, expressions));
}
None
}
/// Merge `consequent` and `alternate` of `ConditionalExpression` inside.
///
/// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1`
fn try_merge_conditional_expression_inside(
&self,
expr: &mut ConditionalExpression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let (
Expression::AssignmentExpression(consequent),
Expression::AssignmentExpression(alternate),
) = (&mut expr.consequent, &mut expr.alternate)
else {
return None;
};
if !matches!(consequent.left, AssignmentTarget::AssignmentTargetIdentifier(_)) {
return None;
}
if consequent.right.is_anonymous_function_definition() {
return None;
}
if consequent.operator != AssignmentOperator::Assign
|| consequent.operator != alternate.operator
|| consequent.left.content_ne(&alternate.left)
{
return None;
}
let cond_expr = self.minimize_conditional(
expr.span,
ctx.ast.move_expression(&mut expr.test),
ctx.ast.move_expression(&mut consequent.right),
ctx.ast.move_expression(&mut alternate.right),
ctx,
);
Some(ctx.ast.expression_assignment(
expr.span,
consequent.operator,
ctx.ast.move_assignment_target(&mut alternate.left),
cond_expr,
))
}
/// Modify `expr` if that has `target_expr` as a parent, and returns true if modified.
///
/// For `target_expr` = `a`, `expr` = `a.b`, this function changes `expr` to `a?.b` and returns true.
fn inject_optional_chaining_if_matched(
target_id_name: &str,
expr_to_inject: &mut Expression<'a>,
expr: &mut Expression<'a>,
ctx: Ctx<'a, '_>,
) -> bool {
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
expr,
ctx,
) {
if !matches!(expr, Expression::ChainExpression(_)) {
*expr = ctx.ast.expression_chain(
expr.span(),
ctx.ast.move_expression(expr).into_chain_element().unwrap(),
);
}
true
} else {
false
}
}
/// See [`Self::inject_optional_chaining_if_matched`]
fn inject_optional_chaining_if_matched_inner(
target_id_name: &str,
expr_to_inject: &mut Expression<'a>,
expr: &mut Expression<'a>,
ctx: Ctx<'a, '_>,
) -> bool {
match expr {
Expression::StaticMemberExpression(e) => {
if e.object.is_specific_id(target_id_name) {
e.optional = true;
e.object = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.object,
ctx,
) {
return true;
}
}
Expression::ComputedMemberExpression(e) => {
if e.object.is_specific_id(target_id_name) {
e.optional = true;
e.object = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.object,
ctx,
) {
return true;
}
}
Expression::CallExpression(e) => {
if e.callee.is_specific_id(target_id_name) {
e.optional = true;
e.callee = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.callee,
ctx,
) {
return true;
}
}
Expression::ChainExpression(e) => match &mut e.expression {
ChainElement::StaticMemberExpression(e) => {
if e.object.is_specific_id(target_id_name) {
e.optional = true;
e.object = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.object,
ctx,
) {
return true;
}
}
ChainElement::ComputedMemberExpression(e) => {
if e.object.is_specific_id(target_id_name) {
e.optional = true;
e.object = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.object,
ctx,
) {
return true;
}
}
ChainElement::CallExpression(e) => {
if e.callee.is_specific_id(target_id_name) {
e.optional = true;
e.callee = ctx.ast.move_expression(expr_to_inject);
return true;
}
if Self::inject_optional_chaining_if_matched_inner(
target_id_name,
expr_to_inject,
&mut e.callee,
ctx,
) {
return true;
}
}
_ => {}
},
_ => {}
}
false
}
}
#[cfg(test)]
mod test {
use oxc_syntax::es_target::ESTarget;
use crate::{
tester::{run, test, test_same},
CompressOptions,
};
fn test_es2019(source_text: &str, expected: &str) {
let target = ESTarget::ES2019;
assert_eq!(
run(source_text, Some(CompressOptions { target, ..CompressOptions::default() })),
run(expected, None)
);
}
#[test]
fn test_minimize_expr_condition() {
test("(x ? true : false) && y()", "x && y()");
test("(x ? false : true) && y()", "!x && y()");
test("(x ? true : y) && y()", "(x || y) && y();");
test("(x ? y : false) && y()", "(x && y) && y()");
test("var x; (x && true) && y()", "var x; x && y()");
test("var x; (x && false) && y()", "var x; x && !1");
test("(x && true) && y()", "x && y()");
test("(x && false) && y()", "x && !1");
test("var x; (x || true) && y()", "var x; x || !0, y()");
test("var x; (x || false) && y()", "var x; x && y()");
test("(x || true) && y()", "x || !0, y()");
test("(x || false) && y()", "x && y()");
test("let x = foo ? true : false", "let x = !!foo");
test("let x = foo ? true : bar", "let x = foo ? !0 : bar");
test("let x = foo ? bar : false", "let x = foo ? bar : !1");
test("function x () { return a ? true : false }", "function x() { return !!a }");
test("function x () { return a ? false : true }", "function x() { return !a }");
test("function x () { return a ? true : b }", "function x() { return a ? !0 : b }");
// can't be minified e.g. `a = ''` would return `''`
test("function x() { return a && true }", "function x() { return a && !0 }");
test("foo ? bar : bar", "foo, bar");
test_same("foo ? bar : baz");
test("foo() ? bar : bar", "foo(), bar");
test_same("var k = () => !!x;");
}
#[test]
fn minimize_conditional_exprs() {
test("(a, b) ? c : d", "a, b ? c : d");
test("!a ? b : c", "a ? c : b");
// test("/* @__PURE__ */ a() ? b : b", "b");
test("a ? b : b", "a, b");
test("a ? true : false", "a");
test("a ? false : true", "!a");
test("a ? a : b", "a || b");
test("a ? b : a", "a && b");
test("a ? b ? c : d : d", "a && b ? c : d");
test("a ? b : c ? b : d", "a || c ? b : d");
test("a ? c : (b, c)", "(a || b), c");
test("a ? (b, c) : c", "(a && b), c");
test("a ? b || c : c", "(a && b) || c");
test("a ? c : b && c", "(a || b) && c");
test("var a; a ? b(c, d) : b(e, d)", "var a; b(a ? c : e, d)");
test("var a; a ? b(...c) : b(...e)", "var a; b(...a ? c : e)");
test("var a; a ? b(c) : b(e)", "var a; b(a ? c : e)");
test("a() != null ? a() : b", "a() == null ? b : a()");
test("var a; a != null ? a : b", "var a; a ?? b");
test("var a; (a = _a) != null ? a : b", "var a; (a = _a) ?? b");
test("a != null ? a : b", "a == null ? b : a"); // accessing global `a` may have a getter with side effects
test_es2019("var a; a != null ? a : b", "var a; a == null ? b : a");
test("var a; a != null ? a.b.c[d](e) : undefined", "var a; a?.b.c[d](e)");
test("var a; (a = _a) != null ? a.b.c[d](e) : undefined", "var a; (a = _a)?.b.c[d](e)");
test("a != null ? a.b.c[d](e) : undefined", "a != null && a.b.c[d](e)"); // accessing global `a` may have a getter with side effects
test(
"var a, undefined = 1; a != null ? a.b.c[d](e) : undefined",
"var a, undefined = 1; a == null ? undefined : a.b.c[d](e)",
);
test_es2019(
"var a; a != null ? a.b.c[d](e) : undefined",
"var a; a != null && a.b.c[d](e)",
);
test("cmp !== 0 ? cmp : (bar, cmp);", "cmp === 0 && bar, cmp;");
test("cmp === 0 ? cmp : (bar, cmp);", "cmp === 0 || bar, cmp;");
test("cmp !== 0 ? (bar, cmp) : cmp;", "cmp === 0 || bar, cmp;");
test("cmp === 0 ? (bar, cmp) : cmp;", "cmp === 0 && bar, cmp;");
}
#[test]
fn compress_conditional() {
test("foo ? foo : bar", "foo || bar");
test("foo ? bar : foo", "foo && bar");
test_same("x.y ? x.y : bar");
test_same("x.y ? bar : x.y");
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,151 @@
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ValueType};
use oxc_span::GetSpan;
use oxc_traverse::Ancestor;
use crate::ctx::Ctx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn try_fold_stmt_in_boolean_context(stmt: &mut Statement<'a>, ctx: Ctx<'a, '_>) {
let expr = match stmt {
Statement::IfStatement(s) => Some(&mut s.test),
Statement::WhileStatement(s) => Some(&mut s.test),
Statement::ForStatement(s) => s.test.as_mut(),
Statement::DoWhileStatement(s) => Some(&mut s.test),
Statement::ExpressionStatement(s)
if !matches!(
ctx.ancestry.ancestor(1),
Ancestor::ArrowFunctionExpressionBody(_)
) =>
{
Some(&mut s.expression)
}
_ => None,
};
if let Some(expr) = expr {
Self::try_fold_expr_in_boolean_context(expr, ctx);
}
}
/// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`.
///
/// `SimplifyBooleanExpr`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L2059>
pub fn try_fold_expr_in_boolean_context(expr: &mut Expression<'a>, ctx: Ctx<'a, '_>) -> bool {
match expr {
// "!!a" => "a"
Expression::UnaryExpression(u1) if u1.operator.is_not() => {
if let Expression::UnaryExpression(u2) = &mut u1.argument {
if u2.operator.is_not() {
let mut e = ctx.ast.move_expression(&mut u2.argument);
Self::try_fold_expr_in_boolean_context(&mut e, ctx);
*expr = e;
return true;
}
}
}
Expression::BinaryExpression(e)
if e.operator.is_equality()
&& matches!(&e.right, Expression::NumericLiteral(lit) if lit.value == 0.0)
&& ValueType::from(&e.left).is_number() =>
{
let argument = ctx.ast.move_expression(&mut e.left);
*expr = if matches!(
e.operator,
BinaryOperator::StrictInequality | BinaryOperator::Inequality
) {
// `if ((a | b) !== 0)` -> `if (a | b);`
argument
} else {
// `if ((a | b) === 0);", "if (!(a | b));")`
ctx.ast.expression_unary(e.span, UnaryOperator::LogicalNot, argument)
};
return true;
}
// "if (!!a && !!b)" => "if (a && b)"
Expression::LogicalExpression(e) if e.operator == LogicalOperator::And => {
Self::try_fold_expr_in_boolean_context(&mut e.left, ctx);
Self::try_fold_expr_in_boolean_context(&mut e.right, ctx);
// "if (anything && truthyNoSideEffects)" => "if (anything)"
if ctx.get_side_free_boolean_value(&e.right) == Some(true) {
*expr = ctx.ast.move_expression(&mut e.left);
return true;
}
}
// "if (!!a ||!!b)" => "if (a || b)"
Expression::LogicalExpression(e) if e.operator == LogicalOperator::Or => {
Self::try_fold_expr_in_boolean_context(&mut e.left, ctx);
Self::try_fold_expr_in_boolean_context(&mut e.right, ctx);
// "if (anything || falsyNoSideEffects)" => "if (anything)"
if ctx.get_side_free_boolean_value(&e.right) == Some(false) {
*expr = ctx.ast.move_expression(&mut e.left);
return true;
}
}
Expression::ConditionalExpression(e) => {
// "if (a ? !!b : !!c)" => "if (a ? b : c)"
Self::try_fold_expr_in_boolean_context(&mut e.consequent, ctx);
Self::try_fold_expr_in_boolean_context(&mut e.alternate, ctx);
if let Some(boolean) = ctx.get_side_free_boolean_value(&e.consequent) {
let right = ctx.ast.move_expression(&mut e.alternate);
let left = ctx.ast.move_expression(&mut e.test);
let span = e.span;
let (op, left) = if boolean {
// "if (anything1 ? truthyNoSideEffects : anything2)" => "if (anything1 || anything2)"
(LogicalOperator::Or, left)
} else {
// "if (anything1 ? falsyNoSideEffects : anything2)" => "if (!anything1 && anything2)"
(LogicalOperator::And, Self::minimize_not(left.span(), left, ctx))
};
*expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
return true;
}
if let Some(boolean) = ctx.get_side_free_boolean_value(&e.alternate) {
let left = ctx.ast.move_expression(&mut e.test);
let right = ctx.ast.move_expression(&mut e.consequent);
let span = e.span;
let (op, left) = if boolean {
// "if (anything1 ? anything2 : truthyNoSideEffects)" => "if (!anything1 || anything2)"
(LogicalOperator::Or, Self::minimize_not(left.span(), left, ctx))
} else {
// "if (anything1 ? anything2 : falsyNoSideEffects)" => "if (anything1 && anything2)"
(LogicalOperator::And, left)
};
*expr = Self::join_with_left_associative_op(span, op, left, right, ctx);
return true;
}
}
_ => {}
}
false
}
}
#[cfg(test)]
mod test {
use crate::tester::test;
#[test]
fn test_try_fold_in_boolean_context() {
test("if (!!a);", "a");
test("while (!!a);", "for (;a;);");
test("do; while (!!a);", "do; while (a);");
test("for (;!!a;);", "for (;a;);");
test("!!a ? b : c", "a ? b : c");
test("if (!!!a);", "!a");
// test("Boolean(!!a)", "Boolean()");
test("if ((a | b) !== 0);", "a | b");
test("if ((a | b) === 0);", "!(a | b)");
test("if (!!a && !!b);", "a && b");
test("if (!!a || !!b);", "a || b");
test("if (anything || (0, false));", "anything");
test("if (a ? !!b : !!c);", "a ? b : c");
test("if (anything1 ? (0, true) : anything2);", "anything1 || anything2");
test("if (anything1 ? (0, false) : anything2);", "!anything1 && anything2");
test("if (anything1 ? anything2 : (0, true));", "!anything1 || anything2");
test("if (anything1 ? anything2 : (0, false));", "anything1 && anything2");
test("if(!![]);", "");
}
}

View file

@ -0,0 +1,221 @@
use oxc_ast::ast::*;
use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::TraverseCtx;
use crate::ctx::Ctx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
/// `MangleIf`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_parser/js_parser.go#L9860>
pub fn try_minimize_if(
&mut self,
if_stmt: &mut IfStatement<'a>,
traverse_ctx: &mut TraverseCtx<'a>,
) -> Option<Statement<'a>> {
self.wrap_to_avoid_ambiguous_else(if_stmt, traverse_ctx);
let ctx = Ctx(traverse_ctx);
if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
if if_stmt.alternate.is_none() {
let (op, e) = match &mut if_stmt.test {
// "if (!a) b();" => "a || b();"
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::Or, &mut unary_expr.argument)
}
// "if (a) b();" => "a && b();"
e => (LogicalOperator::And, e),
};
let a = ctx.ast.move_expression(e);
let b = ctx.ast.move_expression(&mut expr_stmt.expression);
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
return Some(ctx.ast.statement_expression(if_stmt.span, expr));
} else if let Some(Statement::ExpressionStatement(alternate_expr_stmt)) =
&mut if_stmt.alternate
{
// "if (a) b(); else c();" => "a ? b() : c();"
let test = ctx.ast.move_expression(&mut if_stmt.test);
let consequent = ctx.ast.move_expression(&mut expr_stmt.expression);
let alternate = ctx.ast.move_expression(&mut alternate_expr_stmt.expression);
let expr =
self.minimize_conditional(if_stmt.span, test, consequent, alternate, ctx);
return Some(ctx.ast.statement_expression(if_stmt.span, expr));
}
} else if Self::is_statement_empty(&if_stmt.consequent) {
if if_stmt.alternate.is_none()
|| if_stmt.alternate.as_ref().is_some_and(Self::is_statement_empty)
{
// "if (a) {}" => "a;"
let expr = ctx.ast.move_expression(&mut if_stmt.test);
return Some(ctx.ast.statement_expression(if_stmt.span, expr));
} else if let Some(Statement::ExpressionStatement(expr_stmt)) = &mut if_stmt.alternate {
let (op, e) = match &mut if_stmt.test {
// "if (!a) {} else b();" => "a && b();"
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
(LogicalOperator::And, &mut unary_expr.argument)
}
// "if (a) {} else b();" => "a || b();"
e => (LogicalOperator::Or, e),
};
let a = ctx.ast.move_expression(e);
let b = ctx.ast.move_expression(&mut expr_stmt.expression);
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
return Some(ctx.ast.statement_expression(if_stmt.span, expr));
} else if let Some(stmt) = &mut if_stmt.alternate {
// "yes" is missing and "no" is not missing (and is not an expression)
match &mut if_stmt.test {
// "if (!a) {} else return b;" => "if (a) return b;"
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
if_stmt.test = ctx.ast.move_expression(&mut unary_expr.argument);
if_stmt.consequent = ctx.ast.move_statement(stmt);
if_stmt.alternate = None;
self.mark_current_function_as_changed();
}
// "if (a) {} else return b;" => "if (!a) return b;"
_ => {
if_stmt.test = Self::minimize_not(
if_stmt.test.span(),
ctx.ast.move_expression(&mut if_stmt.test),
ctx,
);
if_stmt.consequent = ctx.ast.move_statement(stmt);
if_stmt.alternate = None;
self.mark_current_function_as_changed();
}
}
}
} else {
// "yes" is not missing (and is not an expression)
if let Some(alternate) = &mut if_stmt.alternate {
// "yes" is not missing (and is not an expression) and "no" is not missing
if let Expression::UnaryExpression(unary_expr) = &mut if_stmt.test {
if unary_expr.operator.is_not() {
// "if (!a) return b; else return c;" => "if (a) return c; else return b;"
if_stmt.test = ctx.ast.move_expression(&mut unary_expr.argument);
std::mem::swap(&mut if_stmt.consequent, alternate);
self.wrap_to_avoid_ambiguous_else(if_stmt, traverse_ctx);
self.mark_current_function_as_changed();
}
}
// "if (a) return b; else {}" => "if (a) return b;" is handled by remove_dead_code
} else {
// "no" is missing
if let Statement::IfStatement(if2_stmt) = &mut if_stmt.consequent {
if if2_stmt.alternate.is_none() {
// "if (a) if (b) return c;" => "if (a && b) return c;"
let a = ctx.ast.move_expression(&mut if_stmt.test);
let b = ctx.ast.move_expression(&mut if2_stmt.test);
if_stmt.test = Self::join_with_left_associative_op(
if_stmt.test.span(),
LogicalOperator::And,
a,
b,
ctx,
);
if_stmt.consequent = ctx.ast.move_statement(&mut if2_stmt.consequent);
self.mark_current_function_as_changed();
}
}
}
}
None
}
/// Wrap to avoid ambiguous else.
/// `if (foo) if (bar) baz else quaz` -> `if (foo) { if (bar) baz else quaz }`
fn wrap_to_avoid_ambiguous_else(
&mut self,
if_stmt: &mut IfStatement<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if let Statement::IfStatement(if2) = &mut if_stmt.consequent {
if if2.consequent.is_jump_statement() && if2.alternate.is_some() {
let scope_id = ctx.create_child_scope_of_current(ScopeFlags::empty());
if_stmt.consequent = Statement::BlockStatement(ctx.ast.alloc(
ctx.ast.block_statement_with_scope_id(
if_stmt.consequent.span(),
ctx.ast.vec1(ctx.ast.move_statement(&mut if_stmt.consequent)),
scope_id,
),
));
self.mark_current_function_as_changed();
}
}
}
fn is_statement_empty(stmt: &Statement<'a>) -> bool {
match stmt {
Statement::BlockStatement(block_stmt) if block_stmt.body.is_empty() => true,
Statement::EmptyStatement(_) => true,
_ => false,
}
}
}
#[cfg(test)]
mod test {
use crate::tester::test;
#[test]
fn test_minimize_if() {
test(
"function writeInteger(int) {
if (int >= 0)
if (int <= 0xffffffff) return this.u32(int);
else if (int > -0x80000000) return this.n32(int);
}",
"function writeInteger(int) {
if (int >= 0) {
if (int <= 4294967295) return this.u32(int);
if (int > -2147483648) return this.n32(int);
}
}",
);
test(
"function bar() {
if (!x) {
return null;
} else if (y) {
return foo;
} else if (z) {
return bar;
}
}",
"function bar() {
if (x) {
if (y)
return foo;
if (z)
return bar;
} else return null;
}",
);
test(
"function f() {
if (foo)
if (bar) return X;
else return Y;
return Z;
}",
"function f() {
return foo ? bar ? X : Y : Z;
}",
);
test(
"function _() {
if (currentChar === '\\n')
return pos + 1;
else if (currentChar !== ' ' && currentChar !== '\\t')
return pos + 1;
}",
"function _() {
if (currentChar === '\\n' || currentChar !== ' ' && currentChar !== '\\t')
return pos + 1;
}",
);
}
}

View file

@ -0,0 +1,87 @@
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::ValueType;
use oxc_span::GetSpan;
use crate::ctx::Ctx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
pub fn minimize_not(span: Span, expr: Expression<'a>, ctx: Ctx<'a, '_>) -> Expression<'a> {
let mut unary = ctx.ast.unary_expression(span, UnaryOperator::LogicalNot, expr);
Self::try_minimize_not(&mut unary, ctx)
.unwrap_or_else(|| Expression::UnaryExpression(ctx.ast.alloc(unary)))
}
/// `MaybeSimplifyNot`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_ast/js_ast_helpers.go#L73>
pub fn try_minimize_not(
expr: &mut UnaryExpression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
if !expr.operator.is_not() {
return None;
}
match &mut expr.argument {
// `!!true` -> `true`
// `!!false` -> `false`
Expression::UnaryExpression(e)
if e.operator.is_not() && ValueType::from(&e.argument).is_boolean() =>
{
Some(ctx.ast.move_expression(&mut e.argument))
}
// `!(a == b)` => `a != b`
// `!(a != b)` => `a == b`
// `!(a === b)` => `a !== b`
// `!(a !== b)` => `a === b`
Expression::BinaryExpression(e) if e.operator.is_equality() => {
e.operator = e.operator.equality_inverse_operator().unwrap();
Some(ctx.ast.move_expression(&mut expr.argument))
}
// "!(a, b)" => "a, !b"
Expression::SequenceExpression(sequence_expr) => {
if let Some(e) = sequence_expr.expressions.pop() {
let e = ctx.ast.expression_unary(e.span(), UnaryOperator::LogicalNot, e);
let expressions = ctx.ast.vec_from_iter(
sequence_expr.expressions.drain(..).chain(std::iter::once(e)),
);
return Some(ctx.ast.expression_sequence(sequence_expr.span, expressions));
}
None
}
_ => None,
}
}
}
#[cfg(test)]
mod test {
use crate::tester::{test, test_same};
#[test]
fn minimize_duplicate_nots() {
// test("!x", "x"); // TODO: in ExpressionStatement
test("!!x", "x");
test("!!!x", "!x");
test("!!!!x", "x");
test("!!!(x && y)", "!(x && y)");
test_same("var k = () => { !!x; }");
test_same("var k = !!x;");
test_same("function k () { return !!x; }");
test("var k = () => { return !!x; }", "var k = () => !!x");
test_same("var k = () => !!x;");
}
#[test]
fn minimize_nots_with_binary_expressions() {
test("!(x === undefined)", "x !== void 0");
test("!(typeof(x) === 'undefined')", "!(typeof x > 'u')");
test("!(x === void 0)", "x !== void 0");
test("!!delete x.y", "delete x.y");
test("!!!delete x.y", "!delete x.y");
test("!!!!delete x.y", "delete x.y");
test("var k = !!(foo instanceof bar)", "var k = foo instanceof bar");
test_same("!(a === 1 ? void 0 : a.b)"); // FIXME: can be compressed to `a === 1 || !a.b`
test("!(a, b)", "a, !b");
}
}

View file

@ -1,8 +1,12 @@
mod collapse_variable_declarations;
mod convert_to_dotted_properties;
mod fold_constants;
mod minimize_conditional_expression;
mod minimize_conditions;
mod minimize_exit_points;
mod minimize_expression_in_boolean_context;
mod minimize_if_statement;
mod minimize_not_expression;
mod minimize_statements;
mod normalize;
mod remove_dead_code;
@ -145,7 +149,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
if !self.is_prev_function_changed() {
return;
}
Self::minimize_conditions_exit_statement(stmt, Ctx(traverse_ctx));
Self::try_fold_stmt_in_boolean_context(stmt, Ctx(traverse_ctx));
self.remove_dead_code_exit_statement(stmt, Ctx(traverse_ctx));
if let Statement::IfStatement(if_stmt) = stmt {
if let Some(folded_stmt) = self.try_minimize_if(if_stmt, traverse_ctx) {