@@ -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
}
@@ -322,6 +323,83 @@ impl<'a> PeepholeReplaceKnownMethods {
322
323
}
323
324
result. into_iter ( ) . rev ( ) . collect ( )
324
325
}
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
+ }
325
403
}
326
404
327
405
/// Port from: <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeReplaceKnownMethodsTest.java>
@@ -1070,25 +1148,15 @@ mod test {
1070
1148
}
1071
1149
1072
1150
#[ test]
1073
- #[ ignore]
1074
1151
fn test_fold_concat_chaining ( ) {
1075
- // enableTypeCheck();
1076
-
1077
1152
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])" ) ;
1079
1154
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)" ) ;
1092
1160
fold_same ( "obj.concat([1,2]).concat(1)" ) ;
1093
1161
}
1094
1162
0 commit comments