swc_ecma_minifier/compress/pure/
strings.rs

1use std::{borrow::Cow, mem::take};
2
3use swc_atoms::{
4    wtf8::{Wtf8, Wtf8Buf},
5    Atom, Wtf8Atom,
6};
7use swc_common::{util::take::Take, DUMMY_SP};
8use swc_ecma_ast::*;
9use swc_ecma_utils::{ExprExt, Type, Value};
10use Value::Known;
11
12use super::Pure;
13
14impl Pure<'_> {
15    /// This only handles `'foo' + ('bar' + baz) because others are handled by
16    /// expression simplifier.
17    pub(super) fn eval_str_addition(&mut self, e: &mut Expr) {
18        let (span, l_l, r_l, r_r) = match e {
19            Expr::Bin(
20                e @ BinExpr {
21                    op: op!(bin, "+"), ..
22                },
23            ) => match &mut *e.right {
24                Expr::Bin(
25                    r @ BinExpr {
26                        op: op!(bin, "+"), ..
27                    },
28                ) => (e.span, &mut *e.left, &mut *r.left, &mut r.right),
29                _ => return,
30            },
31            _ => return,
32        };
33
34        match l_l.get_type(self.expr_ctx) {
35            Known(Type::Str) => {}
36            _ => return,
37        }
38        match r_l.get_type(self.expr_ctx) {
39            Known(Type::Str) => {}
40            _ => return,
41        }
42
43        let lls = l_l.as_pure_string(self.expr_ctx);
44        let rls = r_l.as_pure_string(self.expr_ctx);
45
46        if let (Known(lls), Known(rls)) = (lls, rls) {
47            self.changed = true;
48            report_change!("evaluate: 'foo' + ('bar' + baz) => 'foobar' + baz");
49
50            let s = lls.into_owned() + &*rls;
51            *e = BinExpr {
52                span,
53                op: op!(bin, "+"),
54                left: s.into(),
55                right: r_r.take(),
56            }
57            .into();
58        }
59    }
60
61    pub(super) fn eval_tpl_as_str(&mut self, e: &mut Expr) {
62        if !self.options.evaluate {
63            return;
64        }
65
66        fn need_unsafe(e: &Expr) -> bool {
67            match e {
68                Expr::Lit(..) => false,
69                Expr::Bin(e) => need_unsafe(&e.left) || need_unsafe(&e.right),
70                _ => true,
71            }
72        }
73
74        let tpl = match e {
75            Expr::Tpl(e) => e,
76            _ => return,
77        };
78
79        if tpl.quasis.len() == 2
80            && (tpl.quasis[0].cooked.is_some() || !tpl.quasis[0].raw.contains('\\'))
81            && tpl.quasis[1].raw.is_empty()
82        {
83            if !self.options.unsafe_passes && need_unsafe(&tpl.exprs[0]) {
84                return;
85            }
86
87            self.changed = true;
88            report_change!("evaluating a template to a string");
89            *e = BinExpr {
90                span: tpl.span,
91                op: op!(bin, "+"),
92                left: tpl.quasis[0]
93                    .cooked
94                    .clone()
95                    .unwrap_or_else(|| tpl.quasis[0].raw.clone().into())
96                    .into(),
97                right: tpl.exprs[0].take(),
98            }
99            .into();
100        }
101    }
102
103    pub(super) fn eval_nested_tpl(&mut self, e: &mut Expr) {
104        let tpl = match e {
105            Expr::Tpl(e) => e,
106            _ => return,
107        };
108
109        if !tpl.exprs.iter().any(|e| match &**e {
110            Expr::Tpl(t) => t
111                .quasis
112                .iter()
113                .all(|q| q.cooked.is_some() && !q.raw.contains('\\')),
114            _ => false,
115        }) {
116            return;
117        }
118
119        self.changed = true;
120        report_change!("evaluating nested template literals");
121
122        let mut new_tpl = Tpl {
123            span: tpl.span,
124            quasis: Default::default(),
125            exprs: Default::default(),
126        };
127        let mut cur_cooked_str = Wtf8Buf::new();
128        let mut cur_raw_str = String::new();
129
130        for idx in 0..(tpl.quasis.len() + tpl.exprs.len()) {
131            if idx % 2 == 0 {
132                let q = tpl.quasis[idx / 2].take();
133
134                cur_cooked_str.push_str(&Str::from_tpl_raw(&q.raw));
135                cur_raw_str.push_str(&q.raw);
136            } else {
137                let mut e = tpl.exprs[idx / 2].take();
138                self.eval_nested_tpl(&mut e);
139
140                match *e {
141                    Expr::Tpl(mut e) => {
142                        // We loop again
143                        //
144                        // I think we can merge this code...
145                        for idx in 0..(e.quasis.len() + e.exprs.len()) {
146                            if idx % 2 == 0 {
147                                let q = e.quasis[idx / 2].take();
148
149                                cur_cooked_str.push_str(Str::from_tpl_raw(&q.raw).as_ref());
150                                cur_raw_str.push_str(&q.raw);
151                            } else {
152                                let cooked = Wtf8Atom::from(&*cur_cooked_str);
153                                let raw = Atom::from(&*cur_raw_str);
154                                cur_cooked_str.clear();
155                                cur_raw_str.clear();
156
157                                new_tpl.quasis.push(TplElement {
158                                    span: DUMMY_SP,
159                                    tail: false,
160                                    cooked: Some(cooked),
161                                    raw,
162                                });
163
164                                let e = e.exprs[idx / 2].take();
165
166                                new_tpl.exprs.push(e);
167                            }
168                        }
169                    }
170                    _ => {
171                        let cooked = Wtf8Atom::from(&*cur_cooked_str);
172                        let raw = Atom::from(&*cur_raw_str);
173                        cur_cooked_str.clear();
174                        cur_raw_str.clear();
175
176                        new_tpl.quasis.push(TplElement {
177                            span: DUMMY_SP,
178                            tail: false,
179                            cooked: Some(cooked),
180                            raw,
181                        });
182
183                        new_tpl.exprs.push(e);
184                    }
185                }
186            }
187        }
188
189        let cooked = Wtf8Atom::from(&*cur_cooked_str);
190        let raw = Atom::from(&*cur_raw_str);
191        new_tpl.quasis.push(TplElement {
192            span: DUMMY_SP,
193            tail: false,
194            cooked: Some(cooked),
195            raw,
196        });
197
198        *e = new_tpl.into();
199    }
200
201    /// Converts template literals to string if `exprs` of [Tpl] is empty.
202    pub(super) fn convert_tpl_to_str(&mut self, e: &mut Expr) {
203        match e {
204            Expr::Tpl(t) if t.quasis.len() == 1 && t.exprs.is_empty() => {
205                if let Some(value) = &t.quasis[0].cooked {
206                    if let Some(value) = value.as_str() {
207                        if value.chars().all(|c| match c {
208                            '\\' => false,
209                            '\u{0020}'..='\u{007e}' => true,
210                            '\n' | '\r' => self.config.force_str_for_tpl,
211                            _ => false,
212                        }) {
213                            report_change!("converting a template literal to a string literal");
214
215                            *e = Lit::Str(Str {
216                                span: t.span,
217                                raw: None,
218                                value: t.quasis[0].cooked.clone().unwrap(),
219                            })
220                            .into();
221                            return;
222                        }
223                    }
224                }
225
226                let c = &t.quasis[0].raw;
227
228                if c.chars().all(|c| match c {
229                    '\u{0020}'..='\u{007e}' => true,
230                    '\n' | '\r' => self.config.force_str_for_tpl,
231                    _ => false,
232                }) && (self.config.force_str_for_tpl
233                    || c.contains("\\`")
234                    || (!c.contains("\\n") && !c.contains("\\r")))
235                    && !c.contains("\\0")
236                    && !c.contains("\\x")
237                    && !c.contains("\\u")
238                {
239                    let value = Str::from_tpl_raw(c);
240
241                    report_change!("converting a template literal to a string literal");
242
243                    *e = Lit::Str(Str {
244                        span: t.span,
245                        raw: None,
246                        value: value.into(),
247                    })
248                    .into();
249                }
250            }
251            _ => {}
252        }
253    }
254
255    /// This compresses a template literal by inlining string literals in
256    /// expresions into quasis.
257    ///
258    /// Note that this pass only cares about string literals and conversion to a
259    /// string literal should be done before calling this pass.
260    pub(super) fn compress_tpl(&mut self, tpl: &mut Tpl) {
261        debug_assert_eq!(tpl.exprs.len() + 1, tpl.quasis.len());
262        let has_str_lit = tpl
263            .exprs
264            .iter()
265            .any(|expr| matches!(&**expr, Expr::Lit(Lit::Str(..))));
266        if !has_str_lit {
267            return;
268        }
269
270        trace_op!("compress_tpl");
271
272        let mut quasis = Vec::new();
273        let mut exprs = Vec::new();
274        let mut cur_raw = String::new();
275        let mut cur_cooked = Some(Wtf8Buf::new());
276
277        for i in 0..(tpl.exprs.len() + tpl.quasis.len()) {
278            if i % 2 == 0 {
279                let i = i / 2;
280                let q = tpl.quasis[i].clone();
281
282                if q.cooked.is_some() {
283                    if let Some(cur_cooked) = &mut cur_cooked {
284                        cur_cooked.push_str("");
285                    }
286                } else {
287                    // If cooked is None, it means that the template literal contains invalid escape
288                    // sequences.
289                    cur_cooked = None;
290                }
291            } else {
292                let i = i / 2;
293                let e = &tpl.exprs[i];
294
295                match &**e {
296                    Expr::Lit(Lit::Str(s)) => {
297                        if cur_cooked.is_none() && s.raw.is_none() {
298                            return;
299                        }
300
301                        if let Some(cur_cooked) = &mut cur_cooked {
302                            cur_cooked.push_str("");
303                        }
304                    }
305                    _ => {
306                        cur_cooked = Some(Wtf8Buf::new());
307                    }
308                }
309            }
310        }
311
312        cur_cooked = Some(Default::default());
313
314        for i in 0..(tpl.exprs.len() + tpl.quasis.len()) {
315            if i % 2 == 0 {
316                let i = i / 2;
317                let q = tpl.quasis[i].take();
318
319                cur_raw.push_str(&q.raw);
320                if let Some(cooked) = q.cooked {
321                    if let Some(cur_cooked) = &mut cur_cooked {
322                        cur_cooked.push_wtf8(&cooked);
323                    }
324                } else {
325                    // If cooked is None, it means that the template literal contains invalid escape
326                    // sequences.
327                    cur_cooked = None;
328                }
329            } else {
330                let i = i / 2;
331                let e = tpl.exprs[i].take();
332
333                match *e {
334                    Expr::Lit(Lit::Str(s)) => {
335                        if let Some(cur_cooked) = &mut cur_cooked {
336                            cur_cooked.push_wtf8(&convert_str_value_to_tpl_cooked(&s.value));
337                        }
338
339                        if let Some(raw) = &s.raw {
340                            if raw.len() >= 2 {
341                                // Exclude quotes
342                                cur_raw
343                                    .push_str(&convert_str_raw_to_tpl_raw(&raw[1..raw.len() - 1]));
344                            }
345                        } else {
346                            cur_raw.push_str(&convert_str_value_to_tpl_raw(&s.value));
347                        }
348                    }
349                    _ => {
350                        quasis.push(TplElement {
351                            span: DUMMY_SP,
352                            tail: true,
353                            cooked: cur_cooked.take().map(From::from),
354                            raw: take(&mut cur_raw).into(),
355                        });
356                        cur_cooked = Some(Wtf8Buf::new());
357
358                        exprs.push(e);
359                    }
360                }
361            }
362        }
363
364        report_change!("compressing template literals");
365
366        quasis.push(TplElement {
367            span: DUMMY_SP,
368            tail: true,
369            cooked: cur_cooked.map(From::from),
370            raw: cur_raw.into(),
371        });
372
373        debug_assert_eq!(exprs.len() + 1, quasis.len());
374
375        tpl.quasis = quasis;
376        tpl.exprs = exprs;
377    }
378
379    /// Called for binary operations with `+`.
380    pub(super) fn concat_tpl(&mut self, l: &mut Expr, r: &mut Expr) {
381        match (&mut *l, &mut *r) {
382            (Expr::Tpl(l), Expr::Lit(Lit::Str(rs))) => {
383                if let Some(raw) = &rs.raw {
384                    if raw.len() <= 2 {
385                        return;
386                    }
387                }
388
389                // Append
390                if let Some(l_last) = l.quasis.last_mut() {
391                    self.changed = true;
392
393                    report_change!(
394                        "template: Concatted a string (`{}`) on rhs of `+` to a template literal",
395                        rs.value
396                    );
397
398                    if let Some(cooked) = &mut l_last.cooked {
399                        let mut c = Wtf8Buf::from(&*cooked);
400                        c.push_wtf8(&convert_str_value_to_tpl_cooked(&rs.value));
401                        *cooked = c.into();
402                    }
403
404                    l_last.raw = format!(
405                        "{}{}",
406                        l_last.raw,
407                        rs.raw
408                            .clone()
409                            .map(|s| convert_str_raw_to_tpl_raw(&s[1..s.len() - 1]))
410                            .unwrap_or_else(|| convert_str_value_to_tpl_raw(&rs.value).into())
411                    )
412                    .into();
413
414                    r.take();
415                }
416            }
417
418            (Expr::Lit(Lit::Str(ls)), Expr::Tpl(r)) => {
419                if let Some(raw) = &ls.raw {
420                    if raw.len() <= 2 {
421                        return;
422                    }
423                }
424
425                // Append
426                if let Some(r_first) = r.quasis.first_mut() {
427                    self.changed = true;
428
429                    report_change!(
430                        "template: Prepended a string (`{}`) on lhs of `+` to a template literal",
431                        ls.value
432                    );
433
434                    if let Some(cooked) = &mut r_first.cooked {
435                        let mut c = Wtf8Buf::new();
436                        c.push_wtf8(&convert_str_value_to_tpl_cooked(&ls.value));
437                        c.push_wtf8(&*cooked);
438                        *cooked = c.into();
439                    }
440
441                    let new: Atom = format!(
442                        "{}{}",
443                        ls.raw
444                            .clone()
445                            .map(|s| convert_str_raw_to_tpl_raw(&s[1..s.len() - 1]))
446                            .unwrap_or_else(|| convert_str_value_to_tpl_raw(&ls.value).into()),
447                        r_first.raw
448                    )
449                    .into();
450                    r_first.raw = new;
451
452                    l.take();
453                }
454            }
455
456            (Expr::Tpl(l), Expr::Tpl(rt)) => {
457                // We prepend the last quasis of l to the first quasis of r.
458                // After doing so, we can append all data of r to l.
459
460                {
461                    let l_last = l.quasis.pop().unwrap();
462                    let r_first = rt.quasis.first_mut().unwrap();
463                    let new: Atom = format!("{}{}", l_last.raw, r_first.raw).into();
464
465                    r_first.raw = new;
466                }
467
468                l.quasis.extend(rt.quasis.take());
469                l.exprs.extend(rt.exprs.take());
470                // Remove r
471                r.take();
472
473                debug_assert!(l.quasis.len() == l.exprs.len() + 1, "{l:?} is invalid");
474                self.changed = true;
475                report_change!("strings: Merged two template literals");
476            }
477            _ => {}
478        }
479    }
480
481    ///
482    /// - `a + 'foo' + 'bar'` => `a + 'foobar'`
483    pub(super) fn concat_str(&mut self, e: &mut Expr) {
484        if let Expr::Bin(
485            bin @ BinExpr {
486                op: op!(bin, "+"), ..
487            },
488        ) = e
489        {
490            if let Expr::Bin(
491                left @ BinExpr {
492                    op: op!(bin, "+"), ..
493                },
494            ) = &mut *bin.left
495            {
496                let type_of_second = left.right.get_type(self.expr_ctx);
497                let type_of_third = bin.right.get_type(self.expr_ctx);
498
499                if let Value::Known(Type::Str) = type_of_second {
500                    if let Value::Known(Type::Str) = type_of_third {
501                        if let Value::Known(second_str) = left.right.as_pure_wtf8(self.expr_ctx) {
502                            if let Value::Known(third_str) = bin.right.as_pure_wtf8(self.expr_ctx) {
503                                let new_str = second_str.into_owned() + &*third_str;
504                                let left_span = left.span;
505
506                                self.changed = true;
507                                report_change!(
508                                    "strings: Concatting `{} + {}` to `{}`",
509                                    second_str,
510                                    third_str,
511                                    new_str
512                                );
513
514                                *e = BinExpr {
515                                    span: bin.span,
516                                    op: op!(bin, "+"),
517                                    left: left.left.take(),
518                                    right: Lit::Str(Str {
519                                        span: left_span,
520                                        raw: None,
521                                        value: new_str.into(),
522                                    })
523                                    .into(),
524                                }
525                                .into();
526                            }
527                        }
528                    }
529                }
530            }
531        }
532    }
533
534    pub(super) fn drop_useless_addition_of_str(&mut self, e: &mut Expr) {
535        if let Expr::Bin(BinExpr {
536            op: op!(bin, "+"),
537            left,
538            right,
539            ..
540        }) = e
541        {
542            let lt = left.get_type(self.expr_ctx);
543            let rt = right.get_type(self.expr_ctx);
544            if let Value::Known(Type::Str) = lt {
545                if let Value::Known(Type::Str) = rt {
546                    match &**left {
547                        Expr::Lit(Lit::Str(Str { value, .. })) if value.is_empty() => {
548                            self.changed = true;
549                            report_change!(
550                                "string: Dropping empty string literal (in lhs) because it does \
551                                 not changes type"
552                            );
553
554                            *e = *right.take();
555                            return;
556                        }
557                        _ => (),
558                    }
559
560                    match &**right {
561                        Expr::Lit(Lit::Str(Str { value, .. })) if value.is_empty() => {
562                            self.changed = true;
563                            report_change!(
564                                "string: Dropping empty string literal (in rhs) because it does \
565                                 not changes type"
566                            );
567
568                            *e = *left.take();
569                        }
570                        _ => (),
571                    }
572                }
573            }
574        }
575    }
576}
577
578pub(super) fn convert_str_value_to_tpl_cooked(value: &Wtf8) -> Cow<Wtf8> {
579    let mut result = Wtf8Buf::default();
580    let mut need_replace = false;
581
582    let mut iter = value.code_points().peekable();
583    while let Some(code_point) = iter.next() {
584        if let Some(ch) = code_point.to_char() {
585            match ch {
586                '\\' => {
587                    if let Some(next) = iter.peek().and_then(|c| c.to_char()) {
588                        match next {
589                            '\\' => {
590                                need_replace = true;
591                                result.push_char('\\');
592                                iter.next();
593                            }
594                            '`' => {
595                                need_replace = true;
596                                result.push_char('`');
597                                iter.next();
598                            }
599                            '$' => {
600                                need_replace = true;
601                                result.push_char('$');
602                                iter.next();
603                            }
604                            _ => result.push_char(ch),
605                        }
606                    } else {
607                        result.push_char(ch);
608                    }
609                }
610                _ => result.push_char(ch),
611            }
612        } else {
613            need_replace = true;
614            result.push_str(&format!("\\u{:04X}", code_point.to_u32()));
615        }
616    }
617
618    if need_replace {
619        result.into()
620    } else {
621        Cow::Borrowed(value)
622    }
623}
624
625pub(super) fn convert_str_value_to_tpl_raw(value: &Wtf8) -> Cow<str> {
626    let mut result = String::default();
627
628    let iter = value.code_points();
629    for code_point in iter {
630        if let Some(ch) = code_point.to_char() {
631            match ch {
632                '\\' => {
633                    result.push_str("\\\\");
634                }
635                '`' => {
636                    result.push_str("\\`");
637                }
638                '$' => {
639                    result.push_str("\\$");
640                }
641                '\n' => {
642                    result.push_str("\\n");
643                }
644                '\r' => {
645                    result.push_str("\\r");
646                }
647                _ => result.push(ch),
648            }
649        } else {
650            result.push_str(&format!("\\u{:04X}", code_point.to_u32()));
651        }
652    }
653
654    result.into()
655}
656
657pub(super) fn convert_str_raw_to_tpl_raw(value: &str) -> Atom {
658    value.replace('`', "\\`").replace('$', "\\$").into()
659}