diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 164c4c714915c..fdcb2fca8c843 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -181,6 +181,11 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations { } } + fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + let ctx = &mut Ctx::new(ctx); + Self::keep_track_of_empty_functions(stmt, ctx); + } + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { if !self.is_prev_function_changed() { return; diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index 80d75ad1ca98c..792086a07385c 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -3,6 +3,7 @@ use oxc_ast::ast::*; use oxc_ast_visit::Visit; use oxc_ecmascript::{constant_evaluation::ConstantEvaluation, side_effects::MayHaveSideEffects}; use oxc_span::GetSpan; +use oxc_syntax::symbol::SymbolId; use oxc_traverse::Ancestor; use crate::{ctx::Ctx, keep_var::KeepVar}; @@ -53,6 +54,8 @@ impl<'a> PeepholeOptimizations { self.remove_unused_assignment_expression(expr, state, ctx); None } + Expression::CallExpression(call_expr) => self.remove_call_expression(call_expr, ctx), + _ => None, } { *expr = folded_expr; @@ -494,6 +497,70 @@ impl<'a> PeepholeOptimizations { None } + pub fn keep_track_of_empty_functions(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { + match stmt { + Statement::FunctionDeclaration(func) => { + if let Some(body) = &func.body { + if body.is_empty() { + let symbol_id = func.id.as_ref().and_then(|id| id.symbol_id.get()); + Self::save_empty_function(symbol_id, ctx); + } + } + } + Statement::VariableDeclaration(decl) => { + for d in &decl.declarations { + if d.init.as_ref().is_some_and(|e|matches!(e, Expression::ArrowFunctionExpression(arrow) if arrow.body.is_empty())) { + if let BindingPatternKind::BindingIdentifier(id) = &d.id.kind { + let symbol_id = id.symbol_id.get(); + Self::save_empty_function(symbol_id,ctx); + } + } + } + } + _ => {} + } + } + + fn save_empty_function(symbol_id: Option, ctx: &mut Ctx<'a, '_>) { + if let Some(symbol_id) = symbol_id { + if ctx.scoping().get_resolved_references(symbol_id).all(|r| r.flags().is_read_only()) { + ctx.state.empty_functions.insert(symbol_id); + } + } + } + + fn remove_call_expression( + &self, + call_expr: &mut CallExpression<'a>, + ctx: &mut Ctx<'a, '_>, + ) -> Option> { + if let Expression::Identifier(ident) = &call_expr.callee { + if let Some(reference_id) = ident.reference_id.get() { + if let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() { + if ctx.state.empty_functions.contains(&symbol_id) { + if call_expr.arguments.is_empty() { + return Some(ctx.ast.void_0(call_expr.span)); + } + let mut exprs = ctx.ast.vec(); + for arg in call_expr.arguments.drain(..) { + match arg { + Argument::SpreadElement(e) => { + exprs.push(e.unbox().argument); + } + match_expression!(Argument) => { + exprs.push(arg.into_expression()); + } + } + } + exprs.push(ctx.ast.void_0(call_expr.span)); + return Some(ctx.ast.expression_sequence(call_expr.span, exprs)); + } + } + } + } + None + } + /// Whether the indirect access should be kept. /// For example, `(0, foo.bar)()` should not be transformed to `foo.bar()`. /// Example case: `let o = { f() { assert.ok(this !== o); } }; (true && o.f)(); (true && o.f)``;` @@ -563,7 +630,10 @@ impl<'a> LatePeepholeOptimizations { /// #[cfg(test)] mod test { - use crate::tester::{test, test_same}; + use crate::{ + CompressOptions, + tester::{test, test_options, test_same}, + }; #[test] fn test_fold_block() { @@ -770,4 +840,18 @@ mod test { fn remove_constant_value() { test("const foo = false; if (foo) { console.log('foo') }", "const foo = !1;"); } + + #[test] + fn remove_empty_function() { + let options = CompressOptions::smallest(); + test_options("function foo() {} foo()", "", &options); + test_options("function foo() {} foo(); foo()", "", &options); + test_options("var foo = () => {}; foo()", "", &options); + test_options("var foo = () => {}; foo(a)", "a", &options); + test_options("var foo = () => {}; foo(a, b)", "a, b", &options); + test_options("var foo = () => {}; foo(...a, b)", "a, b", &options); + test_options("var foo = () => {}; foo(...a, ...b)", "a, b", &options); + test_options("var foo = () => {}; x = foo()", "x = void 0", &options); + test_options("var foo = () => {}; x = foo(a(), b())", "x = (a(), b(), void 0)", &options); + } } diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 84542ad210562..aee6b3e681171 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -953,7 +953,7 @@ mod test { treeshake: TreeShakeOptions { annotations: false, ..TreeShakeOptions::default() }, ..default_options() }; - test_same_options("function test() {} /* @__PURE__ */ test()", &options); + test_same_options("function test() { bar } /* @__PURE__ */ test()", &options); test_same_options("function test() {} /* @__PURE__ */ new test()", &options); let options = CompressOptions { diff --git a/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs b/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs index 8831f4c785b93..43589294d567a 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs @@ -115,7 +115,7 @@ mod test { fn remove_unused_function_declaration() { let options = CompressOptions::smallest(); test_options("function foo() {}", "", &options); - test_same_options("function foo() {} foo()", &options); + test_same_options("function foo() { bar } foo()", &options); test_same_options("export function foo() {} foo()", &options); } diff --git a/crates/oxc_minifier/src/state.rs b/crates/oxc_minifier/src/state.rs index fcc5a5a351b8f..2e27ecc3a1c42 100644 --- a/crates/oxc_minifier/src/state.rs +++ b/crates/oxc_minifier/src/state.rs @@ -1,4 +1,4 @@ -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use oxc_ecmascript::constant_evaluation::ConstantValue; use oxc_semantic::SymbolId; @@ -16,10 +16,18 @@ pub struct MinifierState<'a> { /// Values are saved during constant evaluation phase. /// Values are read during [oxc_ecmascript::is_global_reference::IsGlobalReference::get_constant_value_for_reference_id]. pub constant_values: FxHashMap>, + + /// Function declarations that are empty + pub empty_functions: FxHashSet, } impl MinifierState<'_> { pub fn new(source_type: SourceType, options: CompressOptions) -> Self { - Self { source_type, options, constant_values: FxHashMap::default() } + Self { + source_type, + options, + constant_values: FxHashMap::default(), + empty_functions: FxHashSet::default(), + } } }