Skip to content

Commit 02f0785

Browse files
committed
feat(minifier): fold array concat chaining
1 parent 30a869e commit 02f0785

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

crates/oxc_minifier/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ The compressor is responsible for rewriting statements and expressions for minim
2929
- Examples that breaks this assumption: `(() => { console.log(v); let v; })()`
3030
- `with` statement is not used
3131
- Examples that breaks this assumption: `with (Math) { console.log(PI); }`
32+
- Errors thrown when creating a String or an Array that exceeds the maximum length can disappear or moved
33+
- Examples that breaks this assumption: `try { new Array(Number(2n**53n)) } catch { console.log('log') }`
3234

3335
## Terser Tests
3436

crates/oxc_minifier/src/ast_passes/peephole_replace_known_methods.rs

+85-17
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use oxc_ecmascript::{
77
constant_evaluation::ConstantEvaluation, StringCharAt, StringCharCodeAt, StringIndexOf,
88
StringLastIndexOf, StringSubstring, ToInt32,
99
};
10-
use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx};
10+
use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
1111

1212
use crate::{ctx::Ctx, CompressorPass};
1313

@@ -26,6 +26,7 @@ impl<'a> CompressorPass<'a> for PeepholeReplaceKnownMethods {
2626

2727
impl<'a> Traverse<'a> for PeepholeReplaceKnownMethods {
2828
fn exit_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
29+
self.try_fold_array_concat(node, ctx);
2930
self.try_fold_known_string_methods(node, ctx);
3031
}
3132
}
@@ -338,6 +339,83 @@ impl<'a> PeepholeReplaceKnownMethods {
338339
}
339340
result.into_iter().rev().collect()
340341
}
342+
343+
/// `[].concat(a).concat(b)` -> `[].concat(a, b)`
344+
fn try_fold_array_concat(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
345+
if matches!(ctx.parent(), Ancestor::StaticMemberExpressionObject(_)) {
346+
return;
347+
}
348+
349+
let original_span = if let Expression::CallExpression(root_call_expr) = node {
350+
root_call_expr.span
351+
} else {
352+
return;
353+
};
354+
355+
let mut current_node: &mut Expression = node;
356+
let mut collected_arguments = ctx.ast.vec();
357+
let new_root_callee: &mut Expression<'a>;
358+
loop {
359+
let Expression::CallExpression(ce) = current_node else {
360+
return;
361+
};
362+
let Expression::StaticMemberExpression(member) = &ce.callee else {
363+
return;
364+
};
365+
if member.optional || member.property.name != "concat" {
366+
return;
367+
}
368+
369+
// We don't need to check if the arguments has a side effect here.
370+
//
371+
// The only side effect Array::concat can cause is throwing an error when the created array is too long.
372+
// With the compressor assumption, that error can be moved.
373+
//
374+
// For example, if we have `[].concat(a).concat(b)`, the steps before the compression is:
375+
// 1. evaluate `a`
376+
// 2. `[].concat(a)` creates `[a]`
377+
// 3. evaluate `b`
378+
// 4. `.concat(b)` creates `[a, b]`
379+
//
380+
// The steps after the compression (`[].concat(a, b)`) is:
381+
// 1. evaluate `a`
382+
// 2. evaluate `b`
383+
// 3. `[].concat(a, b)` creates `[a, b]`
384+
//
385+
// The error that has to be thrown in the second step before the compression will be thrown in the third step.
386+
387+
let CallExpression { callee, arguments, .. } = ce.as_mut();
388+
collected_arguments.push(arguments);
389+
390+
// [].concat()
391+
let is_root_expr_concat = {
392+
let Expression::StaticMemberExpression(member) = callee else { unreachable!() };
393+
matches!(&member.object, Expression::ArrayExpression(_))
394+
};
395+
if is_root_expr_concat {
396+
new_root_callee = callee;
397+
break;
398+
}
399+
400+
let Expression::StaticMemberExpression(member) = callee else { unreachable!() };
401+
current_node = &mut member.object;
402+
}
403+
404+
if collected_arguments.len() <= 1 {
405+
return;
406+
}
407+
408+
*node = ctx.ast.expression_call(
409+
original_span,
410+
ctx.ast.move_expression(new_root_callee),
411+
Option::<TSTypeParameterInstantiation>::None,
412+
ctx.ast.vec_from_iter(
413+
collected_arguments.into_iter().rev().flat_map(|arg| ctx.ast.move_vec(arg)),
414+
),
415+
false,
416+
);
417+
self.changed = true;
418+
}
341419
}
342420

343421
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
@@ -1083,25 +1161,15 @@ mod test {
10831161
}
10841162

10851163
#[test]
1086-
#[ignore]
10871164
fn test_fold_concat_chaining() {
1088-
// enableTypeCheck();
1089-
10901165
fold("[1,2].concat(1).concat(2,['abc']).concat('abc')", "[1,2].concat(1,2,['abc'],'abc')");
1091-
fold("[].concat(['abc']).concat(1).concat([2,3])", "['abc'].concat(1,[2,3])");
1166+
fold("[].concat(['abc']).concat(1).concat([2,3])", "[].concat(['abc'],1,[2,3])");
10921167

1093-
// cannot fold concat based on type information
1094-
fold_same("returnArrayType().concat(returnArrayType()).concat(1).concat(2)");
1095-
fold_same("returnArrayType().concat(returnUnionType()).concat(1).concat(2)");
1096-
fold(
1097-
"[1,2,1].concat(1).concat(returnArrayType()).concat(2)",
1098-
"[1,2,1].concat(1).concat(returnArrayType(),2)",
1099-
);
1100-
fold(
1101-
"[1].concat(1).concat(2).concat(returnArrayType())",
1102-
"[1].concat(1,2).concat(returnArrayType())",
1103-
);
1104-
fold_same("[].concat(1).concat(returnArrayType())");
1168+
fold("var x, y; [1].concat(x).concat(y)", "var x, y; [1].concat(x, y)");
1169+
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
1170+
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
1171+
1172+
fold_same("[].concat(1)");
11051173
fold_same("obj.concat([1,2]).concat(1)");
11061174
}
11071175

tasks/minsize/minsize.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Original | minified | minified | gzip | gzip | Fixture
1919

2020
2.14 MB | 725.56 kB | 724.14 kB | 180.06 kB | 181.07 kB | victory.js
2121

22-
3.20 MB | 1.01 MB | 1.01 MB | 332.02 kB | 331.56 kB | echarts.js
22+
3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js
2323

2424
6.69 MB | 2.32 MB | 2.31 MB | 492.65 kB | 488.28 kB | antd.js
2525

0 commit comments

Comments
 (0)