Skip to content

Commit df9b2d1

Browse files
committed
feat(minifier): fold Array::concat into literal (#8442)
Compress `[].concat(a, [b])` into `[a, b]`. **References** - [Spec of `Array::concat`](https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.concat)
1 parent 0854246 commit df9b2d1

File tree

2 files changed

+77
-21
lines changed

2 files changed

+77
-21
lines changed

crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs

+76-20
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ impl<'a> PeepholeReplaceKnownMethods {
6262
"indexOf" | "lastIndexOf" => Self::try_fold_string_index_of(ce, name, object, ctx),
6363
"charAt" => Self::try_fold_string_char_at(ce, object, ctx),
6464
"charCodeAt" => Self::try_fold_string_char_code_at(ce, object, ctx),
65+
"concat" => Self::try_fold_concat(ce, ctx),
6566
"replace" | "replaceAll" => Self::try_fold_string_replace(ce, name, object, ctx),
6667
"fromCharCode" => Self::try_fold_string_from_char_code(ce, object, ctx),
6768
"toString" => Self::try_fold_to_string(ce, object, ctx),
@@ -420,6 +421,68 @@ impl<'a> PeepholeReplaceKnownMethods {
420421
);
421422
self.changed = true;
422423
}
424+
425+
/// `[].concat(1, 2)` -> `[1, 2]`
426+
fn try_fold_concat(
427+
ce: &mut CallExpression<'a>,
428+
ctx: &mut TraverseCtx<'a>,
429+
) -> Option<Expression<'a>> {
430+
// let concat chaining reduction handle it first
431+
if let Ancestor::StaticMemberExpressionObject(parent_member) = ctx.parent() {
432+
if parent_member.property().name.as_str() == "concat" {
433+
return None;
434+
}
435+
}
436+
437+
let Expression::StaticMemberExpression(member) = &mut ce.callee else { unreachable!() };
438+
let Expression::ArrayExpression(array_expr) = &mut member.object else { return None };
439+
440+
let can_merge_until = ce
441+
.arguments
442+
.iter()
443+
.enumerate()
444+
.take_while(|(_, argument)| match argument {
445+
Argument::SpreadElement(_) => false,
446+
match_expression!(Argument) => {
447+
let argument = argument.to_expression();
448+
if argument.is_literal() {
449+
true
450+
} else {
451+
matches!(argument, Expression::ArrayExpression(_))
452+
}
453+
}
454+
})
455+
.map(|(i, _)| i)
456+
.last();
457+
458+
if let Some(can_merge_until) = can_merge_until {
459+
for argument in ce.arguments.drain(..=can_merge_until) {
460+
let argument = argument.into_expression();
461+
if argument.is_literal() {
462+
array_expr.elements.push(ArrayExpressionElement::from(argument));
463+
} else {
464+
let Expression::ArrayExpression(mut argument_array) = argument else {
465+
unreachable!()
466+
};
467+
array_expr.elements.append(&mut argument_array.elements);
468+
}
469+
}
470+
}
471+
472+
if ce.arguments.is_empty() {
473+
Some(ctx.ast.move_expression(&mut member.object))
474+
} else if can_merge_until.is_some() {
475+
Some(ctx.ast.expression_call(
476+
ce.span,
477+
ctx.ast.move_expression(&mut ce.callee),
478+
Option::<TSTypeParameterInstantiation>::None,
479+
ctx.ast.move_vec(&mut ce.arguments),
480+
false,
481+
))
482+
} else {
483+
None
484+
}
485+
}
423486
}
424487

425488
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
@@ -1167,52 +1230,45 @@ mod test {
11671230
#[test]
11681231
fn test_fold_concat_chaining() {
11691232
// array
1170-
fold("[1,2].concat(1).concat(2,['abc']).concat('abc')", "[1,2].concat(1,2,['abc'],'abc')");
1171-
fold("[].concat(['abc']).concat(1).concat([2,3])", "[].concat(['abc'],1,[2,3])");
1233+
fold("[1,2].concat(1).concat(2,['abc']).concat('abc')", "[1,2,1,2,'abc','abc']");
1234+
fold("[].concat(['abc']).concat(1).concat([2,3])", "['abc',1,2,3]");
11721235

11731236
fold("var x, y; [1].concat(x).concat(y)", "var x, y; [1].concat(x, y)");
11741237
fold("var y; [1].concat(x).concat(y)", "var y; [1].concat(x, y)"); // x might have a getter that updates y, but that side effect is preserved correctly
11751238
fold("var x; [1].concat(x.a).concat(x)", "var x; [1].concat(x.a, x)"); // x.a might have a getter that updates x, but that side effect is preserved correctly
11761239

1177-
fold_same("[].concat(1)");
1178-
11791240
// string
11801241
fold("'1'.concat(1).concat(2,['abc']).concat('abc')", "'1'.concat(1,2,['abc'],'abc')");
11811242
fold("''.concat(['abc']).concat(1).concat([2,3])", "''.concat(['abc'],1,[2,3])");
1243+
fold_same("''.concat(1)");
11821244

11831245
fold("var x, y; ''.concat(x).concat(y)", "var x, y; ''.concat(x, y)");
11841246
fold("var y; ''.concat(x).concat(y)", "var y; ''.concat(x, y)"); // x might have a getter that updates y, but that side effect is preserved correctly
11851247
fold("var x; ''.concat(x.a).concat(x)", "var x; ''.concat(x.a, x)"); // x.a might have a getter that updates x, but that side effect is preserved correctly
11861248

1187-
fold_same("''.concat(1)");
1188-
11891249
// other
11901250
fold_same("obj.concat([1,2]).concat(1)");
11911251
}
11921252

11931253
#[test]
1194-
#[ignore]
11951254
fn test_remove_array_literal_from_front_of_concat() {
1196-
// enableTypeCheck();
1197-
1198-
fold("[].concat([1,2,3],1)", "[1,2,3].concat(1)");
1255+
fold("[].concat([1,2,3],1)", "[1,2,3,1]");
11991256

1200-
fold_same("[1,2,3].concat(returnArrayType())");
1257+
fold_same("[1,2,3].concat(foo())");
12011258
// Call method with the same name as Array.prototype.concat
12021259
fold_same("obj.concat([1,2,3])");
12031260

1204-
fold_same("[].concat(1,[1,2,3])");
1205-
fold_same("[].concat(1)");
1206-
fold("[].concat([1])", "[1].concat()");
1261+
fold("[].concat(1,[1,2,3])", "[1,1,2,3]");
1262+
fold("[].concat(1)", "[1]");
1263+
fold("[].concat([1])", "[1]");
12071264

12081265
// Chained folding of empty array lit
1209-
fold("[].concat([], [1,2,3], [4])", "[1,2,3].concat([4])");
1210-
fold("[].concat([]).concat([1]).concat([2,3])", "[1].concat([2,3])");
1266+
fold("[].concat([], [1,2,3], [4])", "[1,2,3,4]");
1267+
fold("[].concat([]).concat([1]).concat([2,3])", "[1,2,3]");
12111268

1212-
// Cannot fold based on type information
1213-
fold_same("[].concat(returnArrayType(),1)");
1214-
fold_same("[].concat(returnArrayType())");
1215-
fold_same("[].concat(returnUnionType())");
1269+
fold("[].concat(1, x)", "[1].concat(x)"); // x might be an array or an object with `Symbol.isConcatSpreadable`
1270+
fold("[].concat(1, ...x)", "[1].concat(...x)");
1271+
fold_same("[].concat(x, 1)");
12161272
}
12171273

12181274
#[test]

tasks/minsize/minsize.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Original | minified | minified | gzip | gzip | Fixture
1111

1212
544.10 kB | 71.76 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
1313

14-
555.77 kB | 273.15 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js
14+
555.77 kB | 272.91 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
1515

1616
1.01 MB | 460.17 kB | 458.89 kB | 126.76 kB | 126.71 kB | bundle.min.js
1717

0 commit comments

Comments
 (0)