Skip to content

Commit

Permalink
feat(minifier): fold array and object constructors (#6257)
Browse files Browse the repository at this point in the history
This will fold expressions like `new Object()` to `{}`, and `new Array()` to `[]`. Based on the closure compiler tests: https://github.com/google/closure-compiler/blob/b7e380b6320a52ce7fbac0334ede00d6a8c92fad/test/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntaxTest.java#L78.

This is outside my usual area, so feedback is welcome.

NOTE: this was previously a full stack of PRs, but Graphite decided to stop working completely for some reason and only gave me this error when I submitted a PR:
```
ERROR: Failed to submit PR for 10-02-feat_minifier_fold_single_arg_new_array_expressions:
{}
```
so I decided to just completely remake this stack and submit as 1 PR.
  • Loading branch information
camchenry committed Oct 7, 2024
1 parent 93c6db6 commit 4008afe
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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<Expression<'a>> {
// `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<Expression<'a>> {
// `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)
}
}

/// <https://github.com/google/closure-compiler/blob/master/test/com/google/javascript/jscomp/PeepholeSubstituteAlternateSyntaxTest.java>
Expand Down Expand Up @@ -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\", {}, [[]]]]",
);
}
}
14 changes: 7 additions & 7 deletions tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4008afe

Please sign in to comment.