mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-24 18:57:26 +00:00
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:
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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user