mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user