From 94372d21a0e02085f85e9a2b9d9c9788f76c8269 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:03:54 +0900 Subject: [PATCH] feat(minifier): compress `x = x || 1` to `x ||= 1` --- .../peephole_substitute_alternate_syntax.rs | 58 +++++++++++++++++++ tasks/minsize/minsize.snap | 18 +++--- 2 files changed, 67 insertions(+), 9 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 2ba0b8aa4842b2..6e45babf13711d 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 @@ -138,6 +138,7 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { } Expression::AssignmentExpression(e) => { self.try_compress_normal_assignment_to_combined_assignment(e, ctx); + self.try_compress_normal_assignment_to_combined_logical_assignment(e, ctx); } _ => {} } @@ -631,6 +632,42 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { self.changed = true; } + /// Compress `a = a || b` to `a ||= b` + /// + /// This can only be done for resolved identifiers as this would avoid setting `a` when `a` is truthy. + fn try_compress_normal_assignment_to_combined_logical_assignment( + &mut self, + expr: &mut AssignmentExpression<'a>, + ctx: Ctx<'a, 'b>, + ) { + if self.target < ESTarget::ES2020 { + return; + } + if !matches!(expr.operator, AssignmentOperator::Assign) { + return; + } + + let Expression::LogicalExpression(logical_expr) = &mut expr.right else { return }; + let new_op = logical_expr.operator.to_assignment_operator(); + + let ( + AssignmentTarget::AssignmentTargetIdentifier(write_id_ref), + Expression::Identifier(read_id_ref), + ) = (&expr.left, &logical_expr.left) + else { + return; + }; + // It should also early return when the reference might refer to a reference value created by a with statement + // when the minifier supports with statements + if write_id_ref.name != read_id_ref.name || ctx.is_global_reference(write_id_ref) { + return; + } + + expr.operator = new_op; + expr.right = ctx.ast.move_expression(&mut logical_expr.right); + self.changed = true; + } + fn try_compress_assignment_to_update_expression( expr: &mut AssignmentExpression<'a>, ctx: Ctx<'a, 'b>, @@ -1210,6 +1247,27 @@ mod test { // test_same("var x; with (z) { x.y || (x.y = 3) }"); } + #[test] + fn test_compress_normal_assignment_to_combined_logical_assignment() { + test("var x; x = x || 1", "var x; x ||= 1"); + test("var x; x = x && 1", "var x; x &&= 1"); + test("var x; x = x ?? 1", "var x; x ??= 1"); + + // `x` is a global reference and might have a setter + // Example case: `Object.defineProperty(globalThis, 'x', { get: () => true, set: () => console.log('x') }); x = x || 1` + test_same("x = x || 1"); + // setting x.y might have a side effect + test_same("var x; x.y = x.y || 1"); + // This case is not supported, since the minifier does not support with statements + // test_same("var x; with (z) { x = x || 1 }"); + + let allocator = Allocator::default(); + let target = ESTarget::ES2019; + let mut pass = super::PeepholeSubstituteAlternateSyntax::new(target, false); + let code = "var x; x = x || 1"; + tester::test(&allocator, code, code, &mut pass); + } + #[test] fn test_fold_subtraction_assignment() { test("x -= 1", "--x"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index c4776eedb92764..79d4577b0e3d0b 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.70 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 59.80 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js +173.90 kB | 59.79 kB | 59.82 kB | 19.40 kB | 19.33 kB | moment.js -287.63 kB | 90.10 kB | 90.07 kB | 32.04 kB | 31.95 kB | jquery.js +287.63 kB | 90.08 kB | 90.07 kB | 32.02 kB | 31.95 kB | jquery.js -342.15 kB | 118.11 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js +342.15 kB | 118.09 kB | 118.14 kB | 44.43 kB | 44.37 kB | vue.js 544.10 kB | 71.74 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js 555.77 kB | 273.19 kB | 270.13 kB | 90.91 kB | 90.80 kB | d3.js -1.01 MB | 460.18 kB | 458.89 kB | 126.76 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.17 kB | 458.89 kB | 126.75 kB | 126.71 kB | bundle.min.js -1.25 MB | 652.82 kB | 646.76 kB | 163.51 kB | 163.73 kB | three.js +1.25 MB | 652.81 kB | 646.76 kB | 163.50 kB | 163.73 kB | three.js -2.14 MB | 726.27 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js +2.14 MB | 726.23 kB | 724.14 kB | 180.12 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.80 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.78 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.65 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.64 kB | 488.28 kB | antd.js -10.95 MB | 3.49 MB | 3.49 MB | 907.49 kB | 915.50 kB | typescript.js +10.95 MB | 3.49 MB | 3.49 MB | 907.46 kB | 915.50 kB | typescript.js