@@ -95,6 +95,12 @@ fn collect_exprs_from_object(obj: &mut ObjectLit) -> Vec<Box<Expr>> {
9595 exprs
9696}
9797
98+ #[ derive( Debug ) ]
99+ enum GroupType < ' a > {
100+ Literals ( Vec < & ' a ExprOrSpread > ) ,
101+ Expression ( & ' a ExprOrSpread ) ,
102+ }
103+
98104impl Pure < ' _ > {
99105 /// `a = a + 1` => `a += 1`.
100106 pub ( super ) fn compress_bin_assignment_to_left ( & mut self , e : & mut AssignExpr ) {
@@ -501,6 +507,19 @@ impl Pure<'_> {
501507 return ;
502508 }
503509
510+ // Handle empty array case first
511+ if arr. elems . is_empty ( ) {
512+ report_change ! ( "Compressing empty array.join()" ) ;
513+ self . changed = true ;
514+ * e = Lit :: Str ( Str {
515+ span : call. span ,
516+ raw : None ,
517+ value : atom ! ( "" ) ,
518+ } )
519+ . into ( ) ;
520+ return ;
521+ }
522+
504523 let cannot_join_as_str_lit = arr
505524 . elems
506525 . iter ( )
@@ -520,11 +539,19 @@ impl Pure<'_> {
520539 return ;
521540 }
522541
523- if !self . options . unsafe_passes {
542+ // Try partial optimization (grouping consecutive literals)
543+ if let Some ( new_expr) =
544+ self . compress_array_join_partial ( arr. span , & mut arr. elems , & separator)
545+ {
546+ self . changed = true ;
547+ report_change ! ( "Compressing array.join() with partial optimization" ) ;
548+ * e = new_expr;
524549 return ;
525550 }
526551
527- // TODO: Partial join
552+ if !self . options . unsafe_passes {
553+ return ;
554+ }
528555
529556 if arr
530557 . elems
@@ -625,6 +652,277 @@ impl Pure<'_> {
625652 . into ( )
626653 }
627654
655+ /// Performs partial optimization on array.join() when there are mixed
656+ /// literals and expressions. Groups consecutive literals into string
657+ /// concatenations.
658+ fn compress_array_join_partial (
659+ & mut self ,
660+ _span : Span ,
661+ elems : & mut Vec < Option < ExprOrSpread > > ,
662+ separator : & str ,
663+ ) -> Option < Expr > {
664+ if !self . options . evaluate {
665+ return None ;
666+ }
667+
668+ // Check if we have any non-literal elements
669+ let has_non_literals = elems. iter ( ) . flatten ( ) . any ( |elem| match & * elem. expr {
670+ Expr :: Lit ( Lit :: Str ( ..) | Lit :: Num ( ..) | Lit :: Null ( ..) ) => false ,
671+ e if is_pure_undefined ( self . expr_ctx , e) => false ,
672+ _ => true ,
673+ } ) ;
674+
675+ if !has_non_literals {
676+ return None ; // Pure literal case will be handled elsewhere
677+ }
678+
679+ // For non-empty separators, only optimize if we have at least 2 consecutive
680+ // literals This prevents infinite loop and ensures meaningful
681+ // optimization
682+ if !separator. is_empty ( ) {
683+ let mut consecutive_literals = 0 ;
684+ let mut max_consecutive = 0 ;
685+
686+ for elem in elems. iter ( ) . flatten ( ) {
687+ let is_literal = match & * elem. expr {
688+ Expr :: Lit ( Lit :: Str ( ..) | Lit :: Num ( ..) | Lit :: Null ( ..) ) => true ,
689+ e if is_pure_undefined ( self . expr_ctx , e) => true ,
690+ _ => false ,
691+ } ;
692+
693+ if is_literal {
694+ consecutive_literals += 1 ;
695+ max_consecutive = max_consecutive. max ( consecutive_literals) ;
696+ } else {
697+ consecutive_literals = 0 ;
698+ }
699+ }
700+
701+ if max_consecutive < 2 {
702+ return None ;
703+ }
704+
705+ // Only optimize for single-character separators to avoid bloating the code
706+ // Long separators like "really-long-separator" should not be optimized
707+ if separator. len ( ) > 1 {
708+ return None ;
709+ }
710+
711+ // For comma separator, require a higher threshold to avoid infinite loops
712+ if separator == "," && max_consecutive < 6 {
713+ return None ;
714+ }
715+ } else {
716+ // For empty string joins, optimize more aggressively since we're
717+ // doing string concatenation We can always optimize
718+ // these as long as there are mixed expressions and literals
719+ }
720+
721+ // Group consecutive literals and create a string concatenation expression
722+ let mut groups = Vec :: new ( ) ;
723+ let mut current_group = Vec :: new ( ) ;
724+
725+ for elem in elems. iter ( ) . flatten ( ) {
726+ let is_literal = match & * elem. expr {
727+ Expr :: Lit ( Lit :: Str ( ..) | Lit :: Num ( ..) | Lit :: Null ( ..) ) => true ,
728+ e if is_pure_undefined ( self . expr_ctx , e) => true ,
729+ _ => false ,
730+ } ;
731+
732+ if is_literal {
733+ current_group. push ( elem) ;
734+ } else {
735+ if !current_group. is_empty ( ) {
736+ groups. push ( GroupType :: Literals ( current_group) ) ;
737+ current_group = Vec :: new ( ) ;
738+ }
739+ groups. push ( GroupType :: Expression ( elem) ) ;
740+ }
741+ }
742+
743+ if !current_group. is_empty ( ) {
744+ groups. push ( GroupType :: Literals ( current_group) ) ;
745+ }
746+
747+ // If we don't have any grouped literals, no optimization possible
748+ if groups. iter ( ) . all ( |g| matches ! ( g, GroupType :: Expression ( _) ) ) {
749+ return None ;
750+ }
751+
752+ // Handle different separators
753+ let is_string_concat = separator. is_empty ( ) ;
754+
755+ if is_string_concat {
756+ // Convert to string concatenation
757+ let mut result_parts = Vec :: new ( ) ;
758+
759+ // Only add empty string prefix when the first element is a non-string
760+ // expression that needs coercion to string AND there's no string
761+ // literal early enough to provide coercion
762+ let needs_empty_string_prefix = match groups. first ( ) {
763+ Some ( GroupType :: Expression ( first_expr) ) => {
764+ // Check if the first expression is already a string concatenation
765+ let first_needs_coercion = match & * first_expr. expr {
766+ Expr :: Bin ( BinExpr {
767+ op : op ! ( bin, "+" ) , ..
768+ } ) => false , // Already string concat
769+ Expr :: Lit ( Lit :: Str ( ..) ) => false , // Already a string literal
770+ Expr :: Call ( _call) => {
771+ // Function calls may return any type and need string coercion
772+ true
773+ }
774+ _ => true , // Other expressions need string coercion
775+ } ;
776+
777+ // If the first element needs coercion, check if the second element is a string
778+ // literal that can provide the coercion
779+ if first_needs_coercion {
780+ match groups. get ( 1 ) {
781+ Some ( GroupType :: Literals ( _) ) => false , /* String literals will */
782+ // provide coercion
783+ _ => true , // No string literal to provide coercion
784+ }
785+ } else {
786+ false
787+ }
788+ }
789+ _ => false ,
790+ } ;
791+
792+ if needs_empty_string_prefix {
793+ result_parts. push ( Box :: new ( Expr :: Lit ( Lit :: Str ( Str {
794+ span : DUMMY_SP ,
795+ raw : None ,
796+ value : atom ! ( "" ) ,
797+ } ) ) ) ) ;
798+ }
799+
800+ for group in groups {
801+ match group {
802+ GroupType :: Literals ( literals) => {
803+ let mut joined = String :: new ( ) ;
804+ for literal in literals. iter ( ) {
805+ match & * literal. expr {
806+ Expr :: Lit ( Lit :: Str ( s) ) => joined. push_str ( & s. value ) ,
807+ Expr :: Lit ( Lit :: Num ( n) ) => write ! ( joined, "{}" , n. value) . unwrap ( ) ,
808+ Expr :: Lit ( Lit :: Null ( ..) ) => {
809+ // For string concatenation, null becomes
810+ // empty string
811+ }
812+ e if is_pure_undefined ( self . expr_ctx , e) => {
813+ // undefined becomes empty string in string
814+ // context
815+ }
816+ _ => unreachable ! ( ) ,
817+ }
818+ }
819+
820+ result_parts. push ( Box :: new ( Expr :: Lit ( Lit :: Str ( Str {
821+ span : DUMMY_SP ,
822+ raw : None ,
823+ value : joined. into ( ) ,
824+ } ) ) ) ) ;
825+ }
826+ GroupType :: Expression ( expr) => {
827+ result_parts. push ( expr. expr . clone ( ) ) ;
828+ }
829+ }
830+ }
831+
832+ // Create string concatenation expression
833+ if result_parts. len ( ) == 1 {
834+ return Some ( * result_parts. into_iter ( ) . next ( ) . unwrap ( ) ) ;
835+ }
836+
837+ let mut result = * result_parts. remove ( 0 ) ;
838+ for part in result_parts {
839+ result = Expr :: Bin ( BinExpr {
840+ span : DUMMY_SP ,
841+ left : Box :: new ( result) ,
842+ op : op ! ( bin, "+" ) ,
843+ right : part,
844+ } ) ;
845+ }
846+
847+ Some ( result)
848+ } else {
849+ // For non-empty separator, create a more compact array
850+ let mut new_elems = Vec :: new ( ) ;
851+
852+ for group in groups {
853+ match group {
854+ GroupType :: Literals ( literals) => {
855+ let mut joined = String :: new ( ) ;
856+ for ( idx, literal) in literals. iter ( ) . enumerate ( ) {
857+ if idx > 0 {
858+ joined. push_str ( separator) ;
859+ }
860+
861+ match & * literal. expr {
862+ Expr :: Lit ( Lit :: Str ( s) ) => joined. push_str ( & s. value ) ,
863+ Expr :: Lit ( Lit :: Num ( n) ) => write ! ( joined, "{}" , n. value) . unwrap ( ) ,
864+ Expr :: Lit ( Lit :: Null ( ..) ) => {
865+ // null becomes empty string
866+ }
867+ e if is_pure_undefined ( self . expr_ctx , e) => {
868+ // undefined becomes empty string
869+ }
870+ _ => unreachable ! ( ) ,
871+ }
872+ }
873+
874+ new_elems. push ( Some ( ExprOrSpread {
875+ spread : None ,
876+ expr : Box :: new ( Expr :: Lit ( Lit :: Str ( Str {
877+ span : DUMMY_SP ,
878+ raw : None ,
879+ value : joined. into ( ) ,
880+ } ) ) ) ,
881+ } ) ) ;
882+ }
883+ GroupType :: Expression ( expr) => {
884+ new_elems. push ( Some ( ExprOrSpread {
885+ spread : None ,
886+ expr : expr. expr . clone ( ) ,
887+ } ) ) ;
888+ }
889+ }
890+ }
891+
892+ // Create a new array.join() call with the original separator
893+ let new_array = Expr :: Array ( ArrayLit {
894+ span : _span,
895+ elems : new_elems,
896+ } ) ;
897+
898+ // For comma separator, use .join() without arguments (shorter)
899+ let args = if separator == "," {
900+ vec ! [ ]
901+ } else {
902+ vec ! [ ExprOrSpread {
903+ spread: None ,
904+ expr: Box :: new( Expr :: Lit ( Lit :: Str ( Str {
905+ span: DUMMY_SP ,
906+ raw: None ,
907+ value: separator. into( ) ,
908+ } ) ) ) ,
909+ } ]
910+ } ;
911+
912+ Some ( Expr :: Call ( CallExpr {
913+ span : _span,
914+ ctxt : Default :: default ( ) ,
915+ callee : Callee :: Expr ( Box :: new ( Expr :: Member ( MemberExpr {
916+ span : _span,
917+ obj : Box :: new ( new_array) ,
918+ prop : MemberProp :: Ident ( IdentName :: new ( atom ! ( "join" ) , _span) ) ,
919+ } ) ) ) ,
920+ args,
921+ ..Default :: default ( )
922+ } ) )
923+ }
924+ }
925+
628926 pub ( super ) fn drop_undefined_from_return_arg ( & mut self , s : & mut ReturnStmt ) {
629927 if let Some ( e) = s. arg . as_deref ( ) {
630928 if is_pure_undefined ( self . expr_ctx , e) {
0 commit comments