use std::{collections::BTreeMap, fmt::Write};
use serde::{
ser::{SerializeMap, SerializeSeq},
Serialize, Serializer,
};
use serde_json::value::RawValue;
use swc_atoms::Wtf8Atom;
use swc_common::{Spanned, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_transforms_base::perf::Parallel;
use swc_ecma_utils::{calc_literal_cost, member_expr, number::ToJsString, ExprFactory};
use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
pub fn json_parse(min_cost: usize) -> impl Pass {
visit_mut_pass(JsonParse { min_cost })
}
struct JsonParse {
pub min_cost: usize,
}
impl Parallel for JsonParse {
fn create(&self) -> Self {
JsonParse {
min_cost: self.min_cost,
}
}
fn merge(&mut self, _: Self) {}
}
impl Default for JsonParse {
fn default() -> Self {
JsonParse { min_cost: 1024 }
}
}
impl VisitMut for JsonParse {
noop_visit_mut_type!(fail);
fn visit_mut_expr(&mut self, expr: &mut Expr) {
if self.min_cost == usize::MAX {
return;
}
let e = match expr {
Expr::Array(..) | Expr::Object(..) => {
let (is_lit, cost) = calc_literal_cost(&*expr, false);
if is_lit && cost >= self.min_cost {
let span = expr.span();
let value = jsonify(expr);
*expr = CallExpr {
span,
callee: member_expr!(Default::default(), DUMMY_SP, JSON.parse).as_callee(),
args: vec![Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: value.into(),
})
.as_arg()],
..Default::default()
}
.into();
return;
}
expr
}
_ => expr,
};
e.visit_mut_children_with(self)
}
}
fn wtf8_to_json_string(value: &Wtf8Atom) -> String {
if let Some(s) = value.as_str() {
return s.to_string();
}
let mut result = String::with_capacity(value.len());
for cp in value.as_wtf8().code_points() {
if let Some(ch) = cp.to_char() {
result.push(ch);
} else {
write!(&mut result, "\\u{:04X}", cp.to_u32()).unwrap();
}
}
result
}
fn json_number(value: f64) -> serde_json::Number {
let is_i64 = value.fract() == 0.0 && value >= i64::MIN as f64 && value < i64::MAX as f64;
let preserves_zero_sign = value != 0.0 || value.is_sign_positive();
if is_i64 && preserves_zero_sign {
return (value as i64).into();
}
serde_json::Number::from_f64(value)
.unwrap_or_else(|| unreachable!("non-finite numbers require raw JSON serialization"))
}
fn json_key(key: &PropName) -> String {
match key {
PropName::Str(s) => wtf8_to_json_string(&s.value),
PropName::Ident(id) => id.sym.to_string(),
PropName::Num(n) => n.value.to_js_string(),
_ => unreachable!(),
}
}
fn jsonify(e: &Expr) -> String {
serde_json::to_string(&JsonLiteral(e))
.unwrap_or_else(|err| unreachable!("failed to serialize literal as JSON: {err}"))
}
struct JsonLiteral<'a>(&'a Expr);
impl Serialize for JsonLiteral<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
Expr::Object(obj) => {
let mut values = BTreeMap::new();
for prop in &obj.props {
let PropOrSpread::Prop(prop) = prop else {
unreachable!()
};
let Prop::KeyValue(prop) = &**prop else {
unreachable!()
};
values.insert(json_key(&prop.key), &*prop.value);
}
let mut map = serializer.serialize_map(Some(values.len()))?;
for (key, value) in values {
map.serialize_entry(&key, &JsonLiteral(value))?;
}
map.end()
}
Expr::Array(arr) => {
let mut seq = serializer.serialize_seq(Some(arr.elems.len()))?;
for value in &arr.elems {
let value = value.as_ref().unwrap();
seq.serialize_element(&JsonLiteral(value.expr.as_ref()))?;
}
seq.end()
}
Expr::Lit(Lit::Str(Str { value, .. })) => {
wtf8_to_json_string(value).serialize(serializer)
}
Expr::Lit(Lit::Num(Number { value, .. })) if value.is_infinite() => {
let value = if value.is_sign_positive() {
"2e308"
} else {
"-2e308"
};
RawValue::from_string(value.into())
.unwrap()
.serialize(serializer)
}
Expr::Lit(Lit::Num(Number { value, .. })) => json_number(*value).serialize(serializer),
Expr::Lit(Lit::Null(..)) => serializer.serialize_none(),
Expr::Lit(Lit::Bool(v)) => serializer.serialize_bool(v.value),
Expr::Tpl(Tpl { quasis, .. }) => {
let value = match quasis.first() {
Some(TplElement {
cooked: Some(value),
..
}) => wtf8_to_json_string(value),
_ => String::new(),
};
value.serialize(serializer)
}
_ => unreachable!("jsonify: Expr {:?} cannot be converted to json", self.0),
}
}
}
#[cfg(test)]
mod tests {
use swc_ecma_transforms_testing::test;
use super::*;
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
simple_object,
"let a = {b: 'foo'}"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
simple_arr,
"let a = ['foo']"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
empty_object,
"const a = {};"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(15),
min_cost_15,
"const a = { b: 1, c: 2 };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
min_cost_0,
"const a = { b: 1, c: 2 };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
spread,
"const a = { ...a, b: 1 };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
object_method,
"const a = {
method(arg) {
return arg;
},
b: 1
};"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
computed_property,
r#"const a = { b : "b_val", ["c"]: "c_val" };"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
invalid_numeric_key,
r#"const a ={ 77777777777777777.1: "foo" };"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
string,
r#"const a = { b: "b_val" };"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
string_single_quote_1,
r#"const a = { b: "'abc'" };"#,
ok_if_code_eq
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
string_single_quote_2,
r#"const a = { b: "ab\'c" };"#,
ok_if_code_eq
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
number,
"const a = { b: 1 };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
decimal_number,
"const a = { b: 24.0197, c: 0.0, d: 1.0 };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
null,
"const a = { b: null };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
boolean,
"const a = { b: false };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
array,
"const a = { b: [1, 'b_val', null] };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
nested_array,
"const a = { b: [1, ['b_val', { a: 1 }], null] };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
object,
"const a = { b: { c: 1 } };"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
object_numeric_keys,
r#"const a = { 1: "123", 23: 45, b: "b_val" };"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
tpl,
r"const a = [`\x22\x21\x224`];"
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
tpl2,
r#"const a = [`1${b}2`];"#
);
test!(
::swc_ecma_parser::Syntax::default(),
|_| json_parse(0),
tpl3,
r#"const a = [`1${0}2`];"#
);
}