From c8184723f4f843e7f931c7394c058cf4acf43feb Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Thu, 11 Jul 2024 04:40:23 +0000 Subject: [PATCH] feat(minifier): dce conditional expression `&&` or `||` (#4190) --- .../src/ast_passes/remove_dead_code.rs | 10 +++++ crates/oxc_minifier/src/folder/mod.rs | 45 +++++++++---------- .../tests/oxc/remove_dead_code.rs | 43 ++++++++++++------ 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs index 22b810c132016..d857169c29eba 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs @@ -25,6 +25,7 @@ impl<'a> VisitMut<'a> for RemoveDeadCode<'a> { fn visit_expression(&mut self, expr: &mut Expression<'a>) { self.fold_conditional_expression(expr); + self.fold_logical_expression(expr); } } @@ -124,6 +125,15 @@ impl<'a> RemoveDeadCode<'a> { _ => {} } } + + fn fold_logical_expression(&mut self, expr: &mut Expression<'a>) { + let Expression::LogicalExpression(logical_expr) = expr else { + return; + }; + if let Some(e) = self.folder.try_fold_logical_expression(logical_expr) { + *expr = e; + } + } } struct KeepVar<'a> { diff --git a/crates/oxc_minifier/src/folder/mod.rs b/crates/oxc_minifier/src/folder/mod.rs index 53e8262b7270f..50f9b5b71a8ff 100644 --- a/crates/oxc_minifier/src/folder/mod.rs +++ b/crates/oxc_minifier/src/folder/mod.rs @@ -90,12 +90,9 @@ impl<'a> Folder<'a> { UnaryOperator::Void => self.try_reduce_void(unary_expr), _ => None, }, - Expression::LogicalExpression(logic_expr) => match logic_expr.operator { - LogicalOperator::And | LogicalOperator::Or => { - self.try_fold_and_or(logic_expr.operator, logic_expr) - } - LogicalOperator::Coalesce => None, - }, + Expression::LogicalExpression(logic_expr) => { + self.try_fold_logical_expression(logic_expr) + } _ => None, }; if let Some(folded_expr) = folded_expr { @@ -672,39 +669,41 @@ impl<'a> Folder<'a> { None } + /// Try to fold a AND / OR node. + /// /// port from [closure-compiler](https://github.com/google/closure-compiler/blob/09094b551915a6487a980a783831cba58b5739d1/src/com/google/javascript/jscomp/PeepholeFoldConstants.java#L587) - /// Try to fold a AND/OR node. - fn try_fold_and_or( + pub fn try_fold_logical_expression( &mut self, - op: LogicalOperator, - logic_expr: &mut LogicalExpression<'a>, + logical_expr: &mut LogicalExpression<'a>, ) -> Option> { - let boolean_value = get_boolean_value(&logic_expr.left); - - if let Some(boolean_value) = boolean_value { + let op = logical_expr.operator; + if !matches!(op, LogicalOperator::And | LogicalOperator::Or) { + return None; + } + if let Some(boolean_value) = get_boolean_value(&logical_expr.left) { // (TRUE || x) => TRUE (also, (3 || x) => 3) // (FALSE && x) => FALSE if (boolean_value && op == LogicalOperator::Or) || (!boolean_value && op == LogicalOperator::And) { - return Some(self.move_out_expression(&mut logic_expr.left)); - } else if !logic_expr.left.may_have_side_effects() { + return Some(self.move_out_expression(&mut logical_expr.left)); + } else if !logical_expr.left.may_have_side_effects() { // (FALSE || x) => x // (TRUE && x) => x - return Some(self.move_out_expression(&mut logic_expr.right)); + return Some(self.move_out_expression(&mut logical_expr.right)); } // Left side may have side effects, but we know its boolean value. // e.g. true_with_sideeffects || foo() => true_with_sideeffects, foo() // or: false_with_sideeffects && foo() => false_with_sideeffects, foo() - let left = self.move_out_expression(&mut logic_expr.left); - let right = self.move_out_expression(&mut logic_expr.right); + let left = self.move_out_expression(&mut logical_expr.left); + let right = self.move_out_expression(&mut logical_expr.right); let mut vec = self.ast.vec_with_capacity(2); vec.push(left); vec.push(right); - let sequence_expr = self.ast.expression_sequence(logic_expr.span, vec); + let sequence_expr = self.ast.expression_sequence(logical_expr.span, vec); return Some(sequence_expr); - } else if let Expression::LogicalExpression(left_child) = &mut logic_expr.left { - if left_child.operator == logic_expr.operator { + } else if let Expression::LogicalExpression(left_child) = &mut logical_expr.left { + if left_child.operator == logical_expr.operator { let left_child_right_boolean = get_boolean_value(&left_child.right); let left_child_op = left_child.operator; if let Some(right_boolean) = left_child_right_boolean { @@ -715,9 +714,9 @@ impl<'a> Folder<'a> { || right_boolean && left_child_op == LogicalOperator::And { let left = self.move_out_expression(&mut left_child.left); - let right = self.move_out_expression(&mut logic_expr.right); + let right = self.move_out_expression(&mut logical_expr.right); let logic_expr = self.ast.expression_logical( - logic_expr.span, + logical_expr.span, left, left_child_op, right, diff --git a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs index 2d8607a6034cd..3bdb288818d6e 100644 --- a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs +++ b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs @@ -22,7 +22,7 @@ pub(crate) fn test(source_text: &str, expected: &str) { } #[test] -fn remove_dead_code() { +fn dce_if_statement() { test("if (true) { foo }", "{ foo }"); test("if (true) { foo } else { bar }", "{ foo }"); test("if (false) { foo } else { bar }", "{ bar }"); @@ -30,24 +30,15 @@ fn remove_dead_code() { test("if (!false) { foo }", "{ foo }"); test("if (!true) { foo } else { bar }", "{ bar }"); + test("if (!false && xxx) { foo }", "if (xxx) { foo; }"); + test("if (!true && yyy) { foo } else { bar }", "{ bar }"); + test("if ('production' == 'production') { foo } else { bar }", "{ foo }"); test("if ('development' == 'production') { foo } else { bar }", "{ bar }"); test("if ('production' === 'production') { foo } else { bar }", "{ foo }"); test("if ('development' === 'production') { foo } else { bar }", "{ bar }"); - test("false ? foo : bar;", "bar"); - test("true ? foo : bar;", "foo"); - - test("!true ? foo : bar;", "bar"); - test("!false ? foo : bar;", "foo"); - - test("!!false ? foo : bar;", "bar"); - test("!!true ? foo : bar;", "foo"); - - test("const foo = true ? A : B", "const foo = A"); - test("const foo = false ? A : B", "const foo = B"); - // Shadowed `undefined` as a variable should not be erased. test( "function foo(undefined) { if (!undefined) { } }", @@ -67,9 +58,33 @@ fn remove_dead_code() { ); } +#[test] +fn dce_conditional_expression() { + test("false ? foo : bar;", "bar"); + test("true ? foo : bar;", "foo"); + + test("!true ? foo : bar;", "bar"); + test("!false ? foo : bar;", "foo"); + + test("!!false ? foo : bar;", "bar"); + test("!!true ? foo : bar;", "foo"); + + test("const foo = true ? A : B", "const foo = A"); + test("const foo = false ? A : B", "const foo = B"); +} + +#[test] +fn dce_logical_expression() { + test("false && bar()", "false"); + test("true && bar()", "bar()"); + + test("const foo = false && bar()", "const foo = false"); + test("const foo = true && bar()", "const foo = bar()"); +} + // https://github.com/terser/terser/blob/master/test/compress/dead-code.js #[test] -fn remove_dead_code_from_terser() { +fn dce_from_terser() { test( "function f() { a();