Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minifier): implement dce with var hoisting #4160

Merged
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
106 changes: 90 additions & 16 deletions crates/oxc_minifier/src/ast_passes/remove_dead_code.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use oxc_allocator::{Allocator, Vec};
use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut};
use oxc_span::SPAN;
use oxc_ast::{
ast::*, syntax_directed_operations::BoundNames, visit::walk_mut, AstBuilder, Visit, VisitMut,
};
use oxc_span::{Atom, Span, SPAN};
use oxc_syntax::scope::ScopeFlags;

use crate::{compressor::ast_util::get_boolean_value, folder::Folder};

/// Remove Dead Code from the AST.
///
/// Terser option: `dead_code: true`.
///
/// See `KeepVar` at the end of this file for `var` hoisting logic.
pub struct RemoveDeadCode<'a> {
ast: AstBuilder<'a>,
folder: Folder<'a>,
Expand Down Expand Up @@ -35,26 +40,41 @@ impl<'a> RemoveDeadCode<'a> {

/// 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;
// Fold if statements
for stmt in stmts.iter_mut() {
if self.fold_if_statement(stmt) {
removed = true;
break;
}
}

if !removed {
return;
if self.fold_if_statement(stmt) {}
}

// Remove code after `return` and `throw` statements
let mut index = None;
for (i, stmt) in stmts.iter().enumerate() {
if matches!(stmt, Statement::ReturnStatement(_)) {
'outer: for (i, stmt) in stmts.iter().enumerate() {
if matches!(stmt, Statement::ReturnStatement(_) | Statement::ThrowStatement(_)) {
index.replace(i);
break;
}
// Double check block statements folded by if statements above
if let Statement::BlockStatement(block_stmt) = stmt {
for stmt in &block_stmt.body {
if matches!(stmt, Statement::ReturnStatement(_) | Statement::ThrowStatement(_))
{
index.replace(i);
break 'outer;
}
}
}
}
if let Some(index) = index {
stmts.drain(index + 1..);

let Some(index) = index else { return };

let mut keep_var = KeepVar::new(self.ast);

for stmt in stmts.iter().skip(index) {
keep_var.visit_statement(stmt);
}

stmts.drain(index + 1..);
if let Some(stmt) = keep_var.get_variable_declaration_statement() {
stmts.push(stmt);
}
}

Expand All @@ -70,7 +90,14 @@ impl<'a> RemoveDeadCode<'a> {
*stmt = if let Some(alternate) = &mut if_stmt.alternate {
self.ast.move_statement(alternate)
} else {
self.ast.statement_empty(SPAN)
// Keep hoisted `vars` from the consequent block.
let mut keep_var = KeepVar::new(self.ast);
keep_var.visit_statement(&if_stmt.consequent);
if let Some(stmt) = keep_var.get_variable_declaration_statement() {
stmt
} else {
self.ast.statement_empty(SPAN)
}
};
true
}
Expand Down Expand Up @@ -98,3 +125,50 @@ impl<'a> RemoveDeadCode<'a> {
}
}
}

struct KeepVar<'a> {
ast: AstBuilder<'a>,
vars: std::vec::Vec<(Atom<'a>, Span)>,
}

impl<'a> Visit<'a> for KeepVar<'a> {
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
if decl.kind.is_var() {
decl.id.bound_names(&mut |ident| {
self.vars.push((ident.name.clone(), ident.span));
});
}
}

fn visit_function(&mut self, _it: &Function<'a>, _flags: Option<ScopeFlags>) {
/* skip functions */
}

fn visit_class(&mut self, _it: &Class<'a>) {
/* skip classes */
}
}

impl<'a> KeepVar<'a> {
fn new(ast: AstBuilder<'a>) -> Self {
Self { ast, vars: std::vec![] }
}

fn get_variable_declaration_statement(self) -> Option<Statement<'a>> {
if self.vars.is_empty() {
return None;
}

let kind = VariableDeclarationKind::Var;
let decls = self.ast.vec_from_iter(self.vars.into_iter().map(|(name, span)| {
let binding_kind = self.ast.binding_pattern_kind_binding_identifier(span, name);
let id =
self.ast.binding_pattern::<Option<TSTypeAnnotation>>(binding_kind, None, false);
self.ast.variable_declarator(span, kind, id, None, false)
}));

let decl = self.ast.variable_declaration(SPAN, kind, decls, false);
let stmt = self.ast.statement_declaration(self.ast.declaration_from_variable(decl));
Some(stmt)
}
}
62 changes: 58 additions & 4 deletions crates/oxc_minifier/tests/oxc/remove_dead_code.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use oxc_allocator::Allocator;
use oxc_codegen::WhitespaceRemover;
use oxc_codegen::CodeGenerator;
use oxc_minifier::RemoveDeadCode;
use oxc_parser::Parser;
use oxc_span::SourceType;
Expand All @@ -12,7 +12,7 @@ fn print(source_text: &str, remove_dead_code: bool) -> String {
if remove_dead_code {
RemoveDeadCode::new(&allocator).build(program);
}
WhitespaceRemover::new().build(program).source_text
CodeGenerator::new().build(program).source_text
}

pub(crate) fn test(source_text: &str, expected: &str) {
Expand Down Expand Up @@ -53,6 +53,18 @@ fn remove_dead_code() {
"function foo(undefined) { if (!undefined) { } }",
"function foo(undefined) { if (!undefined) { } }",
);

test("if (true) { foo; } if (true) { foo; }", "{ foo; } { foo; }");

test(
"
if (true) { foo; return }
foo;
if (true) { bar; return }
bar;
",
"{foo; return }",
);
}

// https://github.com/terser/terser/blob/master/test/compress/dead-code.js
Expand All @@ -68,12 +80,54 @@ fn remove_dead_code_from_terser() {
y();
}
}",
"
function f() {
"function f() {
a();
b();
x = 10;
return;
}",
);

// NOTE: `if (x)` is changed to `if (true)` because const inlining is not implemented yet.
test(
r#"function f() {
g();
x = 10;
throw new Error("foo");
if (true) {
y();
var x;
function g(){};
(function(){
var q;
function y(){};
})();
}
}
f();
"#,
r#"function f() {
g();
x = 10;
throw new Error("foo");
var x;
}
f();
"#,
);

test(
"if (0) {
let foo = 6;
const bar = 12;
class Baz {};
var qux;
}
console.log(foo, bar, Baz);
",
"
var qux;
console.log(foo, bar, Baz);
",
);
}
Loading