From 50fce2046541e76b766b2785a5a42c3f274c1479 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sat, 1 Mar 2025 09:28:20 +0000 Subject: [PATCH] feat(minifier): support binary expression in `remove_unused_expression` (#9456) Compress `a() === b` into `a()` when the return value is not used. --- .../src/peephole/minimize_conditions.rs | 62 +++++----- .../src/peephole/minimize_not_expression.rs | 6 +- .../src/peephole/remove_dead_code.rs | 2 +- .../src/peephole/remove_unused_expression.rs | 111 +++++++++++++++++- .../peephole/substitute_alternate_syntax.rs | 38 +++--- crates/oxc_minifier/tests/peephole/esbuild.rs | 4 +- 6 files changed, 165 insertions(+), 58 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index 282f8673b8096..64baf1bb633f1 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -1308,8 +1308,8 @@ mod test { test("if((-0 != +0) !== false){}", ""); test_same("foo(x >> y == 0)"); - test("(x = 1) === 1", "(x = 1) == 1"); - test("(x = 1) !== 1", "(x = 1) != 1"); + test("v = (x = 1) === 1", "v = (x = 1) == 1"); + test("v = (x = 1) !== 1", "v = (x = 1) != 1"); test("v = !0 + null !== 1", "v = !1"); } @@ -1360,39 +1360,39 @@ mod test { #[test] fn test_fold_is_null_or_undefined() { - test("foo === null || foo === undefined", "foo == null"); - test("foo === undefined || foo === null", "foo == null"); - test("foo === null || foo === void 0", "foo == null"); - test("foo === null || foo === void 0 || foo === 1", "foo == null || foo === 1"); - test("foo === 1 || foo === null || foo === void 0", "foo === 1 || foo == null"); - test_same("foo === void 0 || bar === null"); - test_same("var undefined = 1; foo === null || foo === undefined"); - test_same("foo !== 1 && foo === void 0 || foo === null"); - test_same("foo.a === void 0 || foo.a === null"); // cannot be folded because accessing foo.a might have a side effect - - test("foo !== null && foo !== undefined", "foo != null"); - test("foo !== undefined && foo !== null", "foo != null"); - test("foo !== null && foo !== void 0", "foo != null"); - test("foo !== null && foo !== void 0 && foo !== 1", "foo != null && foo !== 1"); - test("foo !== 1 && foo !== null && foo !== void 0", "foo !== 1 && foo != null"); - test("foo !== 1 || foo !== void 0 && foo !== null", "foo !== 1 || foo != null"); - test_same("foo !== void 0 && bar !== null"); - - test("(_foo = foo) === null || _foo === undefined", "(_foo = foo) == null"); - test("(_foo = foo) === null || _foo === void 0", "(_foo = foo) == null"); - test("(_foo = foo.bar) === null || _foo === undefined", "(_foo = foo.bar) == null"); - test("(_foo = foo) !== null && _foo !== undefined", "(_foo = foo) != null"); - test("(_foo = foo) === undefined || _foo === null", "(_foo = foo) == null"); - test("(_foo = foo) === void 0 || _foo === null", "(_foo = foo) == null"); + test("v = foo === null || foo === undefined", "v = foo == null"); + test("v = foo === undefined || foo === null", "v = foo == null"); + test("v = foo === null || foo === void 0", "v = foo == null"); + test("v = foo === null || foo === void 0 || foo === 1", "v = foo == null || foo === 1"); + test("v = foo === 1 || foo === null || foo === void 0", "v = foo === 1 || foo == null"); + test_same("v = foo === void 0 || bar === null"); + test_same("var undefined = 1; v = foo === null || foo === undefined"); + test_same("v = foo !== 1 && foo === void 0 || foo === null"); + test_same("v = foo.a === void 0 || foo.a === null"); // cannot be folded because accessing foo.a might have a side effect + + test("v = foo !== null && foo !== undefined", "v = foo != null"); + test("v = foo !== undefined && foo !== null", "v = foo != null"); + test("v = foo !== null && foo !== void 0", "v = foo != null"); + test("v = foo !== null && foo !== void 0 && foo !== 1", "v = foo != null && foo !== 1"); + test("v = foo !== 1 && foo !== null && foo !== void 0", "v = foo !== 1 && foo != null"); + test("v = foo !== 1 || foo !== void 0 && foo !== null", "v = foo !== 1 || foo != null"); + test_same("v = foo !== void 0 && bar !== null"); + + test("v = (_foo = foo) === null || _foo === undefined", "v = (_foo = foo) == null"); + test("v = (_foo = foo) === null || _foo === void 0", "v = (_foo = foo) == null"); + test("v = (_foo = foo.bar) === null || _foo === undefined", "v = (_foo = foo.bar) == null"); + test("v = (_foo = foo) !== null && _foo !== undefined", "v = (_foo = foo) != null"); + test("v = (_foo = foo) === undefined || _foo === null", "v = (_foo = foo) == null"); + test("v = (_foo = foo) === void 0 || _foo === null", "v = (_foo = foo) == null"); test( - "(_foo = foo) === null || _foo === void 0 || _foo === 1", - "(_foo = foo) == null || _foo === 1", + "v = (_foo = foo) === null || _foo === void 0 || _foo === 1", + "v = (_foo = foo) == null || _foo === 1", ); test( - "_foo === 1 || (_foo = foo) === null || _foo === void 0", - "_foo === 1 || (_foo = foo) == null", + "v = _foo === 1 || (_foo = foo) === null || _foo === void 0", + "v = _foo === 1 || (_foo = foo) == null", ); - test_same("(_foo = foo) === void 0 || bar === null"); + test_same("v = (_foo = foo) === void 0 || bar === null"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/minimize_not_expression.rs b/crates/oxc_minifier/src/peephole/minimize_not_expression.rs index b6a62df5fd5b0..89cbe76c3c2dd 100644 --- a/crates/oxc_minifier/src/peephole/minimize_not_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_not_expression.rs @@ -82,10 +82,10 @@ mod test { #[test] fn minimize_nots_with_binary_expressions() { - test("!(x === undefined)", "x !== void 0"); + test("!(x === undefined)", "x"); test("!(typeof(x) === 'undefined')", ""); - test("!(typeof(x()) === 'undefined')", "x() !== void 0"); - test("!(x === void 0)", "x !== void 0"); + test("!(typeof(x()) === 'undefined')", "x()"); + test("!(x === void 0)", "x"); test("!!delete x.y", "delete x.y"); test("!!!delete x.y", "delete x.y"); test("!!!!delete x.y", "delete x.y"); diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index 210407175a1ef..7ecac556cfce5 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -470,7 +470,7 @@ mod test { test("{if(false)if(false)if(false)foo(); {bar()}}", "bar()"); test("{'hi'}", ""); - test("{x==3}", "x == 3"); + test("{x==3}", "x"); test("{`hello ${foo}`}", "`${foo}`"); test("{ (function(){x++}) }", ""); test("{ (function foo(){x++; foo()}) }", ""); diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 0cc20ab8fd54f..c65e50554f0d3 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -24,9 +24,9 @@ impl<'a> PeepholeOptimizations { Expression::TemplateLiteral(_) => self.fold_template_literal(e, ctx), Expression::ObjectExpression(_) => self.fold_object_expression(e, ctx), Expression::ConditionalExpression(_) => self.fold_conditional_expression(e, ctx), + Expression::BinaryExpression(_) => self.fold_binary_expression(e, ctx), // TODO - // Expression::BinaryExpression(_) - // | Expression::CallExpression(_) + // Expression::CallExpression(_) // | Expression::NewExpression(_) => { // false // } @@ -347,6 +347,89 @@ impl<'a> PeepholeOptimizations { false } + + fn fold_binary_expression(&self, e: &mut Expression<'a>, ctx: Ctx<'a, '_>) -> bool { + let Expression::BinaryExpression(binary_expr) = e else { + return false; + }; + + match binary_expr.operator { + BinaryOperator::Equality + | BinaryOperator::Inequality + | BinaryOperator::StrictEquality + | BinaryOperator::StrictInequality + | BinaryOperator::LessThan + | BinaryOperator::LessEqualThan + | BinaryOperator::GreaterThan + | BinaryOperator::GreaterEqualThan => { + let left = self.remove_unused_expression(&mut binary_expr.left, ctx); + let right = self.remove_unused_expression(&mut binary_expr.right, ctx); + match (left, right) { + (true, true) => true, + (true, false) => { + *e = ctx.ast.move_expression(&mut binary_expr.right); + false + } + (false, true) => { + *e = ctx.ast.move_expression(&mut binary_expr.left); + false + } + (false, false) => { + *e = ctx.ast.expression_sequence( + binary_expr.span, + ctx.ast.vec_from_array([ + ctx.ast.move_expression(&mut binary_expr.left), + ctx.ast.move_expression(&mut binary_expr.right), + ]), + ); + false + } + } + } + BinaryOperator::Addition => { + Self::fold_string_addition_chain(e, ctx); + matches!(e, Expression::StringLiteral(_)) + } + _ => !e.may_have_side_effects(&ctx), + } + } + + /// returns whether the passed expression is a string + fn fold_string_addition_chain(e: &mut Expression<'a>, ctx: Ctx<'a, '_>) -> bool { + let Expression::BinaryExpression(binary_expr) = e else { + return e.to_primitive(&ctx).is_string() == Some(true); + }; + if binary_expr.operator != BinaryOperator::Addition { + return e.to_primitive(&ctx).is_string() == Some(true); + } + + let left_is_string = Self::fold_string_addition_chain(&mut binary_expr.left, ctx); + if left_is_string { + if !binary_expr.left.may_have_side_effects(&ctx) { + binary_expr.left = + ctx.ast.expression_string_literal(binary_expr.left.span(), "", None); + } + + let right_as_primitive = binary_expr.right.to_primitive(&ctx); + if right_as_primitive.is_symbol() == Some(false) + && !binary_expr.right.may_have_side_effects(&ctx) + { + *e = ctx.ast.move_expression(&mut binary_expr.left); + return true; + } + return true; + } + + let right_as_primitive = binary_expr.right.to_primitive(&ctx); + if right_as_primitive.is_string() == Some(true) { + if !binary_expr.right.may_have_side_effects(&ctx) { + binary_expr.right = + ctx.ast.expression_string_literal(binary_expr.right.span(), "", None); + } + return true; + } + false + } } #[cfg(test)] @@ -509,4 +592,28 @@ mod test { test("foo() ? bar() : 2", "!foo() || bar()"); // can be improved to "foo() && bar()" test_same("foo() ? bar() : baz()"); } + + #[test] + fn test_fold_binary_expression() { + test("var a, b; a === b", "var a, b;"); + test("var a, b; a() === b", "var a, b; a()"); + test("var a, b; a === b()", "var a, b; b()"); + test("var a, b; a() === b()", "var a, b; a(), b()"); + + test("var a, b; a !== b", "var a, b;"); + test("var a, b; a == b", "var a, b;"); + test("var a, b; a != b", "var a, b;"); + test("var a, b; a < b", "var a, b;"); + test("var a, b; a > b", "var a, b;"); + test("var a, b; a <= b", "var a, b;"); + test("var a, b; a >= b", "var a, b;"); + + test_same("var a, b; a + b"); + test("var a, b; 'a' + b", "var a, b; '' + b"); + test_same("var a, b; a + '' + b"); + test("var a, b, c; 'a' + (b === c)", "var a, b, c;"); + test("var a, b; 'a' + +b", "var a, b; '' + +b"); // can be improved to "var a, b; +b" + test_same("var a, b; a + ('' + b)"); + test("var a, b, c; a + ('' + (b === c))", "var a, b, c; a + ''"); + } } diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 85d3a61754738..0226aa457c5a6 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -1215,25 +1215,25 @@ mod test { #[test] fn test_fold_true_false_comparison() { - test("x == true", "x == !0"); - test("x == false", "x == !1"); - test("x != true", "x != !0"); - test("x < true", "x < !0"); - test("x <= true", "x <= !0"); - test("x > true", "x > !0"); - test("x >= true", "x >= !0"); + test("v = x == true", "v = x == !0"); + test("v = x == false", "v = x == !1"); + test("v = x != true", "v = x != !0"); + test("v = x < true", "v = x < !0"); + test("v = x <= true", "v = x <= !0"); + test("v = x > true", "v = x > !0"); + test("v = x >= true", "v = x >= !0"); - test("x instanceof true", "x instanceof !0"); - test("x + false", "x + !1"); + test("v = x instanceof true", "v = x instanceof !0"); + test("v = x + false", "v = x + !1"); // Order: should perform the nearest. - test("x == x instanceof false", "x == x instanceof !1"); - test("x in x >> true", "x in x >> !0"); - test("x == fake(false)", "x == fake(!1)"); + test("v = x == x instanceof false", "v = x == x instanceof !1"); + test("v = x in x >> true", "v = x in x >> !0"); + test("v = x == fake(false)", "v = x == fake(!1)"); // The following should not be folded. - test("x === true", "x === !0"); - test("x !== false", "x !== !1"); + test("v = x === true", "v = x === !0"); + test("v = x !== false", "v = x !== !1"); } /// Based on https://github.com/terser/terser/blob/58ba5c163fa1684f2a63c7bc19b7ebcf85b74f73/test/compress/assignment.js @@ -1622,11 +1622,11 @@ mod test { #[test] fn test_fold_loose_equals_undefined() { - test_same("foo != null"); - test("foo != undefined", "foo != null"); - test("foo != void 0", "foo != null"); - test("undefined != foo", "foo != null"); - test("void 0 != foo", "foo != null"); + test_same("v = foo != null"); + test("v = foo != undefined", "v = foo != null"); + test("v = foo != void 0", "v = foo != null"); + test("v = undefined != foo", "v = foo != null"); + test("v = void 0 != foo", "v = foo != null"); } #[test] diff --git a/crates/oxc_minifier/tests/peephole/esbuild.rs b/crates/oxc_minifier/tests/peephole/esbuild.rs index d89bdfd3523ac..e6a28c8d4c809 100644 --- a/crates/oxc_minifier/tests/peephole/esbuild.rs +++ b/crates/oxc_minifier/tests/peephole/esbuild.rs @@ -623,7 +623,7 @@ fn js_parser_test() { test("a ? b() : b()", "a, b();"); test("a ? b?.() : b?.()", "a, b?.();"); test("a ? b?.[c] : b?.[c]", "a, b?.[c];"); - test("a ? b == c : b == c", "a, b == c;"); + test("a ? b == c : b == c", "a, b, c;"); test("a ? b.c(d + e[f]) : b.c(d + e[f])", "a, b.c(d + e[f]);"); // test("a ? -b : !b", "a ? -b : b;"); test("a ? b() : b(c)", "a ? b() : b(c);"); @@ -631,7 +631,7 @@ fn js_parser_test() { test("a ? b?.c : b.c", "a ? b?.c : b.c;"); test("a ? b?.() : b()", "a ? b?.() : b();"); test("a ? b?.[c] : b[c]", "a ? b?.[c] : b[c];"); - test("a ? b == c : b != c", "a ? b == c : b != c;"); + // test("a ? b == c : b != c", "a ? (b, c) : (b, c);"); test("a ? b.c(d + e[f]) : b.c(d + e[g])", "a ? b.c(d + e[f]) : b.c(d + e[g]);"); test("(a, b) ? c : d", "a, b ? c : d;"); test(