@@ -2,18 +2,23 @@ use std::borrow::Cow;
2
2
3
3
use cow_utils:: CowUtils ;
4
4
5
+ use oxc_allocator:: IntoIn ;
5
6
use oxc_ast:: ast:: * ;
6
7
use oxc_ecmascript:: {
7
8
constant_evaluation:: ConstantEvaluation , StringCharAt , StringCharCodeAt , StringIndexOf ,
8
9
StringLastIndexOf , StringSubstring , ToInt32 ,
9
10
} ;
11
+ use oxc_span:: SPAN ;
12
+ use oxc_syntax:: es_target:: ESTarget ;
10
13
use oxc_traverse:: { traverse_mut_with_ctx, Ancestor , ReusableTraverseCtx , Traverse , TraverseCtx } ;
11
14
12
15
use crate :: { ctx:: Ctx , CompressorPass } ;
13
16
14
17
/// Minimize With Known Methods
15
18
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeReplaceKnownMethods.java>
16
19
pub struct PeepholeReplaceKnownMethods {
20
+ target : ESTarget ,
21
+
17
22
pub ( crate ) changed : bool ,
18
23
}
19
24
@@ -32,8 +37,8 @@ impl<'a> Traverse<'a> for PeepholeReplaceKnownMethods {
32
37
}
33
38
34
39
impl < ' a > PeepholeReplaceKnownMethods {
35
- pub fn new ( ) -> Self {
36
- Self { changed : false }
40
+ pub fn new ( target : ESTarget ) -> Self {
41
+ Self { target , changed : false }
37
42
}
38
43
39
44
fn try_fold_known_string_methods (
@@ -52,7 +57,7 @@ impl<'a> PeepholeReplaceKnownMethods {
52
57
"indexOf" | "lastIndexOf" => Self :: try_fold_string_index_of ( ce, member, ctx) ,
53
58
"charAt" => Self :: try_fold_string_char_at ( ce, member, ctx) ,
54
59
"charCodeAt" => Self :: try_fold_string_char_code_at ( ce, member, ctx) ,
55
- "concat" => Self :: try_fold_concat ( ce, ctx) ,
60
+ "concat" => self . try_fold_concat ( ce, ctx) ,
56
61
"replace" | "replaceAll" => Self :: try_fold_string_replace ( ce, member, ctx) ,
57
62
"fromCharCode" => Self :: try_fold_string_from_char_code ( ce, member, ctx) ,
58
63
"toString" => Self :: try_fold_to_string ( ce, member, ctx) ,
@@ -407,7 +412,9 @@ impl<'a> PeepholeReplaceKnownMethods {
407
412
}
408
413
409
414
/// `[].concat(1, 2)` -> `[1, 2]`
415
+ /// `"".concat(a, "b")` -> "`${a}b`"
410
416
fn try_fold_concat (
417
+ & self ,
411
418
ce : & mut CallExpression < ' a > ,
412
419
ctx : & mut TraverseCtx < ' a > ,
413
420
) -> Option < Expression < ' a > > {
@@ -419,52 +426,134 @@ impl<'a> PeepholeReplaceKnownMethods {
419
426
}
420
427
421
428
let Expression :: StaticMemberExpression ( member) = & mut ce. callee else { unreachable ! ( ) } ;
422
- let Expression :: ArrayExpression ( array_expr) = & mut member. object else { return None } ;
423
-
424
- let can_merge_until = ce
425
- . arguments
426
- . iter ( )
427
- . enumerate ( )
428
- . take_while ( |( _, argument) | match argument {
429
- Argument :: SpreadElement ( _) => false ,
430
- match_expression ! ( Argument ) => {
431
- let argument = argument. to_expression ( ) ;
432
- if argument. is_literal ( ) {
433
- true
434
- } else {
435
- matches ! ( argument, Expression :: ArrayExpression ( _) )
429
+ match & mut member. object {
430
+ Expression :: ArrayExpression ( array_expr) => {
431
+ let can_merge_until = ce
432
+ . arguments
433
+ . iter ( )
434
+ . enumerate ( )
435
+ . take_while ( |( _, argument) | match argument {
436
+ Argument :: SpreadElement ( _) => false ,
437
+ match_expression ! ( Argument ) => {
438
+ let argument = argument. to_expression ( ) ;
439
+ if argument. is_literal ( ) {
440
+ true
441
+ } else {
442
+ matches ! ( argument, Expression :: ArrayExpression ( _) )
443
+ }
444
+ }
445
+ } )
446
+ . map ( |( i, _) | i)
447
+ . last ( ) ;
448
+
449
+ if let Some ( can_merge_until) = can_merge_until {
450
+ for argument in ce. arguments . drain ( ..=can_merge_until) {
451
+ let argument = argument. into_expression ( ) ;
452
+ if argument. is_literal ( ) {
453
+ array_expr. elements . push ( ArrayExpressionElement :: from ( argument) ) ;
454
+ } else {
455
+ let Expression :: ArrayExpression ( mut argument_array) = argument else {
456
+ unreachable ! ( )
457
+ } ;
458
+ array_expr. elements . append ( & mut argument_array. elements ) ;
459
+ }
436
460
}
437
461
}
438
- } )
439
- . map ( |( i, _) | i)
440
- . last ( ) ;
441
-
442
- if let Some ( can_merge_until) = can_merge_until {
443
- for argument in ce. arguments . drain ( ..=can_merge_until) {
444
- let argument = argument. into_expression ( ) ;
445
- if argument. is_literal ( ) {
446
- array_expr. elements . push ( ArrayExpressionElement :: from ( argument) ) ;
462
+
463
+ if ce. arguments . is_empty ( ) {
464
+ Some ( ctx. ast . move_expression ( & mut member. object ) )
465
+ } else if can_merge_until. is_some ( ) {
466
+ Some ( ctx. ast . expression_call (
467
+ ce. span ,
468
+ ctx. ast . move_expression ( & mut ce. callee ) ,
469
+ Option :: < TSTypeParameterInstantiation > :: None ,
470
+ ctx. ast . move_vec ( & mut ce. arguments ) ,
471
+ false ,
472
+ ) )
447
473
} else {
448
- let Expression :: ArrayExpression ( mut argument_array) = argument else {
449
- unreachable ! ( )
450
- } ;
451
- array_expr. elements . append ( & mut argument_array. elements ) ;
474
+ None
452
475
}
453
476
}
454
- }
477
+ Expression :: StringLiteral ( base_str) => {
478
+ if self . target < ESTarget :: ES2015
479
+ || ce. arguments . is_empty ( )
480
+ || !ce. arguments . iter ( ) . all ( Argument :: is_expression)
481
+ {
482
+ return None ;
483
+ }
455
484
456
- if ce. arguments . is_empty ( ) {
457
- Some ( ctx. ast . move_expression ( & mut member. object ) )
458
- } else if can_merge_until. is_some ( ) {
459
- Some ( ctx. ast . expression_call (
460
- ce. span ,
461
- ctx. ast . move_expression ( & mut ce. callee ) ,
462
- Option :: < TSTypeParameterInstantiation > :: None ,
463
- ctx. ast . move_vec ( & mut ce. arguments ) ,
464
- false ,
465
- ) )
466
- } else {
467
- None
485
+ let expression_count = ce
486
+ . arguments
487
+ . iter ( )
488
+ . filter ( |arg| !matches ! ( arg, Argument :: StringLiteral ( _) ) )
489
+ . count ( ) ;
490
+
491
+ // whether it is shorter to use `String::concat`
492
+ if ".concat()" . len ( ) + ce. arguments . len ( ) < "${}" . len ( ) * expression_count {
493
+ return None ;
494
+ }
495
+
496
+ let mut quasi_strs: Vec < Cow < ' a , str > > =
497
+ vec ! [ Cow :: Borrowed ( base_str. value. as_str( ) ) ] ;
498
+ let mut expressions = ctx. ast . vec ( ) ;
499
+ let mut pushed_quasi = true ;
500
+ for argument in ce. arguments . drain ( ..) {
501
+ if let Argument :: StringLiteral ( str_lit) = argument {
502
+ if pushed_quasi {
503
+ let last_quasi = quasi_strs
504
+ . last_mut ( )
505
+ . expect ( "last element should exist because pushed_quasi is true" ) ;
506
+ last_quasi. to_mut ( ) . push_str ( & str_lit. value ) ;
507
+ } else {
508
+ quasi_strs. push ( Cow :: Borrowed ( str_lit. value . as_str ( ) ) ) ;
509
+ }
510
+ pushed_quasi = true ;
511
+ } else {
512
+ if !pushed_quasi {
513
+ // need a pair
514
+ quasi_strs. push ( Cow :: Borrowed ( "" ) ) ;
515
+ }
516
+ // checked that all the arguments are expression above
517
+ expressions. push ( argument. into_expression ( ) ) ;
518
+ pushed_quasi = false ;
519
+ }
520
+ }
521
+ if !pushed_quasi {
522
+ quasi_strs. push ( Cow :: Borrowed ( "" ) ) ;
523
+ }
524
+
525
+ if expressions. is_empty ( ) {
526
+ debug_assert_eq ! ( quasi_strs. len( ) , 1 ) ;
527
+ return Some ( ctx. ast . expression_string_literal (
528
+ ce. span ,
529
+ quasi_strs. pop ( ) . unwrap ( ) ,
530
+ None ,
531
+ ) ) ;
532
+ }
533
+
534
+ let mut quasis = ctx. ast . vec_from_iter ( quasi_strs. into_iter ( ) . map ( |s| {
535
+ let cooked = s. clone ( ) . into_in ( ctx. ast . allocator ) ;
536
+ ctx. ast . template_element (
537
+ SPAN ,
538
+ false ,
539
+ TemplateElementValue {
540
+ raw : s
541
+ . cow_replace ( "`" , "\\ `" )
542
+ . cow_replace ( "${" , "\\ ${" )
543
+ . cow_replace ( "\r \n " , "\\ r\n " )
544
+ . into_in ( ctx. ast . allocator ) ,
545
+ cooked : Some ( cooked) ,
546
+ } ,
547
+ )
548
+ } ) ) ;
549
+ if let Some ( last_quasi) = quasis. last_mut ( ) {
550
+ last_quasi. tail = true ;
551
+ }
552
+
553
+ debug_assert_eq ! ( quasis. len( ) , expressions. len( ) + 1 ) ;
554
+ Some ( ctx. ast . expression_template_literal ( ce. span , quasis, expressions) )
555
+ }
556
+ _ => None ,
468
557
}
469
558
}
470
559
}
@@ -473,12 +562,14 @@ impl<'a> PeepholeReplaceKnownMethods {
473
562
#[ cfg( test) ]
474
563
mod test {
475
564
use oxc_allocator:: Allocator ;
565
+ use oxc_syntax:: es_target:: ESTarget ;
476
566
477
567
use crate :: tester;
478
568
479
569
fn test ( source_text : & str , positive : & str ) {
480
570
let allocator = Allocator :: default ( ) ;
481
- let mut pass = super :: PeepholeReplaceKnownMethods :: new ( ) ;
571
+ let target = ESTarget :: ESNext ;
572
+ let mut pass = super :: PeepholeReplaceKnownMethods :: new ( target) ;
482
573
tester:: test ( & allocator, source_text, positive, & mut pass) ;
483
574
}
484
575
@@ -1367,4 +1458,26 @@ mod test {
1367
1458
test_same ( "(-1000000).toString(36)" ) ;
1368
1459
test_same ( "(-0).toString(36)" ) ;
1369
1460
}
1461
+
1462
+ #[ test]
1463
+ fn test_fold_string_concat ( ) {
1464
+ test_same ( "x = ''.concat()" ) ;
1465
+ test ( "x = ''.concat(a, b)" , "x = `${a}${b}`" ) ;
1466
+ test ( "x = ''.concat(a, b, c)" , "x = `${a}${b}${c}`" ) ;
1467
+ test ( "x = ''.concat(a, b, c, d)" , "x = `${a}${b}${c}${d}`" ) ;
1468
+ test_same ( "x = ''.concat(a, b, c, d, e)" ) ;
1469
+ test ( "x = ''.concat('a')" , "x = 'a'" ) ;
1470
+ test ( "x = ''.concat('a', 'b')" , "x = 'ab'" ) ;
1471
+ test ( "x = ''.concat('a', 'b', 'c')" , "x = 'abc'" ) ;
1472
+ test ( "x = ''.concat('a', 'b', 'c', 'd')" , "x = 'abcd'" ) ;
1473
+ test ( "x = ''.concat('a', 'b', 'c', 'd', 'e')" , "x = 'abcde'" ) ;
1474
+ test ( "x = ''.concat(a, 'b')" , "x = `${a}b`" ) ;
1475
+ test ( "x = ''.concat('a', b)" , "x = `a${b}`" ) ;
1476
+ test ( "x = ''.concat(a, 'b', c)" , "x = `${a}b${c}`" ) ;
1477
+ test ( "x = ''.concat('a', b, 'c')" , "x = `a${b}c`" ) ;
1478
+ test ( "x = ''.concat(a, 1)" , "x = `${a}${1}`" ) ; // inlining 1 is not implemented yet
1479
+
1480
+ test ( "x = '`'.concat(a)" , "x = `\\ `${a}`" ) ;
1481
+ test ( "x = '${'.concat(a)" , "x = `\\ ${${a}`" ) ;
1482
+ }
1370
1483
}
0 commit comments