mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(minifier): implement return statement dce (#4155)
This isn't complete, I need to figure out `var` hoisting 🙃
This commit is contained in:
parent
03ad1e32cc
commit
44a894a2f9
2 changed files with 90 additions and 36 deletions
|
|
@ -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<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 {
|
||||
|
|
@ -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<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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue