diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index d1194987d..52703b752 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -14,7 +14,7 @@ use crate::ctx::Ctx; use super::PeepholeOptimizations; -impl<'a, 'b> PeepholeOptimizations { +impl<'a> PeepholeOptimizations { /// Constant Folding /// /// @@ -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> { + fn try_fold_unary_expr(e: &UnaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option> { 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> { // 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> { // 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> { 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> { 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> { 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> { 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> { // 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> { + fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option> { 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> { + fn try_fold_comparison(e: &BinaryExpression<'a>, ctx: Ctx<'a, '_>) -> Option> { let left = &e.left; let right = &e.right; let op = e.operator; @@ -414,9 +414,9 @@ impl<'a, 'b> PeepholeOptimizations { /// 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 { let left = ValueType::from(left_expr); let right = ValueType::from(right_expr); @@ -506,9 +506,9 @@ impl<'a, 'b> PeepholeOptimizations { /// #[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 { 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> { 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> { // `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> { let len = e.properties.len(); e.properties.retain(|p| { diff --git a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs new file mode 100644 index 000000000..4d5175858 --- /dev/null +++ b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs @@ -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`: + pub fn try_minimize_conditional( + &self, + expr: &mut ConditionalExpression<'a>, + ctx: Ctx<'a, '_>, + ) -> Option> { + 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> { + 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"); + } +} diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index 0902d98e9..0b60c933b 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -1,11 +1,7 @@ -use oxc_ast::{ast::*, NONE}; -use oxc_ecmascript::{ - constant_evaluation::{ConstantEvaluation, ValueType}, - ToInt32, -}; +use oxc_ast::ast::*; +use oxc_ecmascript::{constant_evaluation::ValueType, ToInt32}; use oxc_span::{cmp::ContentEq, GetSpan}; -use oxc_syntax::{es_target::ESTarget, scope::ScopeFlags}; -use oxc_traverse::{Ancestor, TraverseCtx}; +use oxc_syntax::es_target::ESTarget; use crate::ctx::Ctx; @@ -13,34 +9,8 @@ use super::PeepholeOptimizations; /// Minimize Conditions /// -/// A peephole optimization that minimizes conditional expressions according to De Morgan's laws. -/// Also rewrites conditional statements as expressions by replacing them -/// with `? :` and short-circuit binary operators. -/// /// impl<'a> PeepholeOptimizations { - pub fn minimize_conditions_exit_statement(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); - } - } - pub fn minimize_conditions_exit_expression( &mut self, expr: &mut Expression<'a>, @@ -87,593 +57,6 @@ impl<'a> PeepholeOptimizations { } } - 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`: - fn try_minimize_not( - expr: &mut UnaryExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option> { - 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, - } - } - - /// `MangleIf`: - pub fn try_minimize_if( - &mut self, - if_stmt: &mut IfStatement<'a>, - traverse_ctx: &mut TraverseCtx<'a>, - ) -> Option> { - 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, - } - } - - 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`: - fn try_minimize_conditional( - &self, - expr: &mut ConditionalExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option> { - 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 - } - // The goal of this function is to "rotate" the AST if it's possible to use the // left-associative property of the operator to avoid unnecessary parentheses. // @@ -716,273 +99,6 @@ impl<'a> PeepholeOptimizations { ctx.ast.expression_logical(span, a, op, b) } - /// 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 - } - - /// 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> { - 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, - )) - } - - /// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`. - /// - /// - 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 - } - // `typeof foo === 'number'` -> `typeof foo == 'number'` // ^^^^^^^^^^ `ValueType::from(&e.left).is_string()` is `true`. // `a instanceof b === true` -> `a instanceof b` @@ -1173,7 +289,7 @@ impl<'a> PeepholeOptimizations { } /// Returns the identifier or the assignment target's identifier of the given expression. - fn extract_id_or_assign_to_id<'b>( + pub fn extract_id_or_assign_to_id<'b>( expr: &'b Expression<'a>, ) -> Option<&'b IdentifierReference<'a>> { match expr { @@ -1374,14 +490,6 @@ mod test { }; use oxc_syntax::es_target::ESTarget; - 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) - ); - } - /** Check that removing blocks with 1 child works */ #[test] fn test_fold_one_child_blocks() { @@ -1661,38 +769,6 @@ mod test { test("!!!foo ? bar : baz", "foo ? baz : bar"); } - #[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 test_minimize_while_condition() { // This test uses constant folding logic, so is only here for completeness. @@ -2414,14 +1490,6 @@ mod test { ); } - #[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"); - } - #[test] fn compress_binary_boolean() { test("a instanceof b === true", "a instanceof b"); @@ -2459,97 +1527,6 @@ mod test { test("!0 + null !== 1", "!0 + null != 1"); } - #[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"); - } - - #[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 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(!![]);", ""); - } - #[test] fn test_try_compress_type_of_equal_string() { test("typeof foo === 'number'", "typeof foo == 'number'"); @@ -2695,66 +1672,4 @@ mod test { run(code, None) ); } - - #[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; - }", - ); - } } diff --git a/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs b/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs new file mode 100644 index 000000000..ddd8dbc4f --- /dev/null +++ b/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs @@ -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`: + 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(!![]);", ""); + } +} diff --git a/crates/oxc_minifier/src/peephole/minimize_if_statement.rs b/crates/oxc_minifier/src/peephole/minimize_if_statement.rs new file mode 100644 index 000000000..edbd8e66a --- /dev/null +++ b/crates/oxc_minifier/src/peephole/minimize_if_statement.rs @@ -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`: + pub fn try_minimize_if( + &mut self, + if_stmt: &mut IfStatement<'a>, + traverse_ctx: &mut TraverseCtx<'a>, + ) -> Option> { + 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; + }", + ); + } +} diff --git a/crates/oxc_minifier/src/peephole/minimize_not_expression.rs b/crates/oxc_minifier/src/peephole/minimize_not_expression.rs new file mode 100644 index 000000000..9df7c37d9 --- /dev/null +++ b/crates/oxc_minifier/src/peephole/minimize_not_expression.rs @@ -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`: + pub fn try_minimize_not( + expr: &mut UnaryExpression<'a>, + ctx: Ctx<'a, '_>, + ) -> Option> { + 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"); + } +} diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 704b622ef..70f0dd071 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -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) {