From 54cd04aead1d5c9ecd11fc5da61a4e03e9d56823 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:03:21 +0000 Subject: [PATCH] feat(minifier): implement dce with var hoisting (#4160) --- .../src/ast_passes/remove_dead_code.rs | 106 +++++++++++++++--- .../tests/oxc/remove_dead_code.rs | 62 +++++++++- 2 files changed, 148 insertions(+), 20 deletions(-) diff --git a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs index bcefb5008486d..22b810c132016 100644 --- a/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/src/ast_passes/remove_dead_code.rs @@ -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>, @@ -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); } } @@ -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 } @@ -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) { + /* 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> { + 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::>(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) + } +} diff --git a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs index f3009622b2dd1..2d8607a6034cd 100644 --- a/crates/oxc_minifier/tests/oxc/remove_dead_code.rs +++ b/crates/oxc_minifier/tests/oxc/remove_dead_code.rs @@ -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; @@ -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) { @@ -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 @@ -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); + ", + ); }