From 6bae7e5c0795d4b0ca8322a36ce1f1c41693879b Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Tue, 7 Jan 2025 21:04:43 +0900 Subject: [PATCH] feat(minifier): compress `a || (a = b)` to `a ||= b` --- .../peephole_substitute_alternate_syntax.rs | 54 ++++++++++++++++++- crates/oxc_syntax/src/operator.rs | 9 ++++ tasks/minsize/minsize.snap | 20 +++---- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index 8e66746a5e1d7..53ce14029af3e 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -153,7 +153,8 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { Expression::AssignmentExpression(e) => { Self::try_compress_assignment_to_update_expression(e, ctx) } - Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx), + Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx) + .or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx)), Expression::NewExpression(e) => Self::try_fold_new_expression(e, ctx), Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx), Expression::BinaryExpression(e) => Self::try_fold_loose_equals_undefined(e, ctx) @@ -457,6 +458,39 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { Some(ctx.ast.expression_binary(span, left_id_expr, replace_op, null_expr)) } + /// Compress `a || (a = b)` to `a ||= b` + fn try_compress_logical_expression_to_assignment_expression( + &self, + expr: &mut LogicalExpression<'a>, + ctx: Ctx<'a, 'b>, + ) -> Option> { + if self.target < ESTarget::ES2020 { + return None; + } + + let Expression::AssignmentExpression(assignment_expr) = &mut expr.right else { + return None; + }; + if assignment_expr.operator != AssignmentOperator::Assign { + return None; + } + + let new_op = expr.operator.to_assignment_operator(); + + let AssignmentTarget::AssignmentTargetIdentifier(write_id_ref) = &mut assignment_expr.left + else { + return None; + }; + let Expression::Identifier(read_id_ref) = &mut expr.left else { return None }; + if write_id_ref.name != read_id_ref.name { + return None; + } + + assignment_expr.span = expr.span; + assignment_expr.operator = new_op; + Some(ctx.ast.move_expression(&mut expr.right)) + } + fn commutative_pair( pair: (&A, &A), check_a: F, @@ -1499,6 +1533,24 @@ mod test { test_same("foo !== void 0 && bar !== null"); } + #[test] + fn test_fold_logical_expression_to_assignment_expression() { + test("x || (x = 3)", "x ||= 3"); + test("x && (x = 3)", "x &&= 3"); + test("x ?? (x = 3)", "x ??= 3"); + test("x || (x = g())", "x ||= g()"); + test("x && (x = g())", "x &&= g()"); + test("x ?? (x = g())", "x ??= g()"); + + test_same("x || (y = 3)"); + + let allocator = Allocator::default(); + let target = ESTarget::ES2019; + let mut pass = super::PeepholeSubstituteAlternateSyntax::new(target, false); + let code = "x || (x = 3)"; + tester::test(&allocator, code, code, &mut pass); + } + #[test] fn test_fold_loose_equals_undefined() { test_same("foo != null"); diff --git a/crates/oxc_syntax/src/operator.rs b/crates/oxc_syntax/src/operator.rs index 3033aba99694f..70aa66ef039df 100644 --- a/crates/oxc_syntax/src/operator.rs +++ b/crates/oxc_syntax/src/operator.rs @@ -444,6 +444,15 @@ impl LogicalOperator { Self::Coalesce => Precedence::Conditional, } } + + /// Get [`AssignmentOperator`] corresponding to this [`LogicalOperator`]. + pub fn to_assignment_operator(self) -> AssignmentOperator { + match self { + Self::Or => AssignmentOperator::LogicalOr, + Self::And => AssignmentOperator::LogicalAnd, + Self::Coalesce => AssignmentOperator::LogicalNullish, + } + } } impl GetPrecedence for LogicalOperator { diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index aa40aaa69d3fd..481f54c2a534b 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -5,23 +5,23 @@ Original | minified | minified | gzip | gzip | Fixture 173.90 kB | 59.78 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js -287.63 kB | 90.05 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js +287.63 kB | 90.04 kB | 90.07 kB | 32.06 kB | 31.95 kB | jquery.js -342.15 kB | 118.16 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js +342.15 kB | 118.15 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js -544.10 kB | 71.79 kB | 72.48 kB | 26.18 kB | 26.20 kB | lodash.js +544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js -555.77 kB | 272.96 kB | 270.13 kB | 90.94 kB | 90.80 kB | d3.js +555.77 kB | 272.92 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js -1.01 MB | 460.24 kB | 458.89 kB | 126.84 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.23 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js -1.25 MB | 652.58 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js +1.25 MB | 652.57 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js -2.14 MB | 726.03 kB | 724.14 kB | 180.15 kB | 181.07 kB | victory.js +2.14 MB | 725.98 kB | 724.14 kB | 180.13 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.83 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.81 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.76 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.73 kB | 488.28 kB | antd.js -10.95 MB | 3.50 MB | 3.49 MB | 909.12 kB | 915.50 kB | typescript.js +10.95 MB | 3.50 MB | 3.49 MB | 908.35 kB | 915.50 kB | typescript.js