swc_ecma_transforms_optimization/
json_parse.rs

1use serde_json::Value;
2use swc_atoms::Wtf8Atom;
3use swc_common::{util::take::Take, Spanned, DUMMY_SP};
4use swc_ecma_ast::*;
5use swc_ecma_transforms_base::perf::Parallel;
6use swc_ecma_utils::{calc_literal_cost, member_expr, ExprFactory};
7use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
8
9/// Transform to optimize performance of literals.
10///
11///
12/// This transform converts pure object literals like
13///
14/// ```js
15/// {a: 1, b: 2}
16/// ```
17///
18/// to
19///
20/// ```js
21/// JSON.parse('{"a":1, "b"}')
22/// ```
23///
24/// # Conditions
25/// If any of the conditions below is matched, pure object literal is converter
26/// to `JSON.parse`
27///
28///   - Object literal is deeply nested (threshold: )
29///
30/// See https://github.com/swc-project/swc/issues/409
31pub fn json_parse(min_cost: usize) -> impl Pass {
32    visit_mut_pass(JsonParse { min_cost })
33}
34
35struct JsonParse {
36    pub min_cost: usize,
37}
38
39impl Parallel for JsonParse {
40    fn create(&self) -> Self {
41        JsonParse {
42            min_cost: self.min_cost,
43        }
44    }
45
46    fn merge(&mut self, _: Self) {}
47}
48
49impl Default for JsonParse {
50    fn default() -> Self {
51        JsonParse { min_cost: 1024 }
52    }
53}
54
55impl VisitMut for JsonParse {
56    noop_visit_mut_type!(fail);
57
58    /// Handles parent expressions before child expressions.
59    fn visit_mut_expr(&mut self, expr: &mut Expr) {
60        if self.min_cost == usize::MAX {
61            return;
62        }
63
64        let e = match expr {
65            Expr::Array(..) | Expr::Object(..) => {
66                let (is_lit, cost) = calc_literal_cost(&*expr, false);
67                if is_lit && cost >= self.min_cost {
68                    let value =
69                        serde_json::to_string(&jsonify(expr.take())).unwrap_or_else(|err| {
70                            unreachable!("failed to serialize serde_json::Value as json: {}", err)
71                        });
72
73                    *expr = CallExpr {
74                        span: expr.span(),
75                        callee: member_expr!(Default::default(), DUMMY_SP, JSON.parse).as_callee(),
76                        args: vec![Lit::Str(Str {
77                            span: DUMMY_SP,
78                            raw: None,
79                            value: value.into(),
80                        })
81                        .as_arg()],
82                        ..Default::default()
83                    }
84                    .into();
85                    return;
86                }
87
88                expr
89            }
90            _ => expr,
91        };
92
93        e.visit_mut_children_with(self)
94    }
95}
96
97/// Converts a Wtf8Atom to a JSON-safe string, escaping lone surrogates
98fn wtf8_to_json_string(value: &Wtf8Atom) -> String {
99    if let Some(s) = value.as_str() {
100        // Fast path: valid UTF-8
101        return s.to_string();
102    }
103
104    // Slow path: contains lone surrogates, need to escape them
105    let mut result = String::with_capacity(value.len());
106    for cp in value.as_wtf8().code_points() {
107        if let Some(ch) = cp.to_char() {
108            // Valid Rust char, push directly
109            result.push(ch);
110        } else {
111            // Lone surrogate - escape as \uXXXX
112            use std::fmt::Write;
113            write!(&mut result, "\\u{:04X}", cp.to_u32()).unwrap();
114        }
115    }
116    result
117}
118
119fn jsonify(e: Expr) -> Value {
120    match e {
121        Expr::Object(obj) => Value::Object(
122            obj.props
123                .into_iter()
124                .map(|v| match v {
125                    PropOrSpread::Prop(p) if p.is_key_value() => p.key_value().unwrap(),
126                    _ => unreachable!(),
127                })
128                .map(|p: KeyValueProp| {
129                    let value = jsonify(*p.value);
130                    let key = match p.key {
131                        PropName::Str(s) => wtf8_to_json_string(&s.value),
132                        PropName::Ident(id) => id.sym.to_string(),
133                        PropName::Num(n) => format!("{}", n.value),
134                        _ => unreachable!(),
135                    };
136                    (key, value)
137                })
138                .collect(),
139        ),
140        Expr::Array(arr) => Value::Array(
141            arr.elems
142                .into_iter()
143                .map(|v| jsonify(*v.unwrap().expr))
144                .collect(),
145        ),
146        Expr::Lit(Lit::Str(Str { value, .. })) => Value::String(wtf8_to_json_string(&value)),
147        Expr::Lit(Lit::Num(Number { value, .. })) => {
148            if value.fract() == 0.0 {
149                Value::Number((value as i64).into())
150            } else {
151                match serde_json::Number::from_f64(value) {
152                    Some(n) => Value::Number(n),
153                    None => Value::Number((value as i64).into()),
154                }
155            }
156        }
157        Expr::Lit(Lit::Null(..)) => Value::Null,
158        Expr::Lit(Lit::Bool(v)) => Value::Bool(v.value),
159        Expr::Tpl(Tpl { quasis, .. }) => Value::String(match quasis.first() {
160            Some(TplElement {
161                cooked: Some(value),
162                ..
163            }) => wtf8_to_json_string(value),
164            _ => String::new(),
165        }),
166        _ => unreachable!("jsonify: Expr {:?} cannot be converted to json", e),
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use swc_ecma_transforms_testing::test;
173
174    use super::*;
175
176    test!(
177        ::swc_ecma_parser::Syntax::default(),
178        |_| json_parse(0),
179        simple_object,
180        "let a = {b: 'foo'}"
181    );
182
183    test!(
184        ::swc_ecma_parser::Syntax::default(),
185        |_| json_parse(0),
186        simple_arr,
187        "let a = ['foo']"
188    );
189
190    test!(
191        ::swc_ecma_parser::Syntax::default(),
192        |_| json_parse(0),
193        empty_object,
194        "const a = {};"
195    );
196
197    test!(
198        ::swc_ecma_parser::Syntax::default(),
199        |_| json_parse(15),
200        min_cost_15,
201        "const a = { b: 1, c: 2 };"
202    );
203
204    test!(
205        ::swc_ecma_parser::Syntax::default(),
206        |_| json_parse(0),
207        min_cost_0,
208        "const a = { b: 1, c: 2 };"
209    );
210
211    test!(
212        ::swc_ecma_parser::Syntax::default(),
213        |_| json_parse(0),
214        spread,
215        "const a = { ...a, b: 1 };"
216    );
217
218    test!(
219        ::swc_ecma_parser::Syntax::default(),
220        |_| json_parse(0),
221        object_method,
222        "const a = {
223        method(arg) {
224          return arg;
225        },
226        b: 1
227      };"
228    );
229
230    test!(
231        ::swc_ecma_parser::Syntax::default(),
232        |_| json_parse(0),
233        computed_property,
234        r#"const a = { b : "b_val", ["c"]: "c_val" };"#
235    );
236
237    test!(
238        ::swc_ecma_parser::Syntax::default(),
239        |_| json_parse(0),
240        invalid_numeric_key,
241        r#"const a ={ 77777777777777777.1: "foo" };"#
242    );
243
244    test!(
245        ::swc_ecma_parser::Syntax::default(),
246        |_| json_parse(0),
247        string,
248        r#"const a = { b: "b_val" };"#
249    );
250
251    test!(
252        ::swc_ecma_parser::Syntax::default(),
253        |_| json_parse(0),
254        string_single_quote_1,
255        r#"const a = { b: "'abc'" };"#,
256        ok_if_code_eq
257    );
258
259    test!(
260        ::swc_ecma_parser::Syntax::default(),
261        |_| json_parse(0),
262        string_single_quote_2,
263        r#"const a = { b: "ab\'c" };"#,
264        ok_if_code_eq
265    );
266
267    test!(
268        ::swc_ecma_parser::Syntax::default(),
269        |_| json_parse(0),
270        number,
271        "const a = { b: 1 };"
272    );
273
274    test!(
275        ::swc_ecma_parser::Syntax::default(),
276        |_| json_parse(0),
277        decimal_number,
278        "const a = { b: 24.0197, c: 0.0, d: 1.0 };"
279    );
280
281    test!(
282        ::swc_ecma_parser::Syntax::default(),
283        |_| json_parse(0),
284        null,
285        "const a = { b: null };"
286    );
287
288    test!(
289        ::swc_ecma_parser::Syntax::default(),
290        |_| json_parse(0),
291        boolean,
292        "const a = { b: false };"
293    );
294
295    test!(
296        ::swc_ecma_parser::Syntax::default(),
297        |_| json_parse(0),
298        array,
299        "const a = { b: [1, 'b_val', null] };"
300    );
301
302    test!(
303        ::swc_ecma_parser::Syntax::default(),
304        |_| json_parse(0),
305        nested_array,
306        "const a = { b: [1, ['b_val', { a: 1 }], null] };"
307    );
308
309    test!(
310        ::swc_ecma_parser::Syntax::default(),
311        |_| json_parse(0),
312        object,
313        "const a = { b: { c: 1 } };"
314    );
315
316    test!(
317        ::swc_ecma_parser::Syntax::default(),
318        |_| json_parse(0),
319        object_numeric_keys,
320        r#"const a = { 1: "123", 23: 45, b: "b_val" };"#
321    );
322    test!(
323        ::swc_ecma_parser::Syntax::default(),
324        |_| json_parse(0),
325        tpl,
326        r"const a = [`\x22\x21\x224`];"
327    );
328    test!(
329        ::swc_ecma_parser::Syntax::default(),
330        |_| json_parse(0),
331        tpl2,
332        r#"const a = [`1${b}2`];"#
333    );
334    test!(
335        ::swc_ecma_parser::Syntax::default(),
336        |_| json_parse(0),
337        tpl3,
338        r#"const a = [`1${0}2`];"#
339    );
340}