diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index 895b5d0a8a4c3..0520d103603cf 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -1,4 +1,6 @@ -use oxc_ast::ast::*; +use oxc_allocator::Vec; +use oxc_ast::{ast::*, NONE}; +use oxc_semantic::IsGlobalReference; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::number::ToJsInt32; use oxc_syntax::{ @@ -90,6 +92,24 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { } } + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + match expr { + Expression::NewExpression(new_expr) => { + if let Some(new_expr) = self.try_fold_new_expression(new_expr, ctx) { + *expr = new_expr; + self.changed = true; + } + } + Expression::CallExpression(call_expr) => { + if let Some(call_expr) = self.try_fold_call_expression(call_expr, ctx) { + *expr = call_expr; + self.changed = true; + } + } + _ => {} + } + } + fn enter_binary_expression( &mut self, expr: &mut BinaryExpression<'a>, @@ -328,6 +348,145 @@ impl<'a> PeepholeSubstituteAlternateSyntax { None } } + + fn try_fold_new_expression( + &mut self, + new_expr: &mut NewExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // `new Object` -> `{}` + if new_expr.arguments.is_empty() + && new_expr.callee.is_global_reference_name("Object", ctx.symbols()) + { + Some(ctx.ast.expression_object(new_expr.span, Vec::new_in(ctx.ast.allocator), None)) + } else if new_expr.callee.is_global_reference_name("Array", ctx.symbols()) { + // `new Array` -> `[]` + if new_expr.arguments.is_empty() { + Some(self.empty_array_literal(ctx)) + } else if new_expr.arguments.len() == 1 { + let arg = new_expr.arguments.get_mut(0).and_then(|arg| arg.as_expression_mut())?; + // `new Array(0)` -> `[]` + if arg.is_number_0() { + Some(self.empty_array_literal(ctx)) + } + // `new Array(8)` -> `Array(8)` + else if arg.is_number_literal() { + Some( + self.array_constructor_call(ctx.ast.move_vec(&mut new_expr.arguments), ctx), + ) + } + // `new Array(literal)` -> `[literal]` + else if arg.is_literal() || matches!(arg, Expression::ArrayExpression(_)) { + let mut elements = Vec::new_in(ctx.ast.allocator); + let element = + ctx.ast.array_expression_element_expression(ctx.ast.move_expression(arg)); + elements.push(element); + Some(self.array_literal(elements, ctx)) + } + // `new Array()` -> `Array()` + else { + Some( + self.array_constructor_call(ctx.ast.move_vec(&mut new_expr.arguments), ctx), + ) + } + } else { + // `new Array(1, 2, 3)` -> `[1, 2, 3]` + let elements = Vec::from_iter_in( + new_expr.arguments.iter_mut().filter_map(|arg| arg.as_expression_mut()).map( + |arg| { + ctx.ast + .array_expression_element_expression(ctx.ast.move_expression(arg)) + }, + ), + ctx.ast.allocator, + ); + Some(self.array_literal(elements, ctx)) + } + } else { + None + } + } + + fn try_fold_call_expression( + &mut self, + call_expr: &mut CallExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + // `Object()` -> `{}` + if call_expr.arguments.is_empty() + && call_expr.callee.is_global_reference_name("Object", ctx.symbols()) + { + Some(ctx.ast.expression_object(call_expr.span, Vec::new_in(ctx.ast.allocator), None)) + } else if call_expr.callee.is_global_reference_name("Array", ctx.symbols()) { + // `Array()` -> `[]` + if call_expr.arguments.is_empty() { + Some(self.empty_array_literal(ctx)) + } else if call_expr.arguments.len() == 1 { + let arg = call_expr.arguments.get_mut(0).and_then(|arg| arg.as_expression_mut())?; + // `Array(0)` -> `[]` + if arg.is_number_0() { + Some(self.empty_array_literal(ctx)) + } + // `Array(8)` -> `Array(8)` + else if arg.is_number_literal() { + Some( + self.array_constructor_call( + ctx.ast.move_vec(&mut call_expr.arguments), + ctx, + ), + ) + } + // `Array(literal)` -> `[literal]` + else if arg.is_literal() || matches!(arg, Expression::ArrayExpression(_)) { + let mut elements = Vec::new_in(ctx.ast.allocator); + let element = + ctx.ast.array_expression_element_expression(ctx.ast.move_expression(arg)); + elements.push(element); + Some(self.array_literal(elements, ctx)) + } else { + None + } + } else { + // `Array(1, 2, 3)` -> `[1, 2, 3]` + let elements = Vec::from_iter_in( + call_expr.arguments.iter_mut().filter_map(|arg| arg.as_expression_mut()).map( + |arg| { + ctx.ast + .array_expression_element_expression(ctx.ast.move_expression(arg)) + }, + ), + ctx.ast.allocator, + ); + Some(self.array_literal(elements, ctx)) + } + } else { + None + } + } + + /// returns an `Array()` constructor call with zero, one, or more arguments, copying from the input + fn array_constructor_call( + &self, + arguments: Vec<'a, Argument<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let callee = ctx.ast.expression_identifier_reference(SPAN, "Array"); + ctx.ast.expression_call(SPAN, callee, NONE, arguments, false) + } + + /// returns an array literal `[]` of zero, one, or more elements, copying from the input + fn array_literal( + &self, + elements: Vec<'a, ArrayExpressionElement<'a>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + ctx.ast.expression_array(SPAN, elements, None) + } + + /// returns a new empty array literal expression: `[]` + fn empty_array_literal(&self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + self.array_literal(Vec::new_in(ctx.ast.allocator), ctx) + } } /// @@ -402,4 +561,51 @@ mod test { test_same("x += 1"); // The string concatenation may be triggered, so we don't fold this. test_same("x += -1"); } + + #[test] + fn fold_literal_object_constructors() { + test("x = new Object", "x = ({})"); + test("x = new Object()", "x = ({})"); + test("x = Object()", "x = ({})"); + + test_same("x = (function f(){function Object(){this.x=4}return new Object();})();"); + } + + // tests from closure compiler + #[test] + fn fold_literal_array_constructors() { + test("x = new Array", "x = []"); + test("x = new Array()", "x = []"); + test("x = Array()", "x = []"); + // do not fold optional chains + test_same("x = Array?.()"); + + // One argument + test("x = new Array(0)", "x = []"); + test("x = new Array(\"a\")", "x = [\"a\"]"); + test("x = new Array(7)", "x = Array(7)"); + test("x = new Array(y)", "x = Array(y)"); + test("x = new Array(foo())", "x = Array(foo())"); + test("x = Array(0)", "x = []"); + test("x = Array(\"a\")", "x = [\"a\"]"); + test_same("x = Array(7)"); + test_same("x = Array(y)"); + test_same("x = Array(foo())"); + + // 1+ arguments + test("x = new Array(1, 2, 3, 4)", "x = [1, 2, 3, 4]"); + test("x = Array(1, 2, 3, 4)", "x = [1, 2, 3, 4]"); + test("x = new Array('a', 1, 2, 'bc', 3, {}, 'abc')", "x = ['a', 1, 2, 'bc', 3, {}, 'abc']"); + test("x = Array('a', 1, 2, 'bc', 3, {}, 'abc')", "x = ['a', 1, 2, 'bc', 3, {}, 'abc']"); + test("x = new Array(Array(1, '2', 3, '4'))", "x = [[1, '2', 3, '4']]"); + test("x = Array(Array(1, '2', 3, '4'))", "x = [[1, '2', 3, '4']]"); + test( + "x = new Array(Object(), Array(\"abc\", Object(), Array(Array())))", + "x = [{}, [\"abc\", {}, [[]]]]", + ); + test( + "x = new Array(Object(), Array(\"abc\", Object(), Array(Array())))", + "x = [{}, [\"abc\", {}, [[]]]]", + ); + } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 16599d056f3d6..769e17f84d398 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,26 +1,26 @@ Original | Minified | esbuild | Gzip | esbuild -72.14 kB | 24.47 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js +72.14 kB | 24.46 kB | 23.70 kB | 8.65 kB | 8.54 kB | react.development.js 173.90 kB | 61.69 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js 287.63 kB | 92.83 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js -342.15 kB | 124.14 kB | 118.14 kB | 44.81 kB | 44.37 kB | vue.js +342.15 kB | 124.11 kB | 118.14 kB | 44.80 kB | 44.37 kB | vue.js 544.10 kB | 74.13 kB | 72.48 kB | 26.23 kB | 26.20 kB | lodash.js -555.77 kB | 278.70 kB | 270.13 kB | 91.39 kB | 90.80 kB | d3.js +555.77 kB | 278.24 kB | 270.13 kB | 91.36 kB | 90.80 kB | d3.js 1.01 MB | 470.11 kB | 458.89 kB | 126.97 kB | 126.71 kB | bundle.min.js -1.25 MB | 671.00 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js +1.25 MB | 670.97 kB | 646.76 kB | 164.72 kB | 163.73 kB | three.js -2.14 MB | 756.69 kB | 724.14 kB | 182.87 kB | 181.07 kB | victory.js +2.14 MB | 756.33 kB | 724.14 kB | 182.74 kB | 181.07 kB | victory.js 3.20 MB | 1.05 MB | 1.01 MB | 334.10 kB | 331.56 kB | echarts.js -6.69 MB | 2.44 MB | 2.31 MB | 498.93 kB | 488.28 kB | antd.js +6.69 MB | 2.44 MB | 2.31 MB | 498.86 kB | 488.28 kB | antd.js -10.95 MB | 3.59 MB | 3.49 MB | 913.96 kB | 915.50 kB | typescript.js +10.95 MB | 3.59 MB | 3.49 MB | 913.92 kB | 915.50 kB | typescript.js