Skip to content
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
5 changes: 5 additions & 0 deletions crates/oxc_minifier/src/peephole/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
86 changes: 85 additions & 1 deletion crates/oxc_minifier/src/peephole/remove_dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<SymbolId>, 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<Expression<'a>> {
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)``;`
Expand Down Expand Up @@ -563,7 +630,10 @@ impl<'a> LatePeepholeOptimizations {
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeRemoveDeadCodeTest.java>
#[cfg(test)]
mod test {
use crate::tester::{test, test_same};
use crate::{
CompressOptions,
tester::{test, test_options, test_same},
};

#[test]
fn test_fold_block() {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 10 additions & 2 deletions crates/oxc_minifier/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_hash::FxHashMap;
use rustc_hash::{FxHashMap, FxHashSet};

use oxc_ecmascript::constant_evaluation::ConstantValue;
use oxc_semantic::SymbolId;
Expand All @@ -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<SymbolId, ConstantValue<'a>>,

/// Function declarations that are empty
pub empty_functions: FxHashSet<SymbolId>,
}

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(),
}
}
}
Loading