Skip to content

Commit 892674d

Browse files
committed
feat(minifier): fold array concat chaining
1 parent dd64340 commit 892674d

File tree

2 files changed

+87
-17
lines changed

2 files changed

+87
-17
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
}
@@ -322,6 +323,83 @@ impl<'a> PeepholeReplaceKnownMethods {
322323
}
323324
result.into_iter().rev().collect()
324325
}
326+
327+
/// `[].concat(a).concat(b)` -> `[].concat(a, b)`
328+
fn try_fold_array_concat(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
329+
if matches!(ctx.parent(), Ancestor::StaticMemberExpressionObject(_)) {
330+
return;
331+
}
332+
333+
let original_span = if let Expression::CallExpression(root_call_expr) = node {
334+
root_call_expr.span
335+
} else {
336+
return;
337+
};
338+
339+
let mut current_node: &mut Expression = node;
340+
let mut collected_arguments = vec![];
341+
let new_root_callee: &mut Expression<'a>;
342+
loop {
343+
let Expression::CallExpression(ce) = current_node else {
344+
return;
345+
};
346+
let Expression::StaticMemberExpression(member) = &ce.callee else {
347+
return;
348+
};
349+
if member.optional || member.property.name != "concat" {
350+
return;
351+
}
352+
353+
// We don't need to check if the arguments has a side effect here.
354+
//
355+
// The only side effect Array::concat can cause is throwing an error when the created array is too long.
356+
// With the compressor assumption, that error can be moved.
357+
//
358+
// For example, if we have `[].concat(a).concat(b)`, the steps before the compression is:
359+
// 1. evaluate `a`
360+
// 2. `[].concat(a)` creates `[a]`
361+
// 3. evaluate `b`
362+
// 4. `.concat(b)` creates `[a, b]`
363+
//
364+
// The steps after the compression (`[].concat(a, b)`) is:
365+
// 1. evaluate `a`
366+
// 2. evaluate `b`
367+
// 3. `[].concat(a, b)` creates `[a, b]`
368+
//
369+
// The error that has to be thrown in the second step before the compression will be thrown in the third step.
370+
371+
let CallExpression { callee, arguments, .. } = ce.as_mut();
372+
collected_arguments.push(arguments);
373+
374+
// [].concat()
375+
let is_root_expr_concat = {
376+
let Expression::StaticMemberExpression(member) = callee else { unreachable!() };
377+
matches!(&member.object, Expression::ArrayExpression(_))
378+
};
379+
if is_root_expr_concat {
380+
new_root_callee = callee;
381+
break;
382+
}
383+
384+
let Expression::StaticMemberExpression(member) = callee else { unreachable!() };
385+
current_node = &mut member.object;
386+
}
387+
388+
if collected_arguments.len() <= 1 {
389+
return;
390+
}
391+
392+
*node = ctx.ast.expression_call(
393+
original_span,
394+
ctx.ast.move_expression(new_root_callee),
395+
Option::<TSTypeParameterInstantiation>::None,
396+
ctx.ast.vec_from_iter(
397+
collected_arguments.into_iter().rev().flat_map(|arg| ctx.ast.move_vec(arg)),
398+
),
399+
false,
400+
);
401+
self.changed = true;
402+
}
325403
}
326404

327405
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
@@ -1070,25 +1148,15 @@ mod test {
10701148
}
10711149

10721150
#[test]
1073-
#[ignore]
10741151
fn test_fold_concat_chaining() {
1075-
// enableTypeCheck();
1076-
10771152
fold("[1,2].concat(1).concat(2,['abc']).concat('abc')", "[1,2].concat(1,2,['abc'],'abc')");
1078-
fold("[].concat(['abc']).concat(1).concat([2,3])", "['abc'].concat(1,[2,3])");
1153+
fold("[].concat(['abc']).concat(1).concat([2,3])", "[].concat(['abc'],1,[2,3])");
10791154

1080-
// cannot fold concat based on type information
1081-
fold_same("returnArrayType().concat(returnArrayType()).concat(1).concat(2)");
1082-
fold_same("returnArrayType().concat(returnUnionType()).concat(1).concat(2)");
1083-
fold(
1084-
"[1,2,1].concat(1).concat(returnArrayType()).concat(2)",
1085-
"[1,2,1].concat(1).concat(returnArrayType(),2)",
1086-
);
1087-
fold(
1088-
"[1].concat(1).concat(2).concat(returnArrayType())",
1089-
"[1].concat(1,2).concat(returnArrayType())",
1090-
);
1091-
fold_same("[].concat(1).concat(returnArrayType())");
1155+
fold("var x, y; [1].concat(x).concat(y)", "var x, y; [1].concat(x, y)");
1156+
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
1157+
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
1158+
1159+
fold_same("[].concat(1)");
10921160
fold_same("obj.concat([1,2]).concat(1)");
10931161
}
10941162

0 commit comments

Comments
 (0)