Skip to content

Commit 96d14c8

Browse files
committed
feat(minifier): implement return statement dce
1 parent b632c04 commit 96d14c8

File tree

2 files changed

+90
-36
lines changed

2 files changed

+90
-36
lines changed

crates/oxc_minifier/src/ast_passes/remove_dead_code.rs

+49-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use oxc_allocator::Allocator;
1+
use oxc_allocator::{Allocator, Vec};
22
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};
33
use oxc_span::SPAN;
44

@@ -12,6 +12,17 @@ pub struct RemoveDeadCode<'a> {
1212
folder: Folder<'a>,
1313
}
1414

15+
impl<'a> VisitMut<'a> for RemoveDeadCode<'a> {
16+
fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
17+
self.dead_code_elimintation(stmts);
18+
walk_mut::walk_statements(self, stmts);
19+
}
20+
21+
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
22+
self.fold_conditional_expression(expr);
23+
}
24+
}
25+
1526
impl<'a> RemoveDeadCode<'a> {
1627
pub fn new(allocator: &'a Allocator) -> Self {
1728
let ast = AstBuilder::new(allocator);
@@ -22,33 +33,61 @@ impl<'a> RemoveDeadCode<'a> {
2233
self.visit_program(program);
2334
}
2435

25-
fn test_expression(&mut self, expr: &mut Expression<'a>) -> Option<bool> {
26-
self.folder.fold_expression(expr);
27-
get_boolean_value(expr)
36+
/// Removes dead code thats comes after `return` statements after inlining `if` statements
37+
fn dead_code_elimintation(&mut self, stmts: &mut Vec<'a, Statement<'a>>) {
38+
let mut removed = true;
39+
for stmt in stmts.iter_mut() {
40+
if self.fold_if_statement(stmt) {
41+
removed = true;
42+
break;
43+
}
44+
}
45+
46+
if !removed {
47+
return;
48+
}
49+
50+
let mut index = None;
51+
for (i, stmt) in stmts.iter().enumerate() {
52+
if matches!(stmt, Statement::ReturnStatement(_)) {
53+
index.replace(i);
54+
}
55+
}
56+
if let Some(index) = index {
57+
stmts.drain(index + 1..);
58+
}
2859
}
2960

30-
pub fn remove_if(&mut self, stmt: &mut Statement<'a>) {
31-
let Statement::IfStatement(if_stmt) = stmt else { return };
32-
match self.test_expression(&mut if_stmt.test) {
61+
#[must_use]
62+
fn fold_if_statement(&mut self, stmt: &mut Statement<'a>) -> bool {
63+
let Statement::IfStatement(if_stmt) = stmt else { return false };
64+
match self.fold_expression_and_get_boolean_value(&mut if_stmt.test) {
3365
Some(true) => {
3466
*stmt = self.ast.move_statement(&mut if_stmt.consequent);
67+
true
3568
}
3669
Some(false) => {
3770
*stmt = if let Some(alternate) = &mut if_stmt.alternate {
3871
self.ast.move_statement(alternate)
3972
} else {
4073
self.ast.statement_empty(SPAN)
4174
};
75+
true
4276
}
43-
_ => {}
77+
_ => false,
4478
}
4579
}
4680

47-
pub fn remove_conditional(&mut self, expr: &mut Expression<'a>) {
81+
fn fold_expression_and_get_boolean_value(&mut self, expr: &mut Expression<'a>) -> Option<bool> {
82+
self.folder.fold_expression(expr);
83+
get_boolean_value(expr)
84+
}
85+
86+
fn fold_conditional_expression(&mut self, expr: &mut Expression<'a>) {
4887
let Expression::ConditionalExpression(conditional_expr) = expr else {
4988
return;
5089
};
51-
match self.test_expression(&mut conditional_expr.test) {
90+
match self.fold_expression_and_get_boolean_value(&mut conditional_expr.test) {
5291
Some(true) => {
5392
*expr = self.ast.move_expression(&mut conditional_expr.consequent);
5493
}
@@ -59,14 +98,3 @@ impl<'a> RemoveDeadCode<'a> {
5998
}
6099
}
61100
}
62-
63-
impl<'a> VisitMut<'a> for RemoveDeadCode<'a> {
64-
fn visit_statement(&mut self, stmt: &mut Statement<'a>) {
65-
self.remove_if(stmt);
66-
walk_mut::walk_statement(self, stmt);
67-
}
68-
69-
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
70-
self.remove_conditional(expr);
71-
}
72-
}

crates/oxc_minifier/tests/oxc/remove_dead_code.rs

+41-15
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,37 @@ use oxc_minifier::RemoveDeadCode;
44
use oxc_parser::Parser;
55
use oxc_span::SourceType;
66

7-
fn minify(source_text: &str) -> String {
7+
fn print(source_text: &str, remove_dead_code: bool) -> String {
88
let source_type = SourceType::default();
99
let allocator = Allocator::default();
1010
let ret = Parser::new(&allocator, source_text, source_type).parse();
1111
let program = allocator.alloc(ret.program);
12-
RemoveDeadCode::new(&allocator).build(program);
12+
if remove_dead_code {
13+
RemoveDeadCode::new(&allocator).build(program);
14+
}
1315
WhitespaceRemover::new().build(program).source_text
1416
}
1517

1618
pub(crate) fn test(source_text: &str, expected: &str) {
17-
let minified = minify(source_text);
19+
let minified = print(source_text, true);
20+
let expected = print(expected, false);
1821
assert_eq!(minified, expected, "for source {source_text}");
1922
}
2023

2124
#[test]
2225
fn remove_dead_code() {
23-
test("if (true) { foo }", "{foo}");
24-
test("if (true) { foo } else { bar }", "{foo}");
25-
test("if (false) { foo } else { bar }", "{bar}");
26+
test("if (true) { foo }", "{ foo }");
27+
test("if (true) { foo } else { bar }", "{ foo }");
28+
test("if (false) { foo } else { bar }", "{ bar }");
2629

27-
test("if (!false) { foo }", "{foo}");
28-
test("if (!true) { foo } else { bar }", "{bar}");
30+
test("if (!false) { foo }", "{ foo }");
31+
test("if (!true) { foo } else { bar }", "{ bar }");
2932

30-
test("if ('production' == 'production') { foo } else { bar }", "{foo}");
31-
test("if ('development' == 'production') { foo } else { bar }", "{bar}");
33+
test("if ('production' == 'production') { foo } else { bar }", "{ foo }");
34+
test("if ('development' == 'production') { foo } else { bar }", "{ bar }");
3235

33-
test("if ('production' === 'production') { foo } else { bar }", "{foo}");
34-
test("if ('development' === 'production') { foo } else { bar }", "{bar}");
36+
test("if ('production' === 'production') { foo } else { bar }", "{ foo }");
37+
test("if ('development' === 'production') { foo } else { bar }", "{ bar }");
3538

3639
test("false ? foo : bar;", "bar");
3740
test("true ? foo : bar;", "foo");
@@ -42,12 +45,35 @@ fn remove_dead_code() {
4245
test("!!false ? foo : bar;", "bar");
4346
test("!!true ? foo : bar;", "foo");
4447

45-
test("const foo = true ? A : B", "const foo=A");
46-
test("const foo = false ? A : B", "const foo=B");
48+
test("const foo = true ? A : B", "const foo = A");
49+
test("const foo = false ? A : B", "const foo = B");
4750

4851
// Shadowed `undefined` as a variable should not be erased.
4952
test(
5053
"function foo(undefined) { if (!undefined) { } }",
51-
"function foo(undefined){if(!undefined){}}",
54+
"function foo(undefined) { if (!undefined) { } }",
55+
);
56+
}
57+
58+
// https://github.com/terser/terser/blob/master/test/compress/dead-code.js
59+
#[test]
60+
fn remove_dead_code_from_terser() {
61+
test(
62+
"function f() {
63+
a();
64+
b();
65+
x = 10;
66+
return;
67+
if (x) {
68+
y();
69+
}
70+
}",
71+
"
72+
function f() {
73+
a();
74+
b();
75+
x = 10;
76+
return;
77+
}",
5278
);
5379
}

0 commit comments

Comments
 (0)