fix(minifier): do not remove non-plain empty functions (#12573)

Do not remove empty function calls if the function is async or generator or args can cause type error.

From test262:

```js
function fn({}) {}

assert.throws(TypeError, function() {
  fn(null);
});
```
This commit is contained in:
Boshen committed 2025-07-28 12:54:43 +00:00
1 parent 3ba360e8e5
commit fe9c8e14b1
6 files changed
+83 -20

No files matched your search

@@ -357,7 +357,11 @@ impl<'a> MayHaveSideEffects<'a> for Class<'a> {
// Example cases: `class A extends 0 {}`, `class A extends (async function() {}) {}`
// Considering these cases is difficult and requires to de-opt most classes with a super class.
// To allow classes with a super class to be removed, we ignore this side effect.
if self.super_class.as_ref().is_some_and(|sup| sup.may_have_side_effects(ctx)) {
if self.super_class.as_ref().is_some_and(|sup| {
// `(class C extends (() => {}))` is TypeError.
matches!(sup.without_parentheses(), Expression::ArrowFunctionExpression(_))
|| sup.may_have_side_effects(ctx)
}) {
return true;
}
@@ -3,7 +3,6 @@ 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};
@@ -419,20 +418,45 @@ impl<'a> PeepholeOptimizations {
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::FunctionDeclaration(f) => {
if let Some(body) = &f.body {
Self::try_save_empty_function(
f.id.as_ref(),
&f.params,
body,
f.r#async,
f.generator,
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);
if let BindingPatternKind::BindingIdentifier(id) = &d.id.kind {
match &d.init {
Some(Expression::ArrowFunctionExpression(a)) => {
Self::try_save_empty_function(
Some(id),
&a.params,
&a.body,
a.r#async,
false,
ctx,
);
}
Some(Expression::FunctionExpression(f)) => {
if let Some(body) = &f.body {
Self::try_save_empty_function(
Some(id),
&f.params,
body,
f.r#async,
f.generator,
ctx,
);
}
}
_ => {}
}
}
}
@@ -441,11 +465,24 @@ impl<'a> PeepholeOptimizations {
}
}
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 try_save_empty_function(
id: Option<&BindingIdentifier<'a>>,
params: &FormalParameters<'a>,
body: &FunctionBody<'a>,
r#async: bool,
generator: bool,
ctx: &mut Ctx<'a, '_>,
) {
if !body.is_empty() || r#async || generator {
return;
}
// `function foo({}) {} foo(null)` is runtime type error.
if !params.items.iter().all(|pat| pat.pattern.kind.is_binding_identifier()) {
return;
}
let Some(symbol_id) = id.and_then(|id| id.symbol_id.get()) else { return };
if ctx.scoping().get_resolved_references(symbol_id).all(|r| r.flags().is_read_only()) {
ctx.state.empty_functions.insert(symbol_id);
}
}
@@ -552,7 +589,7 @@ impl<'a> LatePeepholeOptimizations {
mod test {
use crate::{
CompressOptions,
tester::{test, test_options, test_same},
tester::{test, test_options, test_same, test_same_options},
};
#[test]
@@ -773,5 +810,17 @@ mod test {
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);
test_options("var foo = function () {}; foo()", "", &options);
test_same_options("function foo({}) {} foo()", &options);
test_same_options("var foo = ({}) => {}; foo()", &options);
test_same_options("var foo = function ({}) {}; foo()", &options);
test_same_options("async function foo({}) {} foo()", &options);
test_same_options("var foo = async ({}) => {}; foo()", &options);
test_same_options("var foo = async function ({}) {}; foo()", &options);
test_same_options("function* foo({}) {} foo()", &options);
test_same_options("var foo = function*({}) {}; foo()", &options);
}
}
@@ -160,6 +160,12 @@ mod test {
test_options("var x = foo", "foo", &options);
test_same_options("var x; foo(x)", &options);
test_same_options("export var x", &options);
// TypeError
test_options(
"var x = class extends (() => {}) {}",
"(class extends (() => {}) {})",
&options,
);
}
#[test]
@@ -642,6 +642,7 @@ fn test_class_expression() {
test("(@foo class {})", true);
test("(class extends a {})", false); // this may have a side effect, but ignored by the assumption
test("(class extends foo() {})", true);
test("(class extends (() => {}) {})", true);
test("(class { static {} })", false);
test("(class { static { foo(); } })", true);
test("(class { a() {} })", false);
+4 -1
View File
@@ -2,7 +2,7 @@ commit: 4b5d36ab
runtime Summary:
AST Parsed : 18254/18254 (100.00%)
Positive Passed: 17310/18254 (94.83%)
Positive Passed: 17309/18254 (94.82%)
codegen Error: tasks/coverage/test262/test/language/eval-code/direct/var-env-func-init-global-update-configurable.js
Test262Error: obj['f'] descriptor should be enumerable; obj['f'] descriptor should be writable
@@ -2184,6 +2184,9 @@ Test262Error: Expected SameValue(«"_"», «"#field"») to be true
transform Error: tasks/coverage/test262/test/language/statements/class/elements/static-field-init-with-this.js
Test262Error: Expected SameValue(«"undefinedtest"», «"test262test"») to be true
minify Error: tasks/coverage/test262/test/language/statements/class/name-binding/in-extends-expression-assigned.js
Test262Error: Expected a ReferenceError to be thrown but no exception was thrown at all
minify Error: tasks/coverage/test262/test/language/statements/class/name-binding/in-extends-expression-grouped.js
Test262Error: Expected a ReferenceError to be thrown but no exception was thrown at all
+1 -1
View File
@@ -201,7 +201,7 @@ impl Test262RuntimeCase {
};
let mut text = Codegen::new()
.with_options(CodegenOptions { minify, ..CodegenOptions::default() })
.with_options(if minify { CodegenOptions::minify() } else { CodegenOptions::default() })
.with_scoping(symbol_table)
.build(&program)
.code;