Skip to content

Commit 998c67b

Browse files
committed
feat(minifier): remove no-op function call (oxc-project#12373)
Remove function calls where the function is empty: * `function foo() {} foo()` -> `` * `var foo = () => {}; foo() -> `` * `function foo() {} x = foo(a, b)` -> `x = (a, b, void 0)` closes oxc-project#11469
1 parent 8bae417 commit 998c67b

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
181181
}
182182
}
183183

184+
fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
185+
let ctx = &mut Ctx::new(ctx);
186+
Self::keep_track_of_empty_functions(stmt, ctx);
187+
}
188+
184189
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
185190
if !self.is_prev_function_changed() {
186191
return;

crates/oxc_minifier/src/peephole/remove_dead_code.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use oxc_ast::ast::*;
33
use oxc_ast_visit::Visit;
44
use oxc_ecmascript::{constant_evaluation::ConstantEvaluation, side_effects::MayHaveSideEffects};
55
use oxc_span::GetSpan;
6+
use oxc_syntax::symbol::SymbolId;
67
use oxc_traverse::Ancestor;
78

89
use crate::{ctx::Ctx, keep_var::KeepVar};
@@ -53,6 +54,8 @@ impl<'a> PeepholeOptimizations {
5354
self.remove_unused_assignment_expression(expr, state, ctx);
5455
None
5556
}
57+
Expression::CallExpression(call_expr) => self.remove_call_expression(call_expr, ctx),
58+
5659
_ => None,
5760
} {
5861
*expr = folded_expr;
@@ -494,6 +497,70 @@ impl<'a> PeepholeOptimizations {
494497
None
495498
}
496499

500+
pub fn keep_track_of_empty_functions(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) {
501+
match stmt {
502+
Statement::FunctionDeclaration(func) => {
503+
if let Some(body) = &func.body {
504+
if body.is_empty() {
505+
let symbol_id = func.id.as_ref().and_then(|id| id.symbol_id.get());
506+
Self::save_empty_function(symbol_id, ctx);
507+
}
508+
}
509+
}
510+
Statement::VariableDeclaration(decl) => {
511+
for d in &decl.declarations {
512+
if d.init.as_ref().is_some_and(|e|matches!(e, Expression::ArrowFunctionExpression(arrow) if arrow.body.is_empty())) {
513+
if let BindingPatternKind::BindingIdentifier(id) = &d.id.kind {
514+
let symbol_id = id.symbol_id.get();
515+
Self::save_empty_function(symbol_id,ctx);
516+
}
517+
}
518+
}
519+
}
520+
_ => {}
521+
}
522+
}
523+
524+
fn save_empty_function(symbol_id: Option<SymbolId>, ctx: &mut Ctx<'a, '_>) {
525+
if let Some(symbol_id) = symbol_id {
526+
if ctx.scoping().get_resolved_references(symbol_id).all(|r| r.flags().is_read_only()) {
527+
ctx.state.empty_functions.insert(symbol_id);
528+
}
529+
}
530+
}
531+
532+
fn remove_call_expression(
533+
&self,
534+
call_expr: &mut CallExpression<'a>,
535+
ctx: &mut Ctx<'a, '_>,
536+
) -> Option<Expression<'a>> {
537+
if let Expression::Identifier(ident) = &call_expr.callee {
538+
if let Some(reference_id) = ident.reference_id.get() {
539+
if let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() {
540+
if ctx.state.empty_functions.contains(&symbol_id) {
541+
if call_expr.arguments.is_empty() {
542+
return Some(ctx.ast.void_0(call_expr.span));
543+
}
544+
let mut exprs = ctx.ast.vec();
545+
for arg in call_expr.arguments.drain(..) {
546+
match arg {
547+
Argument::SpreadElement(e) => {
548+
exprs.push(e.unbox().argument);
549+
}
550+
match_expression!(Argument) => {
551+
exprs.push(arg.into_expression());
552+
}
553+
}
554+
}
555+
exprs.push(ctx.ast.void_0(call_expr.span));
556+
return Some(ctx.ast.expression_sequence(call_expr.span, exprs));
557+
}
558+
}
559+
}
560+
}
561+
None
562+
}
563+
497564
/// Whether the indirect access should be kept.
498565
/// For example, `(0, foo.bar)()` should not be transformed to `foo.bar()`.
499566
/// Example case: `let o = { f() { assert.ok(this !== o); } }; (true && o.f)(); (true && o.f)``;`
@@ -563,7 +630,10 @@ impl<'a> LatePeepholeOptimizations {
563630
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeRemoveDeadCodeTest.java>
564631
#[cfg(test)]
565632
mod test {
566-
use crate::tester::{test, test_same};
633+
use crate::{
634+
CompressOptions,
635+
tester::{test, test_options, test_same},
636+
};
567637

568638
#[test]
569639
fn test_fold_block() {
@@ -770,4 +840,18 @@ mod test {
770840
fn remove_constant_value() {
771841
test("const foo = false; if (foo) { console.log('foo') }", "const foo = !1;");
772842
}
843+
844+
#[test]
845+
fn remove_empty_function() {
846+
let options = CompressOptions::smallest();
847+
test_options("function foo() {} foo()", "", &options);
848+
test_options("function foo() {} foo(); foo()", "", &options);
849+
test_options("var foo = () => {}; foo()", "", &options);
850+
test_options("var foo = () => {}; foo(a)", "a", &options);
851+
test_options("var foo = () => {}; foo(a, b)", "a, b", &options);
852+
test_options("var foo = () => {}; foo(...a, b)", "a, b", &options);
853+
test_options("var foo = () => {}; foo(...a, ...b)", "a, b", &options);
854+
test_options("var foo = () => {}; x = foo()", "x = void 0", &options);
855+
test_options("var foo = () => {}; x = foo(a(), b())", "x = (a(), b(), void 0)", &options);
856+
}
773857
}

crates/oxc_minifier/src/peephole/remove_unused_expression.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ mod test {
953953
treeshake: TreeShakeOptions { annotations: false, ..TreeShakeOptions::default() },
954954
..default_options()
955955
};
956-
test_same_options("function test() {} /* @__PURE__ */ test()", &options);
956+
test_same_options("function test() { bar } /* @__PURE__ */ test()", &options);
957957
test_same_options("function test() {} /* @__PURE__ */ new test()", &options);
958958

959959
let options = CompressOptions {

crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ mod test {
115115
fn remove_unused_function_declaration() {
116116
let options = CompressOptions::smallest();
117117
test_options("function foo() {}", "", &options);
118-
test_same_options("function foo() {} foo()", &options);
118+
test_same_options("function foo() { bar } foo()", &options);
119119
test_same_options("export function foo() {} foo()", &options);
120120
}
121121

crates/oxc_minifier/src/state.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rustc_hash::FxHashMap;
1+
use rustc_hash::{FxHashMap, FxHashSet};
22

33
use oxc_ecmascript::constant_evaluation::ConstantValue;
44
use oxc_semantic::SymbolId;
@@ -16,10 +16,18 @@ pub struct MinifierState<'a> {
1616
/// Values are saved during constant evaluation phase.
1717
/// Values are read during [oxc_ecmascript::is_global_reference::IsGlobalReference::get_constant_value_for_reference_id].
1818
pub constant_values: FxHashMap<SymbolId, ConstantValue<'a>>,
19+
20+
/// Function declarations that are empty
21+
pub empty_functions: FxHashSet<SymbolId>,
1922
}
2023

2124
impl MinifierState<'_> {
2225
pub fn new(source_type: SourceType, options: CompressOptions) -> Self {
23-
Self { source_type, options, constant_values: FxHashMap::default() }
26+
Self {
27+
source_type,
28+
options,
29+
constant_values: FxHashMap::default(),
30+
empty_functions: FxHashSet::default(),
31+
}
2432
}
2533
}

0 commit comments

Comments
 (0)