diff --git a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs index 10704436999cd..bcefb5008486d 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs @@ -1,4 +1,4 @@ -use oxc_allocator::Allocator; +use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut}; use oxc_span::SPAN; @@ -12,6 +12,17 @@ pub struct RemoveDeadCode<'a> { folder: Folder<'a>, } +impl<'a> VisitMut<'a> for RemoveDeadCode<'a> { + fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + self.dead_code_elimintation(stmts); + walk_mut::walk_statements(self, stmts); + } + + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + self.fold_conditional_expression(expr); + } +} + impl<'a> RemoveDeadCode<'a> { pub fn new(allocator: &'a Allocator) -> Self { let ast = AstBuilder::new(allocator); @@ -22,16 +33,38 @@ impl<'a> RemoveDeadCode<'a> { self.visit_program(program); } - fn test_expression(&mut self, expr: &mut Expression<'a>) -> Option { - self.folder.fold_expression(expr); - get_boolean_value(expr) + /// Removes dead code thats comes after `return` statements after inlining `if` statements + fn dead_code_elimintation(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + let mut removed = true; + for stmt in stmts.iter_mut() { + if self.fold_if_statement(stmt) { + removed = true; + break; + } + } + + if !removed { + return; + } + + let mut index = None; + for (i, stmt) in stmts.iter().enumerate() { + if matches!(stmt, Statement::ReturnStatement(_)) { + index.replace(i); + } + } + if let Some(index) = index { + stmts.drain(index + 1..); + } } - pub fn remove_if(&mut self, stmt: &mut Statement<'a>) { - let Statement::IfStatement(if_stmt) = stmt else { return }; - match self.test_expression(&mut if_stmt.test) { + #[must_use] + fn fold_if_statement(&mut self, stmt: &mut Statement<'a>) -> bool { + let Statement::IfStatement(if_stmt) = stmt else { return false }; + match self.fold_expression_and_get_boolean_value(&mut if_stmt.test) { Some(true) => { *stmt = self.ast.move_statement(&mut if_stmt.consequent); + true } Some(false) => { *stmt = if let Some(alternate) = &mut if_stmt.alternate { @@ -39,16 +72,22 @@ impl<'a> RemoveDeadCode<'a> { } else { self.ast.statement_empty(SPAN) }; + true } - _ => {} + _ => false, } } - pub fn remove_conditional(&mut self, expr: &mut Expression<'a>) { + fn fold_expression_and_get_boolean_value(&mut self, expr: &mut Expression<'a>) -> Option { + self.folder.fold_expression(expr); + get_boolean_value(expr) + } + + fn fold_conditional_expression(&mut self, expr: &mut Expression<'a>) { let Expression::ConditionalExpression(conditional_expr) = expr else { return; }; - match self.test_expression(&mut conditional_expr.test) { + match self.fold_expression_and_get_boolean_value(&mut conditional_expr.test) { Some(true) => { *expr = self.ast.move_expression(&mut conditional_expr.consequent); } @@ -59,14 +98,3 @@ impl<'a> RemoveDeadCode<'a> { } } } - -impl<'a> VisitMut<'a> for RemoveDeadCode<'a> { - fn visit_statement(&mut self, stmt: &mut Statement<'a>) { - self.remove_if(stmt); - walk_mut::walk_statement(self, stmt); - } - - fn visit_expression(&mut self, expr: &mut Expression<'a>) { - self.remove_conditional(expr); - } -} diff --git a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs index cb37879e5736a..f3009622b2dd1 100644 --- a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs +++ b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs @@ -4,34 +4,37 @@ use oxc_minifier::RemoveDeadCode; use oxc_parser::Parser; use oxc_span::SourceType; -fn minify(source_text: &str) -> String { +fn print(source_text: &str, remove_dead_code: bool) -> String { let source_type = SourceType::default(); let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); let program = allocator.alloc(ret.program); - RemoveDeadCode::new(&allocator).build(program); + if remove_dead_code { + RemoveDeadCode::new(&allocator).build(program); + } WhitespaceRemover::new().build(program).source_text } pub(crate) fn test(source_text: &str, expected: &str) { - let minified = minify(source_text); + let minified = print(source_text, true); + let expected = print(expected, false); assert_eq!(minified, expected, "for source {source_text}"); } #[test] fn remove_dead_code() { - test("if (true) { foo }", "{foo}"); - test("if (true) { foo } else { bar }", "{foo}"); - test("if (false) { foo } else { bar }", "{bar}"); + test("if (true) { foo }", "{ foo }"); + test("if (true) { foo } else { bar }", "{ foo }"); + test("if (false) { foo } else { bar }", "{ bar }"); - test("if (!false) { foo }", "{foo}"); - test("if (!true) { foo } else { bar }", "{bar}"); + test("if (!false) { foo }", "{ foo }"); + test("if (!true) { foo } else { bar }", "{ bar }"); - test("if ('production' == 'production') { foo } else { bar }", "{foo}"); - test("if ('development' == 'production') { foo } else { bar }", "{bar}"); + test("if ('production' == 'production') { foo } else { bar }", "{ foo }"); + test("if ('development' == 'production') { foo } else { bar }", "{ bar }"); - test("if ('production' === 'production') { foo } else { bar }", "{foo}"); - test("if ('development' === 'production') { foo } else { bar }", "{bar}"); + test("if ('production' === 'production') { foo } else { bar }", "{ foo }"); + test("if ('development' === 'production') { foo } else { bar }", "{ bar }"); test("false ? foo : bar;", "bar"); test("true ? foo : bar;", "foo"); @@ -42,12 +45,35 @@ fn remove_dead_code() { test("!!false ? foo : bar;", "bar"); test("!!true ? foo : bar;", "foo"); - test("const foo = true ? A : B", "const foo=A"); - test("const foo = false ? A : B", "const foo=B"); + test("const foo = true ? A : B", "const foo = A"); + test("const foo = false ? A : B", "const foo = B"); // Shadowed `undefined` as a variable should not be erased. test( "function foo(undefined) { if (!undefined) { } }", - "function foo(undefined){if(!undefined){}}", + "function foo(undefined) { if (!undefined) { } }", + ); +} + +// https://github.com/terser/terser/blob/master/test/compress/dead-code.js +#[test] +fn remove_dead_code_from_terser() { + test( + "function f() { + a(); + b(); + x = 10; + return; + if (x) { + y(); + } + }", + " + function f() { + a(); + b(); + x = 10; + return; + }", ); }