Skip to content

Commit

Permalink
feat(minifier): implement return statement dce
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Jul 10, 2024
1 parent b632c04 commit 96d14c8
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 36 deletions.
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;
}",
);
}

0 comments on commit 96d14c8

Please sign in to comment.