@@ -7,7 +7,7 @@ use oxc_ecmascript::{
7
7
constant_evaluation:: ConstantEvaluation , StringCharAt , StringCharCodeAt , StringIndexOf ,
8
8
StringLastIndexOf , StringSubstring , ToInt32 ,
9
9
} ;
10
- use oxc_traverse:: { traverse_mut_with_ctx, ReusableTraverseCtx , Traverse , TraverseCtx } ;
10
+ use oxc_traverse:: { traverse_mut_with_ctx, Ancestor , ReusableTraverseCtx , Traverse , TraverseCtx } ;
11
11
12
12
use crate :: { ctx:: Ctx , CompressorPass } ;
13
13
@@ -26,6 +26,7 @@ impl<'a> CompressorPass<'a> for PeepholeReplaceKnownMethods {
26
26
27
27
impl < ' a > Traverse < ' a > for PeepholeReplaceKnownMethods {
28
28
fn exit_expression ( & mut self , node : & mut Expression < ' a > , ctx : & mut TraverseCtx < ' a > ) {
29
+ self . try_fold_array_concat ( node, ctx) ;
29
30
self . try_fold_known_string_methods ( node, ctx) ;
30
31
}
31
32
}
@@ -338,6 +339,83 @@ impl<'a> PeepholeReplaceKnownMethods {
338
339
}
339
340
result. into_iter ( ) . rev ( ) . collect ( )
340
341
}
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
+ }
341
419
}
342
420
343
421
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
@@ -1083,25 +1161,15 @@ mod test {
1083
1161
}
1084
1162
1085
1163
#[ test]
1086
- #[ ignore]
1087
1164
fn test_fold_concat_chaining ( ) {
1088
- // enableTypeCheck();
1089
-
1090
1165
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])" ) ;
1092
1167
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)" ) ;
1105
1173
fold_same ( "obj.concat([1,2]).concat(1)" ) ;
1106
1174
}
1107
1175
0 commit comments