feat(minifier): add folding of delete expression (#27522)

Fold delete expression to true/false if possible

in minifier there are 2 types of NaN, identifier and Number, `delete
Number` even if its a nan or infinity should produce true, and delete on
global unmutable variable should return false

context:
while checking additional cases i noticed that `delete Math.sqrt(-1)`,
`delete Number('a')` where still folded to incorrect syntax, and there
is so many of them that folding them at source seem like a better idea,
we should also update codegen when printing NaN and Infinity Number if
parent is delete, and in that case print it as `(1/0, 0/0)`

follow up on #27466
This commit is contained in:
Armano authored and GitHub committed 2026-10-10 10:43:25 +08:00
1 parent 7d0ee4825b
commit 3d00bbd2c1
6 files changed
+117 -55

No files matched your search

@@ -6,6 +6,7 @@ use oxc_ecmascript::{
side_effects::MayHaveSideEffects,
with_number_literal,
};
use oxc_semantic::IsGlobalReference;
use oxc_span::{GetSpan, SPAN};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};
@@ -28,12 +29,39 @@ impl<'a> PeepholeOptimizations {
{}
// Do not fold big int.
UnaryOperator::UnaryNegation if e.argument.is_big_int_literal() => {}
UnaryOperator::Delete => {
let new_value = match &e.argument {
Expression::Identifier(ident)
if matches!(ident.name.as_str(), "undefined" | "Infinity" | "NaN")
&& ident.is_global_reference(ctx.scoping()) =>
{
false
}
Expression::SequenceExpression(seq) if seq.expressions.len() > 1 => true,
Expression::ComputedMemberExpression(_)
| Expression::ChainExpression(_)
| Expression::PrivateFieldExpression(_)
| Expression::StaticMemberExpression(_)
| Expression::Identifier(_)
| Expression::SequenceExpression(_) => {
return;
}
_ => true,
};
let new_expr = Expression::new_boolean_literal(e.span, new_value, ctx);
if Self::remove_unused_expression(&mut e.argument, ctx) {
ctx.drop_expression(&e.argument);
ctx.replace_expression(expr, new_expr);
} else {
ctx.replace_expression_with(expr, Self::unwrap_unary);
ctx.replace_expression_with(expr, |e, ctx| {
Self::join_sequence(e, new_expr, ctx)
});
}
}
_ if e.may_have_side_effects(ctx) => {}
_ => {
if let Some(mut changed) =
e.evaluate_value(ctx).map(|v| ctx.value_to_expr(e.span, v))
{
Self::normalize_non_finite_number_for_delete(&mut changed, ctx);
if let Some(changed) = e.evaluate_value(ctx).map(|v| ctx.value_to_expr(e.span, v)) {
ctx.replace_expression(expr, changed);
}
}
@@ -284,6 +312,7 @@ impl<'a> PeepholeOptimizations {
if e.operator == BinaryOperator::Division
&& matches!(&e.right, Expression::NumericLiteral(r) if r.value == 0.0 && r.value.is_sign_positive())
&& matches!(&e.left, Expression::NumericLiteral(l) if l.value.abs() == 1.0)
&& !matches!(&ctx.parent(), Ancestor::UnaryExpressionArgument(unary) if unary.operator().is_delete())
{
return;
}
@@ -364,7 +393,14 @@ impl<'a> PeepholeOptimizations {
BinaryOperator::Division => Self::try_fold_safe_integer_numeric_expression(e, ctx)
.or_else(|| {
Self::extract_numeric_values(e, ctx)
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
.filter(|(left, right)| {
*left == 0.0
|| left.is_nan()
|| left.is_infinite()
|| *right == 0.0
|| right.is_nan()
|| right.is_infinite()
})
.and_then(|_| ctx.eval_binary(e))
}),
BinaryOperator::ShiftLeft => {
@@ -389,8 +425,7 @@ impl<'a> PeepholeOptimizations {
}
BinaryOperator::In => None,
};
if let Some(mut changed) = changed {
Self::normalize_non_finite_number_for_delete(&mut changed, ctx);
if let Some(changed) = changed {
ctx.replace_expression(expr, changed);
}
}
@@ -1023,15 +1058,6 @@ impl<'a> PeepholeOptimizations {
}
}
}
fn normalize_non_finite_number_for_delete(expr: &mut Expression<'a>, ctx: &TraverseCtx<'a>) {
if let Expression::NumericLiteral(num_expr) = expr
&& (num_expr.value.is_nan() || num_expr.value.is_infinite())
&& matches!(&ctx.parent(), Ancestor::UnaryExpressionArgument(unary) if unary.operator().is_delete())
{
num_expr.value = 0.0;
}
}
}
/// Outcome of folding an optional-chain at the deepest optional position.
+19 -19
View File
@@ -244,7 +244,7 @@ fn js_parser_test() {
test("let x = undefined", "let x;");
test("var x = undefined", "var x = void 0;");
test("function foo(a) { if (!a) return undefined; a() }", "function foo(a) { a && a(); }");
test("delete undefined", "delete undefined;");
test("delete undefined", "");
test("undefined--", "undefined--;");
test("undefined++", "undefined++;");
test("--undefined", "--undefined;");
@@ -1407,7 +1407,7 @@ fn test_flatten_values() {
test("return !(a, b)", "return a, !b;");
test("return void (a, b)", "a, b; return;");
test("return typeof (a, b)", "return typeof (a, b);");
test("return delete (a, b)", "return delete (a, b);");
test("return delete (a, b)", "return a, b, !0;");
test("return (a, b) && c", "return a, b && c;");
test("return (a, b) == c", "return a, b == c;");
test("return (a, b) + c", "return a, b + c;");
@@ -1587,23 +1587,23 @@ fn test_remove_dead_expr() {
test("delete (x[y])", "delete x[y];");
test("delete (x?.y)", "delete x?.y;");
test("delete (x?.[y])", "delete x?.[y];");
test("delete (2, x)", "delete (0, x);");
test("delete (2, x); var x", "delete (0, x);var x;");
test("delete (2, x.y)", "delete (0, x.y);");
test("delete (2, x[y])", "delete (0, x[y]);");
test("delete (2, x?.y)", "delete (0, x?.y);");
test("delete (2, x?.[y])", "delete (0, x?.[y]);");
test("delete (true && x)", "delete (0, x);");
test("delete (false || x)", "delete (0, x);");
test("delete (null ?? x)", "delete (0, x);");
test("delete (1 ? x : 2)", "delete (0, x);");
test("delete (0 ? 1 : x)", "delete (0, x);");
test("delete (NaN)", "delete NaN;");
test("delete (Infinity)", "delete Infinity;");
test("delete (-Infinity)", "delete -Infinity;");
test("delete (1, NaN)", "delete (0, NaN);");
test("delete (1, Infinity)", "delete (0, Infinity);");
test("delete (1, -Infinity)", "delete -Infinity;");
test("delete (2, x)", "x;");
test("delete (2, x); var x", "var x;");
test("delete (2, x.y)", "x.y;");
test("delete (2, x[y])", "x[y];");
test("delete (2, x?.y)", "x?.y;");
test("delete (2, x?.[y])", "x?.[y];");
test("delete (true && x)", "x;");
test("delete (false || x)", "x;");
test("delete (null ?? x)", "x;");
test("delete (1 ? x : 2)", "x;");
test("delete (0 ? 1 : x)", "x;");
test("delete (NaN)", "");
test("delete (Infinity)", "");
test("delete (-Infinity)", "");
test("delete (1, NaN)", "");
test("delete (1, Infinity)", "");
test("delete (1, -Infinity)", "");
test("foo ? 1 : 2", "foo;");
test("foo ? 1 : bar", "foo || bar;");
test("foo ? bar : 2", "foo && bar;");
@@ -6,7 +6,6 @@ static NEG_MAX_SAFE_INT: i64 = -9_007_199_254_740_991_i64;
use crate::{test, test_same};
// wrap with a function call so it doesn't get removed.
fn fold(source_text: &str, expected: &str) {
let source_text = format!("NOOP({source_text})");
let expected = format!("NOOP({expected})");
@@ -1474,17 +1473,54 @@ fn test_issue_8782() {
}
#[test]
fn test_fold_delete_number() {
fold("delete (+'x')", "delete 0");
fold("delete (5 / 0)", "delete 0");
fold_same("delete (1 / 0)");
fold("delete (0 / -0)", "delete 0");
fold("delete (0 / 0)", "delete 0");
fold("delete (Infinity / Infinity)", "delete 0");
fold("delete (NaN / 0)", "delete 0");
fold("delete (1 / NaN)", "delete 0");
fold_same("delete (0, NaN)");
fold_same("delete (0, Infinity)");
fn test_fold_delete() {
fold("delete 0", "!0");
fold("delete 1", "!0");
fold("delete 1n", "!0");
fold("delete (+'x')", "!0");
fold("delete (1 / -1)", "!0");
fold("delete (5 / 0)", "!0");
fold("delete (1 / 0)", "!0");
fold("delete (0 / -0)", "!0");
fold("delete (-1 / 0)", "!0");
fold("delete (0 / 0)", "!0");
fold("delete (Infinity / Infinity)", "!0");
fold("delete (NaN / 0)", "!0");
fold("delete (1 / NaN)", "!0");
fold("delete undefined", "!1");
fold("delete NaN", "!1");
fold("delete Infinity", "!1");
fold("delete -Infinity", "!0");
fold("delete void 0", "!0");
fold("delete +a", "(+a, !0)");
fold("delete void a", "(a, !0)");
fold("delete Math.sqrt(-1)", "!0");
fold("delete Math.sqrt(0)", "!0");
fold("delete Number('a')", "!0");
fold("delete 'a'", "!0");
fold("delete (0, NaN)", "!0");
fold("delete (0, Infinity)", "!0");
fold("delete true", "!0");
fold("delete false", "!0");
fold("delete null", "!0");
fold("delete (0, x)", "(x, !0)");
fold("delete a()", "(a(), !0)");
fold("delete new a()", "(new a(), !0)");
fold("delete a.b()", "(a.b(), !0)");
fold_same("delete a");
fold_same("delete a().b");
fold_same("delete a()?.b");
fold_same("delete new a().b");
fold_same("delete new a()?.b");
fold_same("delete a[0]");
fold_same("delete a?.[0]");
fold_same("delete a.b");
fold_same("delete a?.b");
fold_same("delete a?.b()");
fold_same("function(NaN){ return delete NaN; }");
}
#[test]
@@ -235,7 +235,7 @@ fn remove_unused_expressions_in_sequence() {
// already-`0` element re-records a mutation every iteration, spinning
// the fixed-point loop into the 10-iteration debug_assert.
test_same("(sideEffect(), 0, foo.bar)();");
test_same("delete (sideEffect(), 0, foo.bar);");
test("delete (sideEffect(), 0, foo.bar);", "sideEffect(), foo.bar;");
test("typeof (0, foo);", "foo");
test_same("v = typeof (0, foo);");
@@ -243,11 +243,11 @@ fn remove_unused_expressions_in_sequence() {
test("v = function(foo) { return typeof (0, foo) }", "v = function(foo) { return typeof foo }");
test("typeof 0", "");
test_same("delete (0, foo);");
test_same("delete (0, foo.#bar);");
test_same("delete (0, foo.bar);");
test_same("delete (0, foo[bar]);");
test_same("delete (0, foo?.bar);");
test("delete (0, foo);", "foo");
test("delete (0, foo.#bar);", "foo.#bar");
test("delete (0, foo.bar);", "foo.bar");
test("delete (0, foo[bar]);", "foo[bar]");
test("delete (0, foo?.bar);", "foo?.bar");
}
#[test]
@@ -255,7 +255,7 @@ fn test_fold_unary_expression_statement() {
test_same("delete x");
test_same("delete x.y");
test_same("delete x.y.z()");
test("delete x.y.z()", "x.y.z()");
test_same("+0n"); // Uncaught TypeError: Cannot convert a BigInt value to a number
test("-0n", "");
test("-1n", "");
@@ -54,7 +54,7 @@ fn test_undefined() {
test_same("var [] = void 0");
// `delete undefined` returns `false`
// `delete void 0` returns `true`
test_same("delete undefined");
test("delete undefined", "");
}
#[test]