Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 49 additions & 21 deletions crates/oxc_minifier/src/ast_passes/remove_dead_code.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
Expand All @@ -22,33 +33,61 @@ impl<'a> RemoveDeadCode<'a> {
self.visit_program(program);
}

fn test_expression(&mut self, expr: &mut Expression<'a>) -> Option<bool> {
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 {
self.ast.move_statement(alternate)
} 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<bool> {
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);
}
Expand All @@ -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);
}
}
56 changes: 41 additions & 15 deletions crates/oxc_minifier/tests/oxc/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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;
}",
);
}