From dea41dc132da8d4990d7eca5dd42e32f3a54b761 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:26:25 +0000 Subject: [PATCH] feat(minifier): compress `Object(expr)(args)` to `(0, expr)(args)` (#13092) Compress `Object(expr)(args)` to `(0, expr)(args)`. This can be done because - the function call throws an error if the callee is not a function. - `Object(expr)` would output a function if and only if `expr` is a function. --- **References** - [spec of `Object()`](https://tc39.es/ecma262/2025/multipage/fundamental-objects.html#sec-object-value) --- .../peephole/substitute_alternate_syntax.rs | 54 +++++++++++++++++++ tasks/minsize/minsize.snap | 4 +- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 40f16b059cd99..867787660fc77 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -131,6 +131,7 @@ impl<'a> PeepholeOptimizations { pub fn substitute_call_expression(expr: &mut CallExpression<'a>, ctx: &mut Ctx<'a, '_>) { Self::try_flatten_arguments(&mut expr.arguments, ctx); + Self::try_rewrite_object_callee_indirect_call(expr, ctx); } pub fn substitute_new_expression(expr: &mut NewExpression<'a>, ctx: &mut Ctx<'a, '_>) { @@ -885,6 +886,49 @@ impl<'a> PeepholeOptimizations { ctx.state.changed = true; } + /// `Object(expr)(args)` -> `(0, expr)(args)` + /// + /// If `expr` is `null` or `undefined`, both before and after throws an TypeError ("something is not a function"). + /// It is because `Object(expr)` returns `{}`. + /// + /// If `expr` is other primitive values, both before and after throws an TypeError ("something is not a function"). + /// It is because `Object(expr)` returns the Object wrapped values (e.g. `new Boolean()`). + /// + /// If `expr` is an object / function, `Object(expr)` returns `expr` as-is. + /// Note that we need to wrap `expr` as `(0, expr)` so that the `this` value is preserved. + /// + /// + fn try_rewrite_object_callee_indirect_call( + expr: &mut CallExpression<'a>, + ctx: &mut Ctx<'a, '_>, + ) { + let Expression::CallExpression(inner_call) = &mut expr.callee else { return }; + if inner_call.optional || inner_call.arguments.len() != 1 { + return; + } + let Expression::Identifier(callee) = &inner_call.callee else { + return; + }; + if callee.name != "Object" || !ctx.is_global_reference(callee) { + return; + } + + let span = inner_call.span; + let Some(arg_expr) = inner_call.arguments[0].as_expression_mut() else { + return; + }; + + let new_callee = ctx.ast.expression_sequence( + span, + ctx.ast.vec_from_array([ + ctx.ast.expression_numeric_literal(span, 0.0, None, NumberBase::Decimal), + arg_expr.take_in(ctx.ast), + ]), + ); + expr.callee = new_callee; + ctx.state.changed = true; + } + /// Remove name from function expressions if it is not used. /// /// e.g. `var a = function f() {}` -> `var a = function () {}` @@ -1832,4 +1876,14 @@ mod test { test("var {y: z, 'z': y} = x", "var {y: z, z: y} = x"); test("var {y: y, 'z': z} = x", "var {y, z} = x"); } + + #[test] + fn test_object_callee_indirect_call() { + test("Object(f)(1,2)", "f(1, 2)"); + test("(Object(g))(a)", "g(a)"); + test("Object(a.b)(x)", "(0, a.b)(x)"); + test_same("Object?.(f)(1)"); + test_same("function Object(x){return x} Object(f)(1)"); + test_same("Object(...a)(1)"); + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 988c5f036e56e..1be9605400185 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -17,11 +17,11 @@ Original | minified | minified | gzip | gzip | Iterations | Fi 1.25 MB | 646.98 kB | 646.76 kB | 160.27 kB | 163.73 kB | 2 | three.js -2.14 MB | 717.51 kB | 724.14 kB | 161.88 kB | 181.07 kB | 2 | victory.js +2.14 MB | 715.66 kB | 724.14 kB | 161.78 kB | 181.07 kB | 2 | victory.js 3.20 MB | 1.01 MB | 1.01 MB | 324.08 kB | 331.56 kB | 2 | echarts.js -6.69 MB | 2.25 MB | 2.31 MB | 463.12 kB | 488.28 kB | 3 | antd.js +6.69 MB | 2.23 MB | 2.31 MB | 462.26 kB | 488.28 kB | 3 | antd.js 10.95 MB | 3.35 MB | 3.49 MB | 860.97 kB | 915.50 kB | 2 | typescript.js