diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index bee9c0c0bdbdb..f5d43464f75f8 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -166,7 +166,6 @@ impl<'a> Traverse<'a> for PeepholeOptimizations { self.mark_current_function_as_changed(); } } - self.substitute_exit_statement(stmt, ctx); } fn exit_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 5503ab9d65320..2147f94e96133 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -79,38 +79,84 @@ impl<'a> PeepholeOptimizations { return false; } + // try optional chaining and nullish coalescing if self.target >= ESTarget::ES2020 { - // "a != null && a.b()" => "a?.b()" - // "a == null || a.b()" => "a?.b()" let LogicalExpression { + span: logical_span, left: logical_left, right: logical_right, operator: logical_op, - .. } = logical_expr.as_mut(); if let Expression::BinaryExpression(binary_expr) = logical_left { - if matches!( - (logical_op, binary_expr.operator), + match (logical_op, binary_expr.operator) { + // "a != null && a.b()" => "a?.b()" + // "a == null || a.b()" => "a?.b()" (LogicalOperator::And, BinaryOperator::Inequality) - | (LogicalOperator::Or, BinaryOperator::Equality) - ) { - let name_and_id = if let Expression::Identifier(id) = &binary_expr.left { - (!ctx.is_global_reference(id) && binary_expr.right.is_null()) - .then_some((id.name, &mut binary_expr.left)) - } else if let Expression::Identifier(id) = &binary_expr.right { - (!ctx.is_global_reference(id) && binary_expr.left.is_null()) - .then_some((id.name, &mut binary_expr.right)) - } else { - None - }; - if let Some((name, id)) = name_and_id { - if Self::inject_optional_chaining_if_matched(&name, id, logical_right, ctx) - { - *e = ctx.ast.move_expression(logical_right); + | (LogicalOperator::Or, BinaryOperator::Equality) => { + let name_and_id = if let Expression::Identifier(id) = &binary_expr.left { + (!ctx.is_global_reference(id) && binary_expr.right.is_null()) + .then_some((id.name, &mut binary_expr.left)) + } else if let Expression::Identifier(id) = &binary_expr.right { + (!ctx.is_global_reference(id) && binary_expr.left.is_null()) + .then_some((id.name, &mut binary_expr.right)) + } else { + None + }; + if let Some((name, id)) = name_and_id { + if Self::inject_optional_chaining_if_matched( + &name, + id, + logical_right, + ctx, + ) { + *e = ctx.ast.move_expression(logical_right); + self.mark_current_function_as_changed(); + return false; + } + } + } + // "a == null && b" => "a ?? b" + // "a != null || b" => "a ?? b" + // "a == null && (a = b)" => "a ??= b" + // "a != null || (a = b)" => "a ??= b" + (LogicalOperator::And, BinaryOperator::Equality) + | (LogicalOperator::Or, BinaryOperator::Inequality) => { + let new_left_hand_expr = if binary_expr.right.is_null() { + Some(&mut binary_expr.left) + } else if binary_expr.left.is_null() { + Some(&mut binary_expr.right) + } else { + None + }; + if let Some(new_left_hand_expr) = new_left_hand_expr { + if let Expression::AssignmentExpression(assignment_expr) = logical_right + { + if assignment_expr.operator == AssignmentOperator::Assign + && Self::has_no_side_effect_for_evaluation_same_target( + &assignment_expr.left, + new_left_hand_expr, + ctx, + ) + { + assignment_expr.span = *logical_span; + assignment_expr.operator = AssignmentOperator::LogicalNullish; + *e = ctx.ast.move_expression(logical_right); + self.mark_current_function_as_changed(); + return false; + } + } + + *e = ctx.ast.expression_logical( + *logical_span, + ctx.ast.move_expression(new_left_hand_expr), + LogicalOperator::Coalesce, + ctx.ast.move_expression(logical_right), + ); self.mark_current_function_as_changed(); return false; } } + _ => {} } } } @@ -735,6 +781,16 @@ mod test { test_same("a == null || a.b()"); // a may have a getter test("var a; null != a && a.b()", "var a; a?.b()"); test("var a; null == a || a.b()", "var a; a?.b()"); + + test("x == null && y", "x ?? y"); + test("x != null || y", "x ?? y"); + test_same("v = x == null && y"); + test_same("v = x != null || y"); + test("a == null && (a = b)", "a ??= b"); + test("a != null || (a = b)", "a ??= b"); + test_same("v = a == null && (a = b)"); + test_same("v = a != null || (a = b)"); + test("void (x == null && y)", "x ?? y"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 0226aa457c5a6..ae6d8cbd6a0c4 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -933,62 +933,6 @@ impl<'a> PeepholeOptimizations { self.mark_current_function_as_changed(); } } - - pub fn substitute_exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: Ctx<'a, '_>) { - if let Statement::ExpressionStatement(expr_stmt) = stmt { - if let Some(folded_expr) = match &mut expr_stmt.expression { - Expression::LogicalExpression(expr) => { - self.try_compress_is_null_and_to_nullish_coalescing(expr, ctx) - } - _ => None, - } { - expr_stmt.expression = folded_expr; - self.mark_current_function_as_changed(); - } - } - } - - /// Compress `a == null && b` to `a ?? b` - /// - /// - `a == null && b` -> `a ?? b` - /// - `a != null || b` -> `a ?? b` - /// - /// This can be only done when the return value is not used. - /// For example when a = 1, `a == null && b` returns `false` while `a ?? b` returns `1`. - fn try_compress_is_null_and_to_nullish_coalescing( - &self, - expr: &mut LogicalExpression<'a>, - ctx: Ctx<'a, '_>, - ) -> Option> { - if self.target < ESTarget::ES2020 { - return None; - } - let target_op = match expr.operator { - LogicalOperator::And => BinaryOperator::Equality, - LogicalOperator::Or => BinaryOperator::Inequality, - LogicalOperator::Coalesce => return None, - }; - let Expression::BinaryExpression(binary_expr) = &mut expr.left else { - return None; - }; - if binary_expr.operator != target_op { - return None; - } - let new_left_hand_expr = if binary_expr.left.is_null() { - ctx.ast.move_expression(&mut binary_expr.right) - } else if binary_expr.right.is_null() { - ctx.ast.move_expression(&mut binary_expr.left) - } else { - return None; - }; - - Some(ctx.ast.expression_logical( - expr.span, - new_left_hand_expr, - LogicalOperator::Coalesce, - ctx.ast.move_expression(&mut expr.right), - )) - } } impl<'a> LatePeepholeOptimizations { @@ -1764,15 +1708,6 @@ mod test { test_same("var a = class C { foo() { return C } }"); } - #[test] - fn test_compress_is_null_and_to_nullish_coalescing() { - test("x == null && y", "x ?? y"); - test("x != null || y", "x ?? y"); - test_same("v = x == null && y"); - test_same("v = x != null || y"); - test("void (x == null && y)", "x ?? y"); - } - #[test] fn test_compress_destructuring_assignment_target() { test_same("var {y} = x");