@@ -2,18 +2,23 @@ use std::borrow::Cow;
22
33use cow_utils:: CowUtils ;
44
5+ use oxc_allocator:: IntoIn ;
56use oxc_ast:: ast:: * ;
67use oxc_ecmascript:: {
78 constant_evaluation:: ConstantEvaluation , StringCharAt , StringCharCodeAt , StringIndexOf ,
89 StringLastIndexOf , StringSubstring , ToInt32 ,
910} ;
11+ use oxc_span:: SPAN ;
12+ use oxc_syntax:: es_target:: ESTarget ;
1013use oxc_traverse:: { traverse_mut_with_ctx, Ancestor , ReusableTraverseCtx , Traverse , TraverseCtx } ;
1114
1215use crate :: { ctx:: Ctx , CompressorPass } ;
1316
1417/// Minimize With Known Methods
1518/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/PeepholeReplaceKnownMethods.java>
1619pub struct PeepholeReplaceKnownMethods {
20+ target : ESTarget ,
21+
1722 pub ( crate ) changed : bool ,
1823}
1924
@@ -32,8 +37,8 @@ impl<'a> Traverse<'a> for PeepholeReplaceKnownMethods {
3237}
3338
3439impl < ' a > PeepholeReplaceKnownMethods {
35- pub fn new ( ) -> Self {
36- Self { changed : false }
40+ pub fn new ( target : ESTarget ) -> Self {
41+ Self { target , changed : false }
3742 }
3843
3944 fn try_fold_known_string_methods (
@@ -62,7 +67,7 @@ impl<'a> PeepholeReplaceKnownMethods {
6267 "indexOf" | "lastIndexOf" => Self :: try_fold_string_index_of ( ce, name, object, ctx) ,
6368 "charAt" => Self :: try_fold_string_char_at ( ce, object, ctx) ,
6469 "charCodeAt" => Self :: try_fold_string_char_code_at ( ce, object, ctx) ,
65- "concat" => Self :: try_fold_concat ( ce, ctx) ,
70+ "concat" => self . try_fold_concat ( ce, ctx) ,
6671 "replace" | "replaceAll" => Self :: try_fold_string_replace ( ce, name, object, ctx) ,
6772 "fromCharCode" => Self :: try_fold_string_from_char_code ( ce, object, ctx) ,
6873 "toString" => Self :: try_fold_to_string ( ce, object, ctx) ,
@@ -423,7 +428,9 @@ impl<'a> PeepholeReplaceKnownMethods {
423428 }
424429
425430 /// `[].concat(1, 2)` -> `[1, 2]`
431+ /// `"".concat(a, "b")` -> "`${a}b`"
426432 fn try_fold_concat (
433+ & self ,
427434 ce : & mut CallExpression < ' a > ,
428435 ctx : & mut TraverseCtx < ' a > ,
429436 ) -> Option < Expression < ' a > > {
@@ -435,52 +442,138 @@ impl<'a> PeepholeReplaceKnownMethods {
435442 }
436443
437444 let Expression :: StaticMemberExpression ( member) = & mut ce. callee else { unreachable ! ( ) } ;
438- let Expression :: ArrayExpression ( array_expr) = & mut member. object else { return None } ;
439-
440- let can_merge_until = ce
441- . arguments
442- . iter ( )
443- . enumerate ( )
444- . take_while ( |( _, argument) | match argument {
445- Argument :: SpreadElement ( _) => false ,
446- match_expression ! ( Argument ) => {
447- let argument = argument. to_expression ( ) ;
448- if argument. is_literal ( ) {
449- true
450- } else {
451- matches ! ( argument, Expression :: ArrayExpression ( _) )
445+ match & mut member. object {
446+ Expression :: ArrayExpression ( array_expr) => {
447+ let can_merge_until = ce
448+ . arguments
449+ . iter ( )
450+ . enumerate ( )
451+ . take_while ( |( _, argument) | match argument {
452+ Argument :: SpreadElement ( _) => false ,
453+ match_expression ! ( Argument ) => {
454+ let argument = argument. to_expression ( ) ;
455+ if argument. is_literal ( ) {
456+ true
457+ } else {
458+ matches ! ( argument, Expression :: ArrayExpression ( _) )
459+ }
460+ }
461+ } )
462+ . map ( |( i, _) | i)
463+ . last ( ) ;
464+
465+ if let Some ( can_merge_until) = can_merge_until {
466+ for argument in ce. arguments . drain ( ..=can_merge_until) {
467+ let argument = argument. into_expression ( ) ;
468+ if argument. is_literal ( ) {
469+ array_expr. elements . push ( ArrayExpressionElement :: from ( argument) ) ;
470+ } else {
471+ let Expression :: ArrayExpression ( mut argument_array) = argument else {
472+ unreachable ! ( )
473+ } ;
474+ array_expr. elements . append ( & mut argument_array. elements ) ;
475+ }
452476 }
453477 }
454- } )
455- . map ( |( i, _) | i)
456- . last ( ) ;
457-
458- if let Some ( can_merge_until) = can_merge_until {
459- for argument in ce. arguments . drain ( ..=can_merge_until) {
460- let argument = argument. into_expression ( ) ;
461- if argument. is_literal ( ) {
462- array_expr. elements . push ( ArrayExpressionElement :: from ( argument) ) ;
478+
479+ if ce. arguments . is_empty ( ) {
480+ Some ( ctx. ast . move_expression ( & mut member. object ) )
481+ } else if can_merge_until. is_some ( ) {
482+ Some ( ctx. ast . expression_call (
483+ ce. span ,
484+ ctx. ast . move_expression ( & mut ce. callee ) ,
485+ Option :: < TSTypeParameterInstantiation > :: None ,
486+ ctx. ast . move_vec ( & mut ce. arguments ) ,
487+ false ,
488+ ) )
463489 } else {
464- let Expression :: ArrayExpression ( mut argument_array) = argument else {
465- unreachable ! ( )
466- } ;
467- array_expr. elements . append ( & mut argument_array. elements ) ;
490+ None
468491 }
469492 }
470- }
493+ Expression :: StringLiteral ( base_str) => {
494+ if self . target < ESTarget :: ES2015
495+ || ce. arguments . is_empty ( )
496+ || !ce. arguments . iter ( ) . all ( Argument :: is_expression)
497+ {
498+ return None ;
499+ }
471500
472- if ce. arguments . is_empty ( ) {
473- Some ( ctx. ast . move_expression ( & mut member. object ) )
474- } else if can_merge_until. is_some ( ) {
475- Some ( ctx. ast . expression_call (
476- ce. span ,
477- ctx. ast . move_expression ( & mut ce. callee ) ,
478- Option :: < TSTypeParameterInstantiation > :: None ,
479- ctx. ast . move_vec ( & mut ce. arguments ) ,
480- false ,
481- ) )
482- } else {
483- None
501+ let expression_count = ce
502+ . arguments
503+ . iter ( )
504+ . filter ( |arg| !matches ! ( arg, Argument :: StringLiteral ( _) ) )
505+ . count ( ) ;
506+ let string_count = ce. arguments . len ( ) - expression_count;
507+
508+ // whether it is shorter to use `String::concat`
509+ if ".concat()" . len ( ) + ce. arguments . len ( ) + "''" . len ( ) * string_count
510+ < "${}" . len ( ) * expression_count
511+ {
512+ return None ;
513+ }
514+
515+ let mut quasi_strs: Vec < Cow < ' a , str > > =
516+ vec ! [ Cow :: Borrowed ( base_str. value. as_str( ) ) ] ;
517+ let mut expressions = ctx. ast . vec ( ) ;
518+ let mut pushed_quasi = true ;
519+ for argument in ce. arguments . drain ( ..) {
520+ if let Argument :: StringLiteral ( str_lit) = argument {
521+ if pushed_quasi {
522+ let last_quasi = quasi_strs
523+ . last_mut ( )
524+ . expect ( "last element should exist because pushed_quasi is true" ) ;
525+ last_quasi. to_mut ( ) . push_str ( & str_lit. value ) ;
526+ } else {
527+ quasi_strs. push ( Cow :: Borrowed ( str_lit. value . as_str ( ) ) ) ;
528+ }
529+ pushed_quasi = true ;
530+ } else {
531+ if !pushed_quasi {
532+ // need a pair
533+ quasi_strs. push ( Cow :: Borrowed ( "" ) ) ;
534+ }
535+ // checked that all the arguments are expression above
536+ expressions. push ( argument. into_expression ( ) ) ;
537+ pushed_quasi = false ;
538+ }
539+ }
540+ if !pushed_quasi {
541+ quasi_strs. push ( Cow :: Borrowed ( "" ) ) ;
542+ }
543+
544+ if expressions. is_empty ( ) {
545+ debug_assert_eq ! ( quasi_strs. len( ) , 1 ) ;
546+ return Some ( ctx. ast . expression_string_literal (
547+ ce. span ,
548+ quasi_strs. pop ( ) . unwrap ( ) ,
549+ None ,
550+ ) ) ;
551+ }
552+
553+ let mut quasis = ctx. ast . vec_from_iter ( quasi_strs. into_iter ( ) . map ( |s| {
554+ let cooked = s. clone ( ) . into_in ( ctx. ast . allocator ) ;
555+ ctx. ast . template_element (
556+ SPAN ,
557+ false ,
558+ TemplateElementValue {
559+ raw : s
560+ . cow_replace ( "\\ " , "\\ \\ " )
561+ . cow_replace ( "`" , "\\ `" )
562+ . cow_replace ( "${" , "\\ ${" )
563+ . cow_replace ( "\r \n " , "\\ r\n " )
564+ . into_in ( ctx. ast . allocator ) ,
565+ cooked : Some ( cooked) ,
566+ } ,
567+ )
568+ } ) ) ;
569+ if let Some ( last_quasi) = quasis. last_mut ( ) {
570+ last_quasi. tail = true ;
571+ }
572+
573+ debug_assert_eq ! ( quasis. len( ) , expressions. len( ) + 1 ) ;
574+ Some ( ctx. ast . expression_template_literal ( ce. span , quasis, expressions) )
575+ }
576+ _ => None ,
484577 }
485578 }
486579}
@@ -489,12 +582,14 @@ impl<'a> PeepholeReplaceKnownMethods {
489582#[ cfg( test) ]
490583mod test {
491584 use oxc_allocator:: Allocator ;
585+ use oxc_syntax:: es_target:: ESTarget ;
492586
493587 use crate :: tester;
494588
495589 fn test ( source_text : & str , positive : & str ) {
496590 let allocator = Allocator :: default ( ) ;
497- let mut pass = super :: PeepholeReplaceKnownMethods :: new ( ) ;
591+ let target = ESTarget :: ESNext ;
592+ let mut pass = super :: PeepholeReplaceKnownMethods :: new ( target) ;
498593 tester:: test ( & allocator, source_text, positive, & mut pass) ;
499594 }
500595
@@ -1238,13 +1333,13 @@ mod test {
12381333 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
12391334
12401335 // string
1241- fold ( "'1'.concat(1).concat(2,['abc']).concat('abc')" , "'1'.concat(1,2, ['abc'],' abc') " ) ;
1242- fold ( "''.concat(['abc']).concat(1).concat([2,3])" , "''.concat( ['abc'],1, [2,3]) " ) ;
1243- fold_same ( "''.concat(1)" ) ;
1336+ fold ( "'1'.concat(1).concat(2,['abc']).concat('abc')" , "`1${1}${2}${ ['abc']} abc` " ) ;
1337+ fold ( "''.concat(['abc']).concat(1).concat([2,3])" , "`${ ['abc']}${1}${ [2, 3]}` " ) ;
1338+ fold ( "''.concat(1)" , "`${1}` ") ;
12441339
1245- fold ( "var x, y; ''.concat(x).concat(y)" , "var x, y; ''.concat(x, y) " ) ;
1246- fold ( "var y; ''.concat(x).concat(y)" , "var y; ''.concat(x, y) " ) ; // x might have a getter that updates y, but that side effect is preserved correctly
1247- fold ( "var x; ''.concat(x.a).concat(x)" , "var x; ''.concat( x.a, x) " ) ; // x.a might have a getter that updates x, but that side effect is preserved correctly
1340+ fold ( "var x, y; ''.concat(x).concat(y)" , "var x, y; `${x}${y}` " ) ;
1341+ fold ( "var y; ''.concat(x).concat(y)" , "var y; `${x}${y}` " ) ; // x might have a getter that updates y, but that side effect is preserved correctly
1342+ fold ( "var x; ''.concat(x.a).concat(x)" , "var x; `${ x.a}${x}` " ) ; // x.a might have a getter that updates x, but that side effect is preserved correctly
12481343
12491344 // other
12501345 fold_same ( "obj.concat([1,2]).concat(1)" ) ;
@@ -1377,4 +1472,28 @@ mod test {
13771472 test ( "1e99.toString(b)" , "1e99.toString(b)" ) ;
13781473 test ( "/./.toString(b)" , "/./.toString(b)" ) ;
13791474 }
1475+
1476+ #[ test]
1477+ fn test_fold_string_concat ( ) {
1478+ test_same ( "x = ''.concat()" ) ;
1479+ test ( "x = ''.concat(a, b)" , "x = `${a}${b}`" ) ;
1480+ test ( "x = ''.concat(a, b, c)" , "x = `${a}${b}${c}`" ) ;
1481+ test ( "x = ''.concat(a, b, c, d)" , "x = `${a}${b}${c}${d}`" ) ;
1482+ test_same ( "x = ''.concat(a, b, c, d, e)" ) ;
1483+ test ( "x = ''.concat('a')" , "x = 'a'" ) ;
1484+ test ( "x = ''.concat('a', 'b')" , "x = 'ab'" ) ;
1485+ test ( "x = ''.concat('a', 'b', 'c')" , "x = 'abc'" ) ;
1486+ test ( "x = ''.concat('a', 'b', 'c', 'd')" , "x = 'abcd'" ) ;
1487+ test ( "x = ''.concat('a', 'b', 'c', 'd', 'e')" , "x = 'abcde'" ) ;
1488+ test ( "x = ''.concat(a, 'b')" , "x = `${a}b`" ) ;
1489+ test ( "x = ''.concat('a', b)" , "x = `a${b}`" ) ;
1490+ test ( "x = ''.concat(a, 'b', c)" , "x = `${a}b${c}`" ) ;
1491+ test ( "x = ''.concat('a', b, 'c')" , "x = `a${b}c`" ) ;
1492+ test ( "x = ''.concat('a', b, 'c', d, 'e', f, 'g', h, 'i', j, 'k', l, 'm', n, 'o', p, 'q', r, 's', t)" , "x = `a${b}c${d}e${f}g${h}i${j}k${l}m${n}o${p}q${r}s${t}`" ) ;
1493+ test ( "x = ''.concat(a, 1)" , "x = `${a}${1}`" ) ; // inlining 1 is not implemented yet
1494+
1495+ test ( "x = '\\ \\ s'.concat(a)" , "x = `\\ \\ s${a}`" ) ;
1496+ test ( "x = '`'.concat(a)" , "x = `\\ `${a}`" ) ;
1497+ test ( "x = '${'.concat(a)" , "x = `\\ ${${a}`" ) ;
1498+ }
13801499}
0 commit comments