swc_ecma_minifier/compress/optimize/
strings.rs

1use swc_atoms::{atom, Atom, Wtf8Atom};
2use swc_common::{util::take::Take, Spanned, SyntaxContext};
3use swc_ecma_ast::*;
4use swc_ecma_utils::{ExprExt, Value::Known};
5
6use super::Optimizer;
7
8impl Optimizer<'_> {
9    pub(super) fn optimize_expr_in_str_ctx_unsafely(&mut self, e: &mut Expr) {
10        if !self.options.unsafe_passes {
11            return;
12        }
13
14        if let Expr::Call(CallExpr {
15            callee: Callee::Expr(callee),
16            args,
17            ..
18        }) = e
19        {
20            if args
21                .iter()
22                .any(|arg| arg.expr.may_have_side_effects(self.ctx.expr_ctx))
23            {
24                return;
25            }
26
27            if callee.is_ident_ref_to("RegExp") && self.options.unsafe_regexp {
28                if args.len() != 1 {
29                    return;
30                }
31
32                self.optimize_expr_in_str_ctx(&mut args[0].expr);
33
34                if let Expr::Lit(Lit::Str(..)) = &*args[0].expr {
35                    self.changed = true;
36                    report_change!("strings: Unsafely reduced `RegExp` call in a string context");
37
38                    *e = *args[0].expr.take();
39                }
40            }
41        }
42    }
43
44    /// Convert expressions to string literal if possible.
45    pub(super) fn optimize_expr_in_str_ctx(&mut self, n: &mut Expr) {
46        match n {
47            Expr::Lit(Lit::Str(..)) => return,
48            Expr::Paren(e) => {
49                self.optimize_expr_in_str_ctx(&mut e.expr);
50                if let Expr::Lit(Lit::Str(..)) = &*e.expr {
51                    *n = *e.expr.take();
52                    self.changed = true;
53                    report_change!("string: Removed a paren in a string context");
54                }
55
56                return;
57            }
58            _ => {}
59        }
60
61        let value = n.as_pure_string(self.ctx.expr_ctx);
62        if let Known(value) = value {
63            let span = n.span();
64
65            self.changed = true;
66            report_change!(
67                "strings: Converted an expression into a string literal (in string context)"
68            );
69            *n = Lit::Str(Str {
70                span,
71                raw: None,
72                value: value.into(),
73            })
74            .into();
75            return;
76        }
77
78        match n {
79            Expr::Lit(Lit::Num(v)) => {
80                self.changed = true;
81                report_change!(
82                    "strings: Converted a numeric literal ({}) into a string literal (in string \
83                     context)",
84                    v.value
85                );
86
87                let value = format!("{:?}", v.value);
88
89                *n = Lit::Str(Str {
90                    span: v.span,
91                    raw: None,
92                    value: value.into(),
93                })
94                .into();
95            }
96
97            Expr::Lit(Lit::Regex(v)) => {
98                if !self.options.evaluate {
99                    return;
100                }
101                self.changed = true;
102                report_change!(
103                    "strings: Converted a regex (/{}/{}) into a string literal (in string context)",
104                    v.exp,
105                    v.flags
106                );
107
108                let value = format!("/{}/{}", v.exp, v.flags);
109
110                *n = Lit::Str(Str {
111                    span: v.span,
112                    raw: None,
113                    value: value.into(),
114                })
115                .into();
116            }
117
118            Expr::Bin(BinExpr {
119                span,
120                op: op!("/"),
121                left,
122                right,
123            }) => {
124                if let (Expr::Lit(Lit::Num(l)), Expr::Lit(Lit::Num(r))) = (&**left, &**right) {
125                    if l.value == 0.0 && r.value == 0.0 {
126                        *n = Ident::new(
127                            atom!("NaN"),
128                            *span,
129                            SyntaxContext::empty().apply_mark(self.marks.unresolved_mark),
130                        )
131                        .into();
132                        self.changed = true;
133                        report_change!("strings: Evaluated 0 / 0 => NaN in string context");
134                    }
135                }
136            }
137
138            _ => {}
139        }
140    }
141
142    /// Convert string literals with escaped newline `'\n'` to template literal
143    /// with newline character.
144    pub(super) fn reduce_escaped_newline_for_str_lit(&mut self, expr: &mut Expr) {
145        if self.options.ecma < EsVersion::Es2015
146            || !self.options.experimental.reduce_escaped_newline
147        {
148            return;
149        }
150
151        if let Expr::Lit(Lit::Str(s)) = expr {
152            if s.value.contains_char('\n') {
153                *expr = Expr::Tpl(Tpl {
154                    span: s.span,
155                    exprs: Default::default(),
156                    quasis: vec![TplElement {
157                        span: s.span,
158                        cooked: Some(s.value.clone()),
159                        raw: convert_str_value_to_tpl_raw(&s.value),
160                        tail: true,
161                    }],
162                });
163                self.changed = true;
164                report_change!("strings: Reduced escaped newline for a string literal");
165            }
166        }
167    }
168}
169
170pub(super) fn convert_str_value_to_tpl_raw(value: &Wtf8Atom) -> Atom {
171    let mut result = String::with_capacity(value.len());
172    let mut code_points = value.code_points().peekable();
173
174    while let Some(code_point) = code_points.next() {
175        if let Some(ch) = code_point.to_char() {
176            // Valid Unicode character
177            match ch {
178                '\\' => result.push_str("\\\\"),
179                '`' => result.push_str("\\`"),
180                '\r' => result.push_str("\\r"),
181                '$' if code_points.peek().and_then(|cp| cp.to_char()) == Some('{') => {
182                    result.push_str("\\${");
183                    code_points.next(); // Consume the '{'
184                }
185                _ => result.push(ch),
186            }
187        } else {
188            // Unparied surrogate, escape as \\uXXXX (two backslashes)
189            result.push_str(&format!("\\\\u{:04X}", code_point.to_u32()));
190        }
191    }
192
193    Atom::new(result)
194}