diff --git a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs index c0db66ddf4855..046da0e3c7dd9 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_minimize_conditions.rs @@ -1,7 +1,8 @@ use oxc_ast::ast::*; +use oxc_span::SPAN; use oxc_traverse::{Traverse, TraverseCtx}; -use crate::CompressorPass; +use crate::{CompressOptions, CompressorPass}; /// Minimize Conditions /// @@ -11,6 +12,7 @@ use crate::CompressorPass; /// /// pub struct PeepholeMinimizeConditions { + options: CompressOptions, changed: bool, } @@ -21,7 +23,9 @@ impl<'a> CompressorPass<'a> for PeepholeMinimizeConditions { fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { self.changed = false; - oxc_traverse::walk_program(self, program, ctx); + if self.options.conditions { + oxc_traverse::walk_program(self, program, ctx); + } } } @@ -35,11 +39,31 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions { self.changed = true; }; } + + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + match stmt { + Statement::BlockStatement(block) => { + if let Some(folded_stmt) = self.try_fold_block_statement(block, ctx) { + *stmt = folded_stmt; + self.changed = true; + } + } + Statement::IfStatement(if_stmt) => { + if let Some(folded_stmt) = + self.try_fold_single_consequent_without_alternate(if_stmt, ctx) + { + *stmt = folded_stmt; + self.changed = true; + } + } + _ => {} + } + } } impl<'a> PeepholeMinimizeConditions { - pub fn new() -> Self { - Self { changed: false } + pub fn new(options: CompressOptions) -> Self { + Self { changed: false, options } } /// Try to minimize NOT nodes such as `!(x==y)`. @@ -57,6 +81,57 @@ impl<'a> PeepholeMinimizeConditions { } None } + + fn try_fold_block_statement( + &self, + stmt: &mut BlockStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // It must have only one statement, so using unwrap is safe. + (stmt.body.len() == 1) + .then(|| { + stmt.body.get_mut(0).and_then(|stmt| { + matches!(stmt, Statement::ExpressionStatement(_)) + .then(|| ctx.ast.move_statement(stmt)) + }) + }) + .unwrap_or(None) + } + + fn try_fold_single_consequent_without_alternate( + &mut self, + stmt: &mut IfStatement<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + if stmt.alternate.is_some() { + return None; + }; + let consequence = match &mut stmt.consequent { + Statement::BlockStatement(block) => { + if block.body.len() != 1 { + return None; + } + block.body.get_mut(0)? + } + _ => &mut stmt.consequent, + }; + if matches!(consequence, Statement::ExpressionStatement(_)) && !stmt.test.is_literal() { + let condition = ctx.ast.move_expression(&mut stmt.test); + let consequent = ctx.ast.move_statement(consequence); + let expression = match consequent { + Statement::ExpressionStatement(mut expr) => { + ctx.ast.move_expression(&mut expr.expression) + } + _ => unreachable!(), + }; + + let new_expr = + ctx.ast.expression_logical(SPAN, condition, LogicalOperator::And, expression); + Some(ctx.ast.statement_expression(SPAN, new_expr)) + } else { + None + } + } } /// @@ -64,11 +139,11 @@ impl<'a> PeepholeMinimizeConditions { mod test { use oxc_allocator::Allocator; - use crate::tester; + use crate::{tester, CompressOptions}; fn test(source_text: &str, positive: &str) { let allocator = Allocator::default(); - let mut pass = super::PeepholeMinimizeConditions::new(); + let mut pass = super::PeepholeMinimizeConditions::new(CompressOptions::all_true()); tester::test(&allocator, source_text, positive, &mut pass); } @@ -86,7 +161,6 @@ mod test { /** Check that removing blocks with 1 child works */ #[test] - #[ignore] fn test_fold_one_child_blocks() { // late = false; fold("function f(){if(x)a();x=3}", "function f(){x&&a();x=3}"); @@ -95,16 +169,16 @@ mod test { fold("function f(){if(x){a()}x=3}", "function f(){x&&a();x=3}"); fold("function f(){if(x){a?.()}x=3}", "function f(){x&&a?.();x=3}"); - fold("function f(){if(x){return 3}}", "function f(){if(x)return 3}"); + // fold("function f(){if(x){return 3}}", "function f(){if(x)return 3}"); fold("function f(){if(x){a()}}", "function f(){x&&a()}"); - fold("function f(){if(x){throw 1}}", "function f(){if(x)throw 1;}"); + // fold("function f(){if(x){throw 1}}", "function f(){if(x)throw 1;}"); // Try it out with functions fold("function f(){if(x){foo()}}", "function f(){x&&foo()}"); - fold("function f(){if(x){foo()}else{bar()}}", "function f(){x?foo():bar()}"); + // fold("function f(){if(x){foo()}else{bar()}}", "function f(){x?foo():bar()}"); // Try it out with properties and methods - fold("function f(){if(x){a.b=1}}", "function f(){if(x)a.b=1}"); + fold("function f(){if(x){a.b=1}}", "function f(){x&&(a.b=1)}"); fold("function f(){if(x){a.b*=1}}", "function f(){x&&(a.b*=1)}"); fold("function f(){if(x){a.b+=1}}", "function f(){x&&(a.b+=1)}"); fold("function f(){if(x){++a.b}}", "function f(){x&&++a.b}"); @@ -112,53 +186,53 @@ mod test { fold("function f(){if(x){a?.foo()}}", "function f(){x&&a?.foo()}"); // Try it out with throw/catch/finally [which should not change] - fold_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}"); + // fold_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}"); // Try it out with switch statements - fold_same("function f(){switch(x){case 1:break}}"); + // fold_same("function f(){switch(x){case 1:break}}"); // Do while loops stay in a block if that's where they started - fold_same("function f(){if(e1){do foo();while(e2)}else foo2()}"); + // fold_same("function f(){if(e1){do foo();while(e2)}else foo2()}"); // Test an obscure case with do and while - fold("if(x){do{foo()}while(y)}else bar()", "if(x){do foo();while(y)}else bar()"); + // fold("if(x){do{foo()}while(y)}else bar()", "if(x){do foo();while(y)}else bar()"); // Play with nested IFs fold("function f(){if(x){if(y)foo()}}", "function f(){x && (y && foo())}"); - fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); - fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); - fold( - "function f(){if(x){if(y)foo();else bar()}else{baz()}}", - "function f(){x?y?foo():bar():baz()}", - ); + // fold("function f(){if(x){if(y)foo();else bar()}}", "function f(){x&&(y?foo():bar())}"); + // fold("function f(){if(x){if(y)foo()}else bar()}", "function f(){x?y&&foo():bar()}"); + // fold( + // "function f(){if(x){if(y)foo();else bar()}else{baz()}}", + // "function f(){x?y?foo():bar():baz()}", + // ); - fold("if(e1){while(e2){if(e3){foo()}}}else{bar()}", "if(e1)while(e2)e3&&foo();else bar()"); + // fold("if(e1){while(e2){if(e3){foo()}}}else{bar()}", "if(e1)while(e2)e3&&foo();else bar()"); - fold("if(e1){with(e2){if(e3){foo()}}}else{bar()}", "if(e1)with(e2)e3&&foo();else bar()"); + // fold("if(e1){with(e2){if(e3){foo()}}}else{bar()}", "if(e1)with(e2)e3&&foo();else bar()"); - fold("if(a||b){if(c||d){var x;}}", "if(a||b)if(c||d)var x"); - fold("if(x){ if(y){var x;}else{var z;} }", "if(x)if(y)var x;else var z"); + // fold("if(a||b){if(c||d){var x;}}", "if(a||b)if(c||d)var x"); + // fold("if(x){ if(y){var x;}else{var z;} }", "if(x)if(y)var x;else var z"); // NOTE - technically we can remove the blocks since both the parent // and child have elses. But we don't since it causes ambiguities in // some cases where not all descendent ifs having elses - fold( - "if(x){ if(y){var x;}else{var z;} }else{var w}", - "if(x)if(y)var x;else var z;else var w", - ); - fold("if (x) {var x;}else { if (y) { var y;} }", "if(x)var x;else if(y)var y"); + // fold( + // "if(x){ if(y){var x;}else{var z;} }else{var w}", + // "if(x)if(y)var x;else var z;else var w", + // ); + // fold("if (x) {var x;}else { if (y) { var y;} }", "if(x)var x;else if(y)var y"); // Here's some of the ambiguous cases - fold( - "if(a){if(b){f1();f2();}else if(c){f3();}}else {if(d){f4();}}", - "if(a)if(b){f1();f2()}else c&&f3();else d&&f4()", - ); + // fold( + // "if(a){if(b){f1();f2();}else if(c){f3();}}else {if(d){f4();}}", + // "if(a)if(b){f1();f2()}else c&&f3();else d&&f4()", + // ); - fold("function f(){foo()}", "function f(){foo()}"); - fold("switch(x){case y: foo()}", "switch(x){case y:foo()}"); - fold( - "try{foo()}catch(ex){bar()}finally{baz()}", - "try{foo()}catch(ex){bar()}finally{baz()}", - ); + // fold("function f(){foo()}", "function f(){foo()}"); + // fold("switch(x){case y: foo()}", "switch(x){case y:foo()}"); + // fold( + // "try{foo()}catch(ex){bar()}finally{baz()}", + // "try{foo()}catch(ex){bar()}finally{baz()}", + // ); } /** Try to minimize returns */ diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index cb1f7076020e9..8f7e781bdf009 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -50,7 +50,7 @@ impl<'a> Compressor<'a> { &mut StatementFusion::new(), &mut PeepholeRemoveDeadCode::new(), // TODO: MinimizeExitPoints - &mut PeepholeMinimizeConditions::new(), + &mut PeepholeMinimizeConditions::new(self.options), &mut PeepholeSubstituteAlternateSyntax::new(self.options), &mut PeepholeReplaceKnownMethods::new(), &mut PeepholeFoldConstants::new(), @@ -78,7 +78,7 @@ impl<'a> Compressor<'a> { fn dead_code_elimination(self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { PeepholeFoldConstants::new().build(program, ctx); - PeepholeMinimizeConditions::new().build(program, ctx); + PeepholeMinimizeConditions::new(self.options).build(program, ctx); PeepholeRemoveDeadCode::new().build(program, ctx); } } diff --git a/crates/oxc_minifier/src/options.rs b/crates/oxc_minifier/src/options.rs index 2dff7c98d2328..dca8c0fcf5a5c 100644 --- a/crates/oxc_minifier/src/options.rs +++ b/crates/oxc_minifier/src/options.rs @@ -2,6 +2,11 @@ pub struct CompressOptions { pub dead_code_elimination: bool, + /// Optimize conditions, for example from `if (a) { b } else { c }` to `a ? b : c`. + /// + /// Default `true` + pub conditions: bool, + /// Various optimizations for boolean context, for example `!!a ? b : c` → `a ? b : c`. /// /// Default `true` @@ -49,6 +54,7 @@ impl CompressOptions { pub fn all_true() -> Self { Self { dead_code_elimination: false, + conditions: true, booleans: true, drop_debugger: true, drop_console: true, @@ -62,6 +68,7 @@ impl CompressOptions { pub fn all_false() -> Self { Self { dead_code_elimination: false, + conditions: false, booleans: false, drop_debugger: false, drop_console: false, diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 16599d056f3d6..0bb66a13399a2 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,26 +1,26 @@ Original | Minified | esbuild | Gzip | esbuild -72.14 kB | 24.47 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js +72.14 kB | 24.43 kB | 23.70 kB | 8.70 kB | 8.54 kB | react.development.js -173.90 kB | 61.69 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js +173.90 kB | 61.62 kB | 59.82 kB | 19.62 kB | 19.33 kB | moment.js -287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js +287.63 kB | 92.60 kB | 90.07 kB | 32.49 kB | 31.95 kB | jquery.js -342.15 kB | 124.14 kB | 118.14 kB | 44.81 kB | 44.37 kB | vue.js +342.15 kB | 123.67 kB | 118.14 kB | 45.05 kB | 44.37 kB | vue.js -544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js +544.10 kB | 74.08 kB | 72.48 kB | 26.31 kB | 26.20 kB | lodash.js -555.77 kB | 278.70 kB | 270.13 kB | 91.39 kB | 90.80 kB | d3.js +555.77 kB | 278.58 kB | 270.13 kB | 91.70 kB | 90.80 kB | d3.js -1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js +1.01 MB | 469.45 kB | 458.89 kB | 127.45 kB | 126.71 kB | bundle.min.js -1.25 MB | 671.00 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js +1.25 MB | 670.46 kB | 646.76 kB | 165.26 kB | 163.73 kB | three.js -2.14 MB | 756.69 kB | 724.14 kB | 182.87 kB | 181.07 kB | victory.js +2.14 MB | 756.45 kB | 724.14 kB | 183.16 kB | 181.07 kB | victory.js -3.20 MB | 1.05 MB | 1.01 MB | 334.10 kB | 331.56 kB | echarts.js +3.20 MB | 1.05 MB | 1.01 MB | 335.29 kB | 331.56 kB | echarts.js -6.69 MB | 2.44 MB | 2.31 MB | 498.93 kB | 488.28 kB | antd.js +6.69 MB | 2.44 MB | 2.31 MB | 499.89 kB | 488.28 kB | antd.js -10.95 MB | 3.59 MB | 3.49 MB | 913.96 kB | 915.50 kB | typescript.js +10.95 MB | 3.58 MB | 3.49 MB | 916.58 kB | 915.50 kB | typescript.js