diff --git a/crates/oxc_minifier/examples/minifier.rs b/crates/oxc_minifier/examples/minifier.rs index 37bb4c97faf63..0c437d0faec57 100644 --- a/crates/oxc_minifier/examples/minifier.rs +++ b/crates/oxc_minifier/examples/minifier.rs @@ -51,7 +51,7 @@ fn main() -> std::io::Result<()> { let mut allocator = Allocator::default(); let ret = minify(&allocator, &source_text, source_type, source_map_path, mangle, nospace); let printed = ret.code; - // println!("{printed}"); + println!("{printed}"); if let Some(source_map) = ret.map { let result = source_map.to_json_string(); diff --git a/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs b/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs index 71bc72b5fd783..071d36b35aa85 100644 --- a/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs +++ b/crates/oxc_minifier/src/peephole/minimize_expression_in_boolean_context.rs @@ -8,20 +8,6 @@ use crate::ctx::Ctx; use super::PeepholeOptimizations; impl<'a> PeepholeOptimizations { - pub fn try_fold_stmt_in_boolean_context(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { - let expr = match stmt { - Statement::IfStatement(s) => Some(&mut s.test), - Statement::WhileStatement(s) => Some(&mut s.test), - Statement::ForStatement(s) => s.test.as_mut(), - Statement::DoWhileStatement(s) => Some(&mut s.test), - _ => None, - }; - - if let Some(expr) = expr { - Self::try_fold_expr_in_boolean_context(expr, ctx); - } - } - /// Simplify syntax when we know it's used inside a boolean context, e.g. `if (boolean_context) {}`. /// /// `SimplifyBooleanExpr`: diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 60a780806dc69..0c4c1c7ed3727 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -129,15 +129,40 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations { } fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - let mut ctx = Ctx::new(ctx); - Self::try_fold_stmt_in_boolean_context(stmt, &mut ctx); - Self::remove_dead_code_exit_statement(stmt, &mut ctx); - if let Statement::IfStatement(if_stmt) = stmt { - if let Some(folded_stmt) = Self::try_minimize_if(if_stmt, &mut ctx) { - *stmt = folded_stmt; - ctx.state.changed = true; + let ctx = &mut Ctx::new(ctx); + match stmt { + Statement::BlockStatement(_) => Self::try_optimize_block(stmt, ctx), + Statement::IfStatement(s) => { + Self::try_fold_expr_in_boolean_context(&mut s.test, ctx); + Self::try_fold_if(stmt, ctx); + if let Statement::IfStatement(if_stmt) = stmt { + if let Some(folded_stmt) = Self::try_minimize_if(if_stmt, ctx) { + *stmt = folded_stmt; + ctx.state.changed = true; + } + } + } + Statement::WhileStatement(s) => { + Self::try_fold_expr_in_boolean_context(&mut s.test, ctx); + } + Statement::ForStatement(s) => { + if let Some(test) = &mut s.test { + Self::try_fold_expr_in_boolean_context(test, ctx); + } + Self::try_fold_for(stmt, ctx); + } + Statement::DoWhileStatement(s) => { + Self::try_fold_expr_in_boolean_context(&mut s.test, ctx); + } + Statement::TryStatement(_) => Self::try_fold_try(stmt, ctx), + Statement::LabeledStatement(_) => Self::try_fold_labeled(stmt, ctx), + Statement::FunctionDeclaration(_) => { + Self::remove_unused_function_declaration(stmt, ctx); } + Statement::ClassDeclaration(_) => Self::remove_unused_class_declaration(stmt, ctx), + _ => {} } + Self::try_fold_expression_stmt(stmt, ctx); } fn exit_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) { @@ -412,8 +437,24 @@ impl<'a> Traverse<'a, MinifierState<'a>> for DeadCodeElimination { } fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - let mut ctx = Ctx::new(ctx); - PeepholeOptimizations::remove_dead_code_exit_statement(stmt, &mut ctx); + let ctx = &mut Ctx::new(ctx); + match stmt { + Statement::BlockStatement(_) => PeepholeOptimizations::try_optimize_block(stmt, ctx), + Statement::IfStatement(_) => PeepholeOptimizations::try_fold_if(stmt, ctx), + Statement::ForStatement(_) => PeepholeOptimizations::try_fold_for(stmt, ctx), + Statement::TryStatement(_) => PeepholeOptimizations::try_fold_try(stmt, ctx), + Statement::LabeledStatement(_) => PeepholeOptimizations::try_fold_labeled(stmt, ctx), + Statement::FunctionDeclaration(_) => { + PeepholeOptimizations::remove_unused_function_declaration(stmt, ctx); + } + Statement::ClassDeclaration(_) => { + PeepholeOptimizations::remove_unused_class_declaration(stmt, ctx); + } + Statement::ExpressionStatement(_) => { + PeepholeOptimizations::try_fold_expression_stmt(stmt, ctx); + } + _ => {} + } } fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index a29b61662f8d5..f6253f707835d 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -16,31 +16,11 @@ use super::PeepholeOptimizations; /// See `KeepVar` at the end of this file for `var` hoisting logic. /// impl<'a> PeepholeOptimizations { - pub fn remove_dead_code_exit_statement(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { - if let Some(new_stmt) = match stmt { - Statement::BlockStatement(s) => Self::try_optimize_block(s, ctx), - Statement::IfStatement(s) => Self::try_fold_if(s, ctx), - Statement::ForStatement(s) => Self::try_fold_for(s, ctx), - Statement::TryStatement(s) => Self::try_fold_try(s, ctx), - Statement::LabeledStatement(s) => Self::try_fold_labeled(s, ctx), - Statement::FunctionDeclaration(f) => Self::remove_unused_function_declaration(f, ctx), - Statement::ClassDeclaration(c) => Self::remove_unused_class_declaration(c, ctx), - _ => None, - } { - *stmt = new_stmt; - ctx.state.changed = true; - } - - Self::try_fold_expression_stmt(stmt, ctx); - } - /// Remove block from single line blocks /// `{ block } -> block` - fn try_optimize_block( - stmt: &mut BlockStatement<'a>, - ctx: &Ctx<'a, '_>, - ) -> Option> { - match stmt.body.len() { + pub fn try_optimize_block(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::BlockStatement(s) = stmt else { return }; + match s.body.len() { 0 => { let parent = ctx.parent(); if parent.is_while_statement() @@ -52,35 +32,31 @@ impl<'a> PeepholeOptimizations { || parent.is_program() { // Remove the block if it is empty and the parent is a block statement. - return Some(ctx.ast.statement_empty(stmt.span)); + *stmt = ctx.ast.statement_empty(s.span); + ctx.state.changed = true; } - None } 1 => { - let s = &stmt.body[0]; - if matches!(s, Statement::VariableDeclaration(decl) if !decl.kind.is_var()) - || matches!(s, Statement::ClassDeclaration(_)) - || matches!(s, Statement::FunctionDeclaration(_)) + let first = &s.body[0]; + if matches!(first, Statement::VariableDeclaration(decl) if !decl.kind.is_var()) + || matches!(first, Statement::ClassDeclaration(_)) + || matches!(first, Statement::FunctionDeclaration(_)) { - return None; + return; } - Some(stmt.body.remove(0)) + *stmt = s.body.remove(0); + ctx.state.changed = true; } - _ => None, + _ => {} } } - fn try_fold_if(if_stmt: &mut IfStatement<'a>, ctx: &mut Ctx<'a, '_>) -> Option> { + pub fn try_fold_if(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::IfStatement(if_stmt) = stmt else { return }; // Descend and remove `else` blocks first. match &mut if_stmt.alternate { - Some(Statement::IfStatement(alternate)) => { - if let Some(new_stmt) = Self::try_fold_if(alternate, ctx) { - if matches!(new_stmt, Statement::EmptyStatement(_)) { - if_stmt.alternate = None; - } else { - if_stmt.alternate = Some(new_stmt); - } - } + Some(Statement::IfStatement(_)) => { + Self::try_fold_if(if_stmt.alternate.as_mut().unwrap(), ctx); } Some(Statement::BlockStatement(s)) if s.body.is_empty() => { if_stmt.alternate = None; @@ -119,7 +95,7 @@ impl<'a> PeepholeOptimizations { } else { if_stmt.consequent = var_stmt; } - return None; + return; } if test_has_side_effects { if !has_var_stmt { @@ -129,25 +105,21 @@ impl<'a> PeepholeOptimizations { if_stmt.consequent = ctx.ast.statement_empty(if_stmt.consequent.span()); } } - return None; + return; } - return Some(if boolean { + *stmt = if boolean { if_stmt.consequent.take_in(ctx.ast) + } else if let Some(alternate) = if_stmt.alternate.take() { + alternate } else { - if_stmt.alternate.as_mut().map_or_else( - || ctx.ast.statement_empty(if_stmt.span), - |alternate| alternate.take_in(ctx.ast), - ) - }); + ctx.ast.statement_empty(if_stmt.span) + }; + ctx.state.changed = true; } - None } - fn try_fold_for( - for_stmt: &mut ForStatement<'a>, - - ctx: &mut Ctx<'a, '_>, - ) -> Option> { + pub fn try_fold_for(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::ForStatement(for_stmt) = stmt else { return }; if let Some(init) = &mut for_stmt.init { if let Some(init) = init.as_expression_mut() { if Self::remove_unused_expression(init, ctx) { @@ -166,14 +138,18 @@ impl<'a> PeepholeOptimizations { let test_boolean = for_stmt.test.as_ref().and_then(|test| test.evaluate_value_to_boolean(ctx)); if for_stmt.test.as_ref().is_some_and(|test| test.may_have_side_effects(ctx)) { - return None; + return; } match test_boolean { - Some(false) => match &mut for_stmt.init { - Some(ForStatementInit::VariableDeclaration(var_init)) => { + Some(false) => match &for_stmt.init { + Some(ForStatementInit::VariableDeclaration(_)) => { let mut keep_var = KeepVar::new(ctx.ast); keep_var.visit_statement(&for_stmt.body); let mut var_decl = keep_var.get_variable_declaration(); + let Some(ForStatementInit::VariableDeclaration(var_init)) = &mut for_stmt.init + else { + return; + }; if var_init.kind.is_var() { if let Some(var_decl) = &mut var_decl { var_decl @@ -183,28 +159,29 @@ impl<'a> PeepholeOptimizations { var_decl = Some(var_init.take_in_box(ctx.ast)); } } - Some(var_decl.map_or_else( + *stmt = var_decl.map_or_else( || ctx.ast.statement_empty(for_stmt.span), Statement::VariableDeclaration, - )) + ); + ctx.state.changed = true; } None => { let mut keep_var = KeepVar::new(ctx.ast); keep_var.visit_statement(&for_stmt.body); - Some(keep_var.get_variable_declaration().map_or_else( + *stmt = keep_var.get_variable_declaration().map_or_else( || ctx.ast.statement_empty(for_stmt.span), Statement::VariableDeclaration, - )) + ); + ctx.state.changed = true; } - _ => None, + _ => {} }, Some(true) => { // Remove the test expression. for_stmt.test = None; ctx.state.changed = true; - None } - None => None, + None => {} } } @@ -213,7 +190,8 @@ impl<'a> PeepholeOptimizations { /// ```js /// a: break a; /// ``` - fn try_fold_labeled(s: &mut LabeledStatement<'a>, ctx: &Ctx<'a, '_>) -> Option> { + pub fn try_fold_labeled(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::LabeledStatement(s) = stmt else { return }; let id = s.label.name.as_str(); // Check the first statement in the block, or just the `break [id] ` statement. // Check if we need to remove the whole block. @@ -222,17 +200,20 @@ impl<'a> PeepholeOptimizations { if break_stmt.label.as_ref().is_some_and(|l| l.name.as_str() == id) => {} Statement::BlockStatement(block) if block.body.first().is_some_and(|first| matches!(first, Statement::BreakStatement(break_stmt) if break_stmt.label.as_ref().is_some_and(|l| l.name.as_str() == id))) => {} Statement::EmptyStatement(_) => { - return Some(ctx.ast.statement_empty(s.span)) + *stmt = ctx.ast.statement_empty(s.span); + ctx.state.changed = true; + return; } - _ => return None, + _ => return } let mut var = KeepVar::new(ctx.ast); var.visit_statement(&s.body); let var_decl = var.get_variable_declaration_statement(); - var_decl.unwrap_or_else(|| ctx.ast.statement_empty(s.span)).into() + *stmt = var_decl.unwrap_or_else(|| ctx.ast.statement_empty(s.span)); + ctx.state.changed = true; } - fn try_fold_expression_stmt(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + pub fn try_fold_expression_stmt(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { let Statement::ExpressionStatement(expr_stmt) = stmt else { return }; // We need to check if it is in arrow function with `expression: true`. // This is the only scenario where we can't remove it even if `ExpressionStatement`. @@ -248,11 +229,13 @@ impl<'a> PeepholeOptimizations { } } - fn try_fold_try(s: &mut TryStatement<'a>, ctx: &Ctx<'a, '_>) -> Option> { - if let Some(handler) = &mut s.handler { + pub fn try_fold_try(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::TryStatement(s) = stmt else { return }; + if let Some(handler) = &s.handler { if s.block.body.is_empty() { let mut var = KeepVar::new(ctx.ast); var.visit_block_statement(&handler.body); + let Some(handler) = &mut s.handler else { return }; handler.body.body.clear(); if let Some(var_decl) = var.get_variable_declaration_statement() { handler.body.body.push(var_decl); @@ -269,15 +252,14 @@ impl<'a> PeepholeOptimizations { if s.block.body.is_empty() && s.handler.as_ref().is_none_or(|handler| handler.body.body.is_empty()) { - if let Some(finalizer) = &mut s.finalizer { + *stmt = if let Some(finalizer) = &mut s.finalizer { let mut block = ctx.ast.block_statement(finalizer.span, ctx.ast.vec()); std::mem::swap(&mut **finalizer, &mut block); - Some(Statement::BlockStatement(ctx.ast.alloc(block))) + Statement::BlockStatement(ctx.ast.alloc(block)) } else { - Some(ctx.ast.statement_empty(s.span)) - } - } else { - None + ctx.ast.statement_empty(s.span) + }; + ctx.state.changed = true; } } diff --git a/crates/oxc_minifier/src/peephole/remove_unused_declaration.rs b/crates/oxc_minifier/src/peephole/remove_unused_declaration.rs index 685b015075eb6..427322058334f 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_declaration.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_declaration.rs @@ -27,47 +27,47 @@ impl<'a> PeepholeOptimizations { false } - pub fn remove_unused_function_declaration( - f: &Function<'a>, - ctx: &Ctx<'a, '_>, - ) -> Option> { + pub fn remove_unused_function_declaration(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::FunctionDeclaration(f) = stmt else { return }; if ctx.state.options.unused == CompressOptionsUnused::Keep { - return None; + return; } + let Some(id) = &f.id else { return }; + let Some(symbol_id) = id.symbol_id.get() else { return }; if Self::keep_top_level_var_in_script_mode(ctx) { - return None; + return; } - let id = f.id.as_ref()?; - let symbol_id = id.symbol_id.get()?; if !ctx.scoping().symbol_is_unused(symbol_id) { - return None; + return; } - Some(ctx.ast.statement_empty(f.span)) + *stmt = ctx.ast.statement_empty(f.span); + ctx.state.changed = true; } - pub fn remove_unused_class_declaration( - c: &mut Class<'a>, - ctx: &mut Ctx<'a, '_>, - ) -> Option> { + pub fn remove_unused_class_declaration(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + let Statement::ClassDeclaration(c) = stmt else { return }; if ctx.state.options.unused == CompressOptionsUnused::Keep { - return None; + return; } + let Some(id) = &c.id else { return }; + let Some(symbol_id) = id.symbol_id.get() else { return }; if Self::keep_top_level_var_in_script_mode(ctx) { - return None; + return; } - let id = c.id.as_ref()?; - let symbol_id = id.symbol_id.get()?; if !ctx.scoping().symbol_is_unused(symbol_id) { - return None; + return; } - Self::remove_unused_class(c, ctx).map(|exprs| { + if let Some(changed) = Self::remove_unused_class(c, ctx).map(|exprs| { if exprs.is_empty() { ctx.ast.statement_empty(c.span) } else { let expr = ctx.ast.expression_sequence(c.span, exprs); ctx.ast.statement_expression(c.span, expr) } - }) + }) { + *stmt = changed; + ctx.state.changed = true; + } } /// Do remove top level vars in script mode. diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index cc57bf6d454d3..988c5f036e56e 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -9,7 +9,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi 342.15 kB | 117.22 kB | 118.14 kB | 43.27 kB | 44.37 kB | 2 | vue.js -544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | 2 | lodash.js +544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | 1 | lodash.js 555.77 kB | 270.87 kB | 270.13 kB | 88.23 kB | 90.80 kB | 2 | d3.js