Skip to content

Commit

Permalink
feat(minifier): dce conditional expression && or || (#4190)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Jul 11, 2024
1 parent e9ad03b commit c818472
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 37 deletions.
10 changes: 10 additions & 0 deletions crates/oxc_minifier/src/ast_passes/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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> {
Expand Down
45 changes: 22 additions & 23 deletions crates/oxc_minifier/src/folder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Expression<'a>> {
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 {
Expand All @@ -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,
Expand Down
43 changes: 29 additions & 14 deletions crates/oxc_minifier/tests/oxc/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,23 @@ 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 }");

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) { } }",
Expand All @@ -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();
Expand Down

0 comments on commit c818472

Please sign in to comment.