swc_ecma_compat_bugfixes/
template_literal_caching.rs

1use swc_common::DUMMY_SP;
2use swc_ecma_ast::*;
3use swc_ecma_utils::{prepend_stmt, private_ident, ExprFactory};
4use swc_ecma_visit::{fold_pass, standard_only_fold, Fold, FoldWith};
5use swc_trace_macro::swc_trace;
6
7// Converts destructured parameters with default values to non-shorthand syntax.
8// This fixes the only Tagged Templates-related bug in ES Modules-supporting
9// browsers (Safari 10 & 11).
10//
11// Bug 1: Safari 10/11 doesn't reliably return the same Strings value.
12// The value changes depending on invocation and function optimization state.
13//   function f() { return Object`` }
14//   f() === new f()  // false, should be true.
15//
16// Bug 2: Safari 10/11 use the same cached strings value when the string parts
17// are the same. This behavior comes from an earlier version of the spec, and
18// can cause tricky bugs.
19//   Object``===Object``  // true, should be false.
20//
21// Benchmarks: https://jsperf.com/compiled-tagged-template-performance
22pub fn template_literal_caching() -> impl Pass {
23    fold_pass(TemplateLiteralCaching::default())
24}
25#[derive(Default, Clone)]
26struct TemplateLiteralCaching {
27    decls: Vec<VarDeclarator>,
28    helper_ident: Option<Ident>,
29}
30
31impl TemplateLiteralCaching {
32    fn create_binding(&mut self, name: Ident, init: Option<Expr>) {
33        let init = init.map(Box::new);
34        self.decls.push(VarDeclarator {
35            span: DUMMY_SP,
36            name: name.into(),
37            init,
38            definite: false,
39        })
40    }
41
42    fn create_var_decl(&mut self) -> Option<Stmt> {
43        if !self.decls.is_empty() {
44            return Some(
45                VarDecl {
46                    span: DUMMY_SP,
47                    kind: VarDeclKind::Let,
48                    declare: false,
49                    decls: self.decls.clone(),
50                    ..Default::default()
51                }
52                .into(),
53            );
54        }
55        None
56    }
57}
58
59/// TODO: VisitMut
60#[swc_trace]
61impl Fold for TemplateLiteralCaching {
62    standard_only_fold!();
63
64    fn fold_expr(&mut self, n: Expr) -> Expr {
65        let n = n.fold_children_with(self);
66        match n {
67            Expr::TaggedTpl(n) => {
68                if self.helper_ident.is_none() {
69                    // Create an identity function helper:
70                    //   identity = t => t
71                    let helper_ident = private_ident!("_");
72                    let t = private_ident!("t");
73                    self.helper_ident = Some(helper_ident.clone());
74                    self.create_binding(
75                        helper_ident,
76                        Some(
77                            ArrowExpr {
78                                span: DUMMY_SP,
79                                params: vec![t.clone().into()],
80                                body: Box::new(BlockStmtOrExpr::Expr(t.into())),
81                                is_async: false,
82                                is_generator: false,
83                                ..Default::default()
84                            }
85                            .into(),
86                        ),
87                    )
88                }
89
90                let helper_ident = self.helper_ident.as_ref().unwrap();
91
92                // Use the identity function helper to get a reference to the template's
93                // Strings. We replace all expressions with `0` ensure Strings has
94                // the same shape.   identity`a${0}`
95                let template = TaggedTpl {
96                    span: DUMMY_SP,
97                    tag: helper_ident.clone().into(),
98                    tpl: Box::new(Tpl {
99                        span: DUMMY_SP,
100                        quasis: n.tpl.quasis,
101                        exprs: n.tpl.exprs.iter().map(|_| 0.0.into()).collect(),
102                    }),
103                    ..Default::default()
104                };
105
106                // Install an inline cache at the callsite using the global variable:
107                //   _t || (_t = identity`a${0}`)
108                let t = private_ident!("t");
109                self.create_binding(t.clone(), None);
110                let inline_cache: Expr = BinExpr {
111                    span: DUMMY_SP,
112                    op: op!("||"),
113                    left: t.clone().into(),
114                    right: AssignExpr {
115                        span: DUMMY_SP,
116                        op: op!("="),
117                        left: t.into(),
118                        right: Box::new(Expr::TaggedTpl(template)),
119                    }
120                    .into(),
121                }
122                .into();
123
124                // The original tag function becomes a plain function call.
125                // The expressions omitted from the cached Strings tag are
126                // directly applied as arguments.
127                //   tag(_t || (_t = Object`a${0}`), 'hello')
128                CallExpr {
129                    span: DUMMY_SP,
130                    callee: n.tag.as_callee(),
131                    args: vec![inline_cache.as_arg()]
132                        .into_iter()
133                        .chain(n.tpl.exprs.into_iter().map(|expr| expr.as_arg()))
134                        .collect(),
135                    ..Default::default()
136                }
137                .into()
138            }
139            _ => n,
140        }
141    }
142
143    fn fold_module(&mut self, n: Module) -> Module {
144        let mut body = n.body.fold_children_with(self);
145        if let Some(var) = self.create_var_decl() {
146            prepend_stmt(&mut body, var.into())
147        }
148
149        Module { body, ..n }
150    }
151
152    fn fold_script(&mut self, n: Script) -> Script {
153        let mut body = n.body.fold_children_with(self);
154        if let Some(var) = self.create_var_decl() {
155            prepend_stmt(&mut body, var)
156        }
157
158        Script { body, ..n }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use swc_common::Mark;
165    use swc_ecma_transforms_base::resolver;
166    use swc_ecma_transforms_testing::test;
167
168    use super::*;
169
170    fn tr() -> impl Pass {
171        (
172            resolver(Mark::new(), Mark::new(), false),
173            template_literal_caching(),
174        )
175    }
176
177    test!(
178        ::swc_ecma_parser::Syntax::default(),
179        |_| tr(),
180        single_tag,
181        "t`a`;"
182    );
183
184    test!(
185        ::swc_ecma_parser::Syntax::default(),
186        |_| tr(),
187        single_tag_empty,
188        "x``;"
189    );
190
191    test!(
192        ::swc_ecma_parser::Syntax::default(),
193        |_| tr(),
194        multiple_tags,
195        r#"
196        t`a`;
197        x``;
198        "#
199    );
200
201    test!(
202        ::swc_ecma_parser::Syntax::default(),
203        |_| tr(),
204        function_scoped_tag,
205        "const f = t => t`a`;"
206    );
207
208    test!(
209        ::swc_ecma_parser::Syntax::default(),
210        |_| tr(),
211        dynamic_tag,
212        "fn()``;"
213    );
214
215    test!(
216        ::swc_ecma_parser::Syntax::default(),
217        |_| tr(),
218        dynamic_expressions,
219        "const f = t => t`a${1}b${t}${[\"hello\"]}`;"
220    );
221
222    test!(
223        ::swc_ecma_parser::Syntax::default(),
224        |_| tr(),
225        same_tag_safari_11,
226        "x`a` === x`a`;"
227    );
228
229    test!(
230        ::swc_ecma_parser::Syntax::default(),
231        |_| tr(),
232        shared_strings_safari_11,
233        "x`a` === y`a`;"
234    );
235
236    test!(
237        ::swc_ecma_parser::Syntax::default(),
238        |_| tr(),
239        template_literals,
240        r#"
241        `a`;
242        t(`a`);
243        t;
244        `a`;
245        "#
246    );
247
248    test!(
249        ::swc_ecma_parser::Syntax::default(),
250        |_| tr(),
251        prevent_tag_collision,
252        r#"
253        const _ = 1;
254        t``;
255        "#
256    );
257
258    test!(
259        ::swc_ecma_parser::Syntax::default(),
260        |_| tr(),
261        block_scoped_tag,
262        "for (let t of []) t`a`;"
263    );
264}