Skip to main content

boa_engine/builtins/json/
mod.rs

1//! Boa's implementation of ECMAScript's global `JSON` object.
2//!
3//! The `JSON` object contains methods for parsing [JavaScript Object Notation (JSON)][spec]
4//! and converting values to JSON. It can't be called or constructed, and aside from its
5//! two method properties, it has no interesting functionality of its own.
6//!
7//! More information:
8//!  - [ECMAScript reference][spec]
9//!  - [MDN documentation][mdn]
10//!  - [JSON specification][json]
11//!
12//! [spec]: https://tc39.es/ecma262/#sec-json
13//! [json]: https://www.json.org/json-en.html
14//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
15
16use std::{borrow::Cow, iter::once};
17
18use rustc_hash::FxHashSet;
19
20use boa_ast::scope::Scope;
21use boa_gc::{Finalize, Gc, Trace};
22use boa_macros::utf16;
23use itertools::Itertools;
24
25use crate::JsExpect;
26use crate::{
27    Context, JsArgs, JsBigInt, JsData, JsResult, JsString, JsValue, SpannedSourceText,
28    builtins::BuiltInObject,
29    bytecompiler::ByteCompiler,
30    context::intrinsics::Intrinsics,
31    error::JsNativeError,
32    js_string,
33    object::{IntegrityLevel, JsObject, internal_methods::InternalMethodPropertyContext},
34    property::{Attribute, PropertyNameKind},
35    realm::Realm,
36    string::{CodePoint, StaticJsStrings},
37    symbol::JsSymbol,
38    value::IntegerOrInfinity,
39    vm::{CallFrame, CallFrameFlags, source_info::SourcePath},
40};
41use boa_parser::{Parser, Source};
42
43use super::{BuiltInBuilder, IntrinsicObject};
44
45/// Converts a nibble (0-15) to its lowercase hex digit as a UTF-16 code unit.
46const fn to_hex_digit(val: u16) -> u16 {
47    match val & 0xF {
48        v @ 0..=9 => b'0' as u16 + v,
49        v => b'a' as u16 + (v - 10),
50    }
51}
52
53#[cfg(test)]
54mod tests;
55
56/// Marker struct for the `[[IsRawJSON]]` internal slot.
57#[derive(Debug, Trace, Finalize, JsData)]
58pub(crate) struct RawJson;
59
60/// A tree node representing the structure of parsed JSON, preserving source
61/// text spans for primitive values. Used to provide `context.source` to
62/// the reviver function in `JSON.parse`.
63#[derive(Debug)]
64enum JsonNode {
65    /// A primitive value (string, number, boolean, null) with its original source text.
66    Primitive(String),
67    /// An array of child nodes.
68    Array(Vec<JsonNode>),
69    /// An object with key-value pairs.
70    Object(Vec<(String, JsonNode)>),
71}
72
73/// Visitor that builds a `JsonNode` source tree by walking the parsed AST,
74/// extracting source text from `LinearSpan` information tracked by the parser.
75/// Uses a stack to build the tree bottom-up during depth-first traversal.
76struct JsonSourceVisitor<'a> {
77    source_text: &'a boa_ast::SourceText,
78    interner: &'a boa_interner::Interner,
79    /// Stack used to build the tree bottom-up. Leaf nodes (literals) are pushed
80    /// first, then parent nodes (arrays/objects) pop their children and push themselves.
81    stack: Vec<JsonNode>,
82}
83
84impl<'a> JsonSourceVisitor<'a> {
85    /// Creates a new `JsonSourceVisitor`.
86    fn new(source_text: &'a boa_ast::SourceText, interner: &'a boa_interner::Interner) -> Self {
87        Self {
88            source_text,
89            interner,
90            stack: Vec::new(),
91        }
92    }
93
94    /// Consumes the visitor and returns the final `JsonNode` tree.
95    fn finish(mut self) -> Option<JsonNode> {
96        self.stack.pop()
97    }
98}
99
100impl<'ast> boa_ast::visitor::Visitor<'ast> for JsonSourceVisitor<'_> {
101    type BreakTy = std::convert::Infallible;
102
103    fn visit_literal(
104        &mut self,
105        node: &'ast boa_ast::expression::literal::Literal,
106    ) -> std::ops::ControlFlow<Self::BreakTy> {
107        let span = node.linear_span();
108        let code_points = self.source_text.get_code_points_from_span(span);
109        let text = String::from_utf16_lossy(code_points);
110        self.stack.push(JsonNode::Primitive(text));
111        std::ops::ControlFlow::Continue(())
112    }
113
114    fn visit_array_literal(
115        &mut self,
116        node: &'ast boa_ast::expression::literal::ArrayLiteral,
117    ) -> std::ops::ControlFlow<Self::BreakTy> {
118        let count = node.as_ref().len();
119        let base = self.stack.len();
120
121        // Visit each element, which pushes child nodes onto the stack
122        for elem in node.as_ref() {
123            if let Some(expr) = elem {
124                self.visit_expression(expr)?;
125            } else {
126                // Sparse element (should not happen in JSON)
127                self.stack.push(JsonNode::Primitive("null".to_string()));
128            }
129        }
130
131        // Pop the children that were just pushed and collect into array
132        let children: Vec<JsonNode> = self.stack.drain(base..).collect();
133        debug_assert_eq!(children.len(), count);
134        self.stack.push(JsonNode::Array(children));
135        std::ops::ControlFlow::Continue(())
136    }
137
138    fn visit_object_literal(
139        &mut self,
140        node: &'ast boa_ast::expression::literal::ObjectLiteral,
141    ) -> std::ops::ControlFlow<Self::BreakTy> {
142        use boa_ast::expression::literal::PropertyDefinition;
143
144        let base = self.stack.len();
145        let mut keys: Vec<String> = Vec::new();
146
147        for prop in node.properties() {
148            if let PropertyDefinition::Property(name, value) = prop {
149                let key = if let Some(ident) = name.literal() {
150                    self.interner.resolve_expect(ident.sym()).to_string()
151                } else {
152                    String::new()
153                };
154                keys.push(key);
155                // Visit the value expression, which pushes the child node
156                self.visit_expression(value)?;
157            }
158        }
159
160        // Pop the value nodes and zip with keys
161        let values: Vec<JsonNode> = self.stack.drain(base..).collect();
162        debug_assert_eq!(keys.len(), values.len());
163        let entries: Vec<(String, JsonNode)> = keys.into_iter().zip(values).collect();
164        self.stack.push(JsonNode::Object(entries));
165        std::ops::ControlFlow::Continue(())
166    }
167
168    fn visit_unary(
169        &mut self,
170        node: &'ast boa_ast::expression::operator::Unary,
171    ) -> std::ops::ControlFlow<Self::BreakTy> {
172        use boa_ast::expression::operator::unary::UnaryOp;
173        use boa_ast::visitor::VisitWith;
174
175        if node.op() == UnaryOp::Minus
176            && let boa_ast::Expression::Literal(lit) = node.target()
177        {
178            let span = lit.linear_span();
179            let code_points = self.source_text.get_code_points_from_span(span);
180            let num_text = String::from_utf16_lossy(code_points);
181            self.stack.push(JsonNode::Primitive(format!("-{num_text}")));
182            return std::ops::ControlFlow::Continue(());
183        }
184
185        // Default: recurse into children
186        node.visit_with(self)
187    }
188
189    fn visit_parenthesized(
190        &mut self,
191        node: &'ast boa_ast::expression::Parenthesized,
192    ) -> std::ops::ControlFlow<Self::BreakTy> {
193        // Unwrap parentheses and visit inner expression
194        self.visit_expression(node.expression())
195    }
196}
197
198/// JavaScript `JSON` global object.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
200pub(crate) struct Json;
201
202impl IntrinsicObject for Json {
203    fn init(realm: &Realm) {
204        let to_string_tag = JsSymbol::to_string_tag();
205        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
206
207        BuiltInBuilder::with_intrinsic::<Self>(realm)
208            .static_method(Self::parse, js_string!("parse"), 2)
209            .static_method(Self::stringify, js_string!("stringify"), 3)
210            .static_method(Self::raw_json, js_string!("rawJSON"), 1)
211            .static_method(Self::is_raw_json, js_string!("isRawJSON"), 1)
212            .static_property(to_string_tag, Self::NAME, attribute)
213            .build();
214    }
215
216    fn get(intrinsics: &Intrinsics) -> JsObject {
217        intrinsics.objects().json()
218    }
219}
220
221impl BuiltInObject for Json {
222    const NAME: JsString = StaticJsStrings::JSON;
223}
224
225impl Json {
226    /// `JSON.parse( text[, reviver] )`
227    ///
228    /// This `JSON` method parses a JSON string, constructing the JavaScript value or object described by the string.
229    ///
230    /// An optional `reviver` function can be provided to perform a transformation on the resulting object before it is returned.
231    ///
232    /// More information:
233    ///  - [ECMAScript reference][spec]
234    ///  - [MDN documentation][mdn]
235    ///
236    /// [spec]: https://tc39.es/ecma262/#sec-json.parse
237    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
238    pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
239        // 1. Let jsonString be ? ToString(text).
240        let json_string = args
241            .first()
242            .cloned()
243            .unwrap_or_default()
244            .to_string(context)?
245            .to_std_string()
246            .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;
247
248        // 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404.
249        //    Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification.
250        if let Err(e) = serde_json::from_str::<serde_json::Value>(&json_string) {
251            return Err(JsNativeError::syntax().with_message(e.to_string()).into());
252        }
253
254        // Check if a reviver is provided, to determine if we need source text tracking
255        let has_reviver = args.get_or_undefined(1).is_callable();
256
257        // 3. Let scriptString be the string-concatenation of "(", jsonString, and ");".
258        let script_string = format!("({json_string});");
259
260        // 4-10. Parse and evaluate the script
261        let source = Source::from_bytes(&script_string);
262        let mut parser = Parser::new(source);
263        parser.set_json_parse();
264
265        // Use parse_script_with_source to get SourceText for span-based source extraction
266        let (script, source_text) =
267            parser.parse_script_with_source(&Scope::new_global(), context.interner_mut())?;
268
269        // Build the source tree from the AST if a reviver is present.
270        // This walks the parsed AST and extracts source text from LinearSpan information,
271        // avoiding the need to re-parse the JSON string separately.
272        let source_tree = if has_reviver {
273            let expr = script.statements().statements().first().and_then(|item| {
274                if let boa_ast::StatementListItem::Statement(stmt) = item
275                    && let boa_ast::Statement::Expression(expr) = stmt.as_ref()
276                {
277                    return Some(expr);
278                }
279                None
280            });
281            expr.and_then(|e| {
282                use boa_ast::visitor::Visitor;
283                let mut visitor = JsonSourceVisitor::new(&source_text, context.interner());
284                let _ = visitor.visit_expression(e);
285                visitor.finish()
286            })
287        } else {
288            None
289        };
290
291        let code_block = {
292            let in_with = context.vm.frame().environments.has_object_environment();
293            let spanned_source_text = SpannedSourceText::new_source_only(
294                crate::spanned_source_text::SourceText::new(source_text),
295            );
296            let mut compiler = ByteCompiler::new(
297                js_string!("<json>"),
298                script.strict(),
299                true,
300                context.realm().scope().clone(),
301                context.realm().scope().clone(),
302                false,
303                false,
304                context.interner_mut(),
305                in_with,
306                spanned_source_text,
307                SourcePath::Json,
308            );
309            compiler.compile_statement_list(script.statements(), true, false);
310            Gc::new(compiler.finish())
311        };
312
313        let realm = context.realm().clone();
314
315        let env_fp = context.vm.frame().environments.len() as u32;
316        context.vm.push_frame_with_stack(
317            CallFrame::new(
318                code_block,
319                None,
320                context.vm.frame().environments.clone(),
321                realm,
322            )
323            .with_env_fp(env_fp)
324            .with_flags(CallFrameFlags::EXIT_EARLY),
325            JsValue::undefined(),
326            JsValue::null(),
327        );
328
329        context.realm().resize_global_env();
330        let record = context.run();
331        context.vm.pop_frame();
332
333        let unfiltered = record.consume()?;
334
335        // 11. If IsCallable(reviver) is true, then
336        if let Some(obj) = args.get_or_undefined(1).as_callable() {
337            // a. Let root be ! OrdinaryObjectCreate(%Object.prototype%).
338            let root = JsObject::with_object_proto(context.intrinsics());
339
340            // b. Let rootName be the empty String.
341            // c. Perform ! CreateDataPropertyOrThrow(root, rootName, unfiltered).
342            root.create_data_property_or_throw(js_string!(), unfiltered, context)
343                .js_expect("CreateDataPropertyOrThrow should never throw here")?;
344
345            // d. Return ? InternalizeJSONProperty(root, rootName, reviver).
346            Self::internalize_json_property(
347                &root,
348                js_string!(),
349                &obj,
350                source_tree.as_ref(),
351                context,
352            )
353        } else {
354            // 12. Else,
355            // a. Return unfiltered.
356            Ok(unfiltered)
357        }
358    }
359
360    /// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )`
361    ///
362    /// More information:
363    ///  - [ECMAScript reference][spec]
364    ///
365    /// [spec]: https://tc39.es/ecma262/#sec-internalizejsonproperty
366    fn internalize_json_property(
367        holder: &JsObject,
368        name: JsString,
369        reviver: &JsObject,
370        source_node: Option<&JsonNode>,
371        context: &mut Context,
372    ) -> JsResult<JsValue> {
373        // 1. Let val be ? Get(holder, name).
374        let val = holder.get(name.clone(), context)?;
375
376        // 2. If Type(val) is Object, then
377        if let Some(obj) = val.as_object() {
378            // a. Let isArray be ? IsArray(val).
379            // b. If isArray is true, then
380            if obj.is_array_abstract()? {
381                // i. Let I be 0.
382                // ii. Let len be ? LengthOfArrayLike(val).
383                // iii. Repeat, while I < len,
384                let len = obj.length_of_array_like(context)? as i64;
385                let children = match source_node {
386                    Some(JsonNode::Array(children)) => Some(children),
387                    _ => None,
388                };
389                for i in 0..len {
390                    let child_node = children.and_then(|c| c.get(i as usize));
391                    // 1. Let prop be ! ToString(𝔽(I)).
392                    // 2. Let newElement be ? InternalizeJSONProperty(val, prop, reviver).
393                    let new_element = Self::internalize_json_property(
394                        &obj,
395                        i.into(),
396                        reviver,
397                        child_node,
398                        context,
399                    )?;
400
401                    // 3. If newElement is undefined, then
402                    if new_element.is_undefined() {
403                        // a. Perform ? val.[[Delete]](prop).
404                        obj.__delete__(
405                            &i.into(),
406                            &mut InternalMethodPropertyContext::new(context),
407                        )?;
408                    }
409                    // 4. Else,
410                    else {
411                        // a. Perform ? CreateDataProperty(val, prop, newElement).
412                        obj.create_data_property(i, new_element, context)?;
413                    }
414                }
415            }
416            // c. Else,
417            else {
418                // i. Let keys be ? EnumerableOwnPropertyNames(val, key).
419                let keys = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?;
420                let entries = match source_node {
421                    Some(JsonNode::Object(entries)) => Some(entries),
422                    _ => None,
423                };
424
425                // ii. For each String P of keys, do
426                for p in keys {
427                    // This is safe, because EnumerableOwnPropertyNames with 'key' type only returns strings.
428                    let p = p
429                        .as_string()
430                        .js_expect("EnumerableOwnPropertyNames only returns strings")?;
431
432                    let p_std = p.to_std_string_escaped();
433                    let child_node =
434                        entries.and_then(|e| e.iter().rfind(|(k, _)| k == &p_std).map(|(_, v)| v));
435
436                    // 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver).
437                    let new_element = Self::internalize_json_property(
438                        &obj,
439                        p.clone(),
440                        reviver,
441                        child_node,
442                        context,
443                    )?;
444
445                    // 2. If newElement is undefined, then
446                    if new_element.is_undefined() {
447                        // a. Perform ? val.[[Delete]](P).
448                        obj.__delete__(
449                            &p.into(),
450                            &mut InternalMethodPropertyContext::new(context),
451                        )?;
452                    }
453                    // 3. Else,
454                    else {
455                        // a. Perform ? CreateDataProperty(val, P, newElement).
456                        obj.create_data_property(p, new_element, context)?;
457                    }
458                }
459            }
460        }
461
462        // Build the context object for the reviver call.
463        // For primitive JSON values: context = { source: "<original text>" }
464        // For objects/arrays or modified values: context = {} (no source property)
465        // Per spec, source is only provided when the value is still the same
466        // primitive that was produced by parsing the original JSON text.
467        let ctx_obj = JsObject::with_object_proto(context.intrinsics());
468        if let Some(JsonNode::Primitive(source_text)) = source_node {
469            // Check if the current value matches what the source text produces.
470            // If the reviver modified the value, it won't match and we skip source.
471            let value_matches = match source_text.as_str() {
472                "null" => val.is_null(),
473                "true" => val.as_boolean() == Some(true),
474                "false" => val.as_boolean() == Some(false),
475                s if s.starts_with('"') => {
476                    // String: compare parsed string value
477                    if let Some(js_str) = val.as_string() {
478                        serde_json::from_str::<String>(s)
479                            .is_ok_and(|parsed| js_str.to_std_string_escaped() == parsed)
480                    } else {
481                        false
482                    }
483                }
484                s => {
485                    // Number: compare parsed number value
486                    // Exact comparison is intentional: we want to detect if the
487                    // value was replaced by the reviver, not approximate equality.
488                    #[allow(clippy::float_cmp)]
489                    if let Some(n) = val.as_number() {
490                        s.parse::<f64>().is_ok_and(|parsed| n == parsed)
491                    } else {
492                        false
493                    }
494                }
495            };
496            if value_matches {
497                ctx_obj.create_data_property_or_throw(
498                    js_string!("source"),
499                    JsValue::from(js_string!(source_text.as_str())),
500                    context,
501                )?;
502            }
503        }
504
505        // 3. Return ? Call(reviver, holder, « name, val, context »).
506        reviver.call(
507            &holder.clone().into(),
508            &[name.into(), val, ctx_obj.into()],
509            context,
510        )
511    }
512
513    /// `JSON.rawJSON ( text )`
514    ///
515    /// Creates a raw JSON object from the given text.
516    ///
517    /// More information:
518    ///  - [ECMAScript reference][spec]
519    ///
520    /// [spec]: https://tc39.es/ecma262/#sec-json.rawjson
521    pub(crate) fn raw_json(
522        _: &JsValue,
523        args: &[JsValue],
524        context: &mut Context,
525    ) -> JsResult<JsValue> {
526        // 1. Let jsonString be ? ToString(text).
527        let json_string = args.get_or_undefined(0).to_string(context)?;
528
529        // 2. Throw a SyntaxError exception if jsonString is the empty String, or if
530        //    either the first or last code unit of jsonString is any of 0x0009
531        //    (CHARACTER TABULATION), 0x000A (LINE FEED), 0x000D (CARRIAGE RETURN), or
532        //    0x0020 (SPACE).
533        let std_string = json_string
534            .to_std_string()
535            .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;
536
537        if std_string.is_empty() {
538            return Err(JsNativeError::syntax()
539                .with_message("JSON.rawJSON text must not be the empty string")
540                .into());
541        }
542
543        let first = std_string.as_bytes()[0];
544        let last = std_string.as_bytes()[std_string.len() - 1];
545        if matches!(first, b'\t' | b'\n' | b'\r' | b' ')
546            || matches!(last, b'\t' | b'\n' | b'\r' | b' ')
547        {
548            return Err(JsNativeError::syntax()
549                .with_message("JSON.rawJSON text must not start or end with whitespace")
550                .into());
551        }
552
553        // 3. Parse StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404.
554        //    Throw a SyntaxError exception if it is not a valid JSON text as defined in that
555        //    specification, or if its outermost value is an object or array.
556        let parsed: serde_json::Value = serde_json::from_str(&std_string)
557            .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?;
558
559        // Must not be an object or array
560        match parsed {
561            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
562                return Err(JsNativeError::syntax()
563                    .with_message("JSON.rawJSON text must not be an object or array")
564                    .into());
565            }
566            _ => {}
567        }
568
569        // 3. Let internalSlotsList be « [[IsRawJSON]] ».
570        // 4. Let obj be OrdinaryObjectCreate(null, internalSlotsList).
571        let obj = JsObject::from_proto_and_data(None::<JsObject>, RawJson);
572
573        // 5. Perform ! CreateDataPropertyOrThrow(obj, "rawJSON", jsonString).
574        obj.create_data_property_or_throw(js_string!("rawJSON"), json_string, context)
575            .js_expect("CreateDataPropertyOrThrow should never throw here")?;
576
577        // 6. Perform ! SetIntegrityLevel(obj, frozen).
578        obj.set_integrity_level(IntegrityLevel::Frozen, context)
579            .js_expect("SetIntegrityLevel should never throw here")?;
580
581        // 7. Return obj.
582        Ok(obj.into())
583    }
584
585    /// `JSON.isRawJSON ( O )`
586    ///
587    /// Checks if the given value is a raw JSON object.
588    ///
589    /// More information:
590    ///  - [ECMAScript reference][spec]
591    ///
592    /// [spec]: https://tc39.es/ecma262/#sec-json.israwjson
593    #[allow(clippy::unnecessary_wraps)]
594    pub(crate) fn is_raw_json(
595        _: &JsValue,
596        args: &[JsValue],
597        _context: &mut Context,
598    ) -> JsResult<JsValue> {
599        // 1. If Type(O) is Object and O has an [[IsRawJSON]] internal slot, return true.
600        // 2. Return false.
601        let value = args.get_or_undefined(0);
602        let result = value.as_object().is_some_and(|obj| obj.is::<RawJson>());
603        Ok(result.into())
604    }
605
606    /// `JSON.stringify( value[, replacer[, space]] )`
607    ///
608    /// This `JSON` method converts a JavaScript object or value to a JSON string.
609    ///
610    /// This method optionally replaces values if a `replacer` function is specified or
611    /// optionally including only the specified properties if a replacer array is specified.
612    ///
613    /// An optional `space` argument can be supplied of type `String` or `Number` that's used to insert
614    /// white space into the output JSON string for readability purposes.
615    ///
616    /// More information:
617    ///  - [ECMAScript reference][spec]
618    ///  - [MDN documentation][mdn]
619    ///
620    /// [spec]: https://tc39.es/ecma262/#sec-json.stringify
621    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
622    pub(crate) fn stringify(
623        _: &JsValue,
624        args: &[JsValue],
625        context: &mut Context,
626    ) -> JsResult<JsValue> {
627        // 1. Let indent be the empty String.
628        let indent = js_string!();
629
630        // 2. Let PropertyList and ReplacerFunction be undefined.
631        let mut property_list = None;
632        let mut replacer_function = None;
633
634        let replacer = args.get_or_undefined(1);
635
636        // 4. If Type(replacer) is Object, then
637        if let Some(replacer_obj) = replacer.as_object() {
638            // a. If IsCallable(replacer) is true, then
639            if replacer_obj.is_callable() {
640                // i. Set ReplacerFunction to replacer.
641                replacer_function = Some(replacer_obj.clone());
642            // b. Else,
643            } else {
644                // i. Let isArray be ? IsArray(replacer).
645                // ii. If isArray is true, then
646                if replacer_obj.is_array_abstract()? {
647                    // 1. Set PropertyList to a new empty List.
648                    let mut property_set = indexmap::IndexSet::new();
649
650                    // 2. Let len be ? LengthOfArrayLike(replacer).
651                    let len = replacer_obj.length_of_array_like(context)?;
652
653                    // 3. Let k be 0.
654                    let mut k = 0;
655
656                    // 4. Repeat, while k < len,
657                    while k < len {
658                        // a. Let prop be ! ToString(𝔽(k)).
659                        // b. Let v be ? Get(replacer, prop).
660                        let v = replacer_obj.get(k, context)?;
661
662                        // c. Let item be undefined.
663                        // d. If Type(v) is String, set item to v.
664                        // e. Else if Type(v) is Number, set item to ! ToString(v).
665                        // f. Else if Type(v) is Object, then
666                        // g. If item is not undefined and item is not currently an element of PropertyList, then
667                        // i. Append item to the end of PropertyList.
668                        if let Some(s) = v.as_string() {
669                            property_set.insert(s);
670                        } else if v.is_number() {
671                            property_set.insert(
672                                v.to_string(context)
673                                    .js_expect("ToString cannot fail on number value")?,
674                            );
675                        } else if let Some(obj) = v.as_object()
676                            && (obj.is::<JsString>() || obj.is::<f64>())
677                        {
678                            // i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v).
679                            property_set.insert(v.to_string(context)?);
680                        }
681
682                        // h. Set k to k + 1.
683                        k += 1;
684                    }
685                    property_list = Some(property_set.into_iter().collect());
686                }
687            }
688        }
689
690        let mut space = args.get_or_undefined(2).clone();
691
692        // 5. If Type(space) is Object, then
693        if let Some(space_obj) = space.as_object() {
694            // a. If space has a [[NumberData]] internal slot, then
695            if space_obj.is::<f64>() {
696                // i. Set space to ? ToNumber(space).
697                space = space.to_number(context)?.into();
698            }
699            // b. Else if space has a [[StringData]] internal slot, then
700            else if space_obj.is::<JsString>() {
701                // i. Set space to ? ToString(space).
702                space = space.to_string(context)?.into();
703            }
704        }
705
706        // 6. If Type(space) is Number, then
707        let gap = if space.is_number() {
708            // a. Let spaceMV be ! ToIntegerOrInfinity(space).
709            // b. Set spaceMV to min(10, spaceMV).
710            // c. If spaceMV < 1, let gap be the empty String; otherwise let gap be the String value containing spaceMV occurrences of the code unit 0x0020 (SPACE).
711            match space
712                .to_integer_or_infinity(context)
713                .js_expect("ToIntegerOrInfinity cannot fail on number")?
714            {
715                IntegerOrInfinity::PositiveInfinity => js_string!("          "),
716                IntegerOrInfinity::NegativeInfinity => js_string!(),
717                IntegerOrInfinity::Integer(i) if i < 1 => js_string!(),
718                IntegerOrInfinity::Integer(i) => {
719                    let mut s = String::new();
720                    let i = std::cmp::min(10, i);
721                    for _ in 0..i {
722                        s.push(' ');
723                    }
724                    s.into()
725                }
726            }
727        // 7. Else if Type(space) is String, then
728        } else if let Some(s) = space.as_string() {
729            // a. If the length of space is 10 or less, let gap be space; otherwise let gap be the substring of space from 0 to 10.
730            s.get(..10).unwrap_or(s)
731        // 8. Else,
732        } else {
733            // a. Let gap be the empty String.
734            js_string!()
735        };
736
737        // 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%).
738        let wrapper = JsObject::with_object_proto(context.intrinsics());
739
740        // 10. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value).
741        wrapper
742            .create_data_property_or_throw(js_string!(), args.get_or_undefined(0).clone(), context)
743            .js_expect("CreateDataPropertyOrThrow should never fail here")?;
744
745        // 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }.
746        let mut state = StateRecord {
747            replacer_function,
748            stack_set: FxHashSet::default(),
749            indent,
750            gap,
751            property_list,
752        };
753
754        // 12. Return ? SerializeJSONProperty(state, the empty String, wrapper).
755        Ok(
756            Self::serialize_json_property(&mut state, js_string!(), &wrapper, context)?
757                .map(Into::into)
758                .unwrap_or_default(),
759        )
760    }
761
762    /// `25.5.2.1 SerializeJSONProperty ( state, key, holder )`
763    ///
764    /// More information:
765    ///  - [ECMAScript reference][spec]
766    ///
767    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonproperty
768    fn serialize_json_property(
769        state: &mut StateRecord,
770        key: JsString,
771        holder: &JsObject,
772        context: &mut Context,
773    ) -> JsResult<Option<JsString>> {
774        // 1. Let value be ? Get(holder, key).
775        let mut value = holder.get(key.clone(), context)?;
776
777        // 2. If Type(value) is Object or BigInt, then
778        if value.is_object() || value.is_bigint() {
779            // a. Let toJSON be ? GetV(value, "toJSON").
780            let to_json = value.get_v(js_string!("toJSON"), context)?;
781
782            // b. If IsCallable(toJSON) is true, then
783            if let Some(obj) = to_json.as_callable() {
784                // i. Set value to ? Call(toJSON, value, « key »).
785                value = obj.call(&value, &[key.clone().into()], context)?;
786            }
787        }
788
789        // 3. If state.[[ReplacerFunction]] is not undefined, then
790        if let Some(obj) = &state.replacer_function {
791            // a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »).
792            value = obj.call(&holder.clone().into(), &[key.into(), value], context)?;
793        }
794
795        // 4. If Type(value) is Object, then
796        if let Some(obj) = value.as_object() {
797            // a. If value has a [[NumberData]] internal slot, then
798            if obj.is::<f64>() {
799                // i. Set value to ? ToNumber(value).
800                value = value.to_number(context)?.into();
801            }
802            // b. Else if value has a [[StringData]] internal slot, then
803            else if obj.is::<JsString>() {
804                // i. Set value to ? ToString(value).
805                value = value.to_string(context)?.into();
806            }
807            // c. Else if value has a [[BooleanData]] internal slot, then
808            else if let Some(boolean) = obj.downcast_ref::<bool>() {
809                // i. Set value to value.[[BooleanData]].
810                value = (*boolean).into();
811            }
812            // d. Else if value has a [[BigIntData]] internal slot, then
813            else if let Some(bigint) = obj.downcast_ref::<JsBigInt>() {
814                // i. Set value to value.[[BigIntData]].
815                value = bigint.clone().into();
816            }
817            // e. Else if value has a [[IsRawJSON]] internal slot, then
818            else if obj.is::<RawJson>() {
819                // i. Return the value of the "rawJSON" property of value.
820                let raw = obj.get(js_string!("rawJSON"), context)?;
821                return Ok(raw.as_string());
822            }
823        }
824
825        // 5. If value is null, return "null".
826        if value.is_null() {
827            return Ok(Some(js_string!("null")));
828        }
829
830        // 6. If value is true, return "true".
831        // 7. If value is false, return "false".
832        if value.is_boolean() {
833            return Ok(Some(js_string!(if value.to_boolean() {
834                "true"
835            } else {
836                "false"
837            })));
838        }
839
840        // 8. If Type(value) is String, return QuoteJSONString(value).
841        if let Some(s) = value.as_string() {
842            return Ok(Some(Self::quote_json_string(&s)));
843        }
844
845        // 9. If Type(value) is Number, then
846        if let Some(n) = value.as_number() {
847            // a. If value is finite, return ! ToString(value).
848            if n.is_finite() {
849                return Ok(Some(
850                    value
851                        .to_string(context)
852                        .js_expect("ToString should never fail here")?,
853                ));
854            }
855
856            // b. Return "null".
857            return Ok(Some(js_string!("null")));
858        }
859
860        // 10. If Type(value) is BigInt, throw a TypeError exception.
861        if value.is_bigint() {
862            return Err(JsNativeError::typ()
863                .with_message("cannot serialize bigint to JSON")
864                .into());
865        }
866
867        // 11. If Type(value) is Object and IsCallable(value) is false, then
868        if let Some(obj) = value.as_object()
869            && !obj.is_callable()
870        {
871            // a. Let isArray be ? IsArray(value).
872            // b. If isArray is true, return ? SerializeJSONArray(state, value).
873            // c. Return ? SerializeJSONObject(state, value).
874            return if obj.is_array_abstract()? {
875                Ok(Some(Self::serialize_json_array(state, &obj, context)?))
876            } else {
877                Ok(Some(Self::serialize_json_object(state, &obj, context)?))
878            };
879        }
880
881        // 12. Return undefined.
882        Ok(None)
883    }
884
885    /// `25.5.2.2 QuoteJSONString ( value )`
886    ///
887    /// More information:
888    ///  - [ECMAScript reference][spec]
889    ///
890    /// [spec]: https://tc39.es/ecma262/#sec-quotejsonstring
891    fn quote_json_string(value: &JsString) -> JsString {
892        let mut buf = [0; 2];
893        // 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK).
894        let mut product = Vec::with_capacity(value.len() + 2);
895        product.push('"' as u16);
896
897        // 2. For each code point C of ! StringToCodePoints(value), do
898        for code_point in value.code_points() {
899            match code_point {
900                // a. If C is listed in the "Code Point" column of Table 73, then
901                // i. Set product to the string-concatenation of product and the
902                // escape sequence for C as specified in the "Escape Sequence"
903                // column of the corresponding row.
904                CodePoint::Unicode('\u{0008}') => product.extend_from_slice(utf16!(r"\b")),
905                CodePoint::Unicode('\u{0009}') => product.extend_from_slice(utf16!(r"\t")),
906                CodePoint::Unicode('\u{000A}') => product.extend_from_slice(utf16!(r"\n")),
907                CodePoint::Unicode('\u{000C}') => product.extend_from_slice(utf16!(r"\f")),
908                CodePoint::Unicode('\u{000D}') => product.extend_from_slice(utf16!(r"\r")),
909                CodePoint::Unicode('\u{0022}') => product.extend_from_slice(utf16!(r#"\""#)),
910                CodePoint::Unicode('\u{005C}') => product.extend_from_slice(utf16!(r"\\")),
911                // b. Else if C has a numeric value less than 0x0020 (SPACE), or
912                // if C has the same numeric value as a leading surrogate or
913                // trailing surrogate, then
914                //     i. Let unit be the code unit whose numeric value is that
915                //     of C.
916                //     ii. Set product to the string-concatenation of product
917                //     and UnicodeEscape(unit).
918                CodePoint::Unicode(c) if c < '\u{0020}' => {
919                    let val = c as u16;
920                    product.extend_from_slice(&[
921                        '\\' as u16,
922                        'u' as u16,
923                        to_hex_digit(val >> 12),
924                        to_hex_digit((val >> 8) & 0xF),
925                        to_hex_digit((val >> 4) & 0xF),
926                        to_hex_digit(val & 0xF),
927                    ]);
928                }
929                CodePoint::UnpairedSurrogate(surr) => {
930                    product.extend_from_slice(&[
931                        '\\' as u16,
932                        'u' as u16,
933                        to_hex_digit(surr >> 12),
934                        to_hex_digit((surr >> 8) & 0xF),
935                        to_hex_digit((surr >> 4) & 0xF),
936                        to_hex_digit(surr & 0xF),
937                    ]);
938                }
939                // c. Else,
940                CodePoint::Unicode(c) => {
941                    // i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C).
942                    product.extend_from_slice(c.encode_utf16(&mut buf));
943                }
944            }
945        }
946
947        // 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK).
948        product.push('"' as u16);
949
950        // 4. Return product.
951        js_string!(&product[..])
952    }
953
954    /// `25.5.2.4 SerializeJSONObject ( state, value )`
955    ///
956    /// More information:
957    ///  - [ECMAScript reference][spec]
958    ///
959    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonobject
960    fn serialize_json_object(
961        state: &mut StateRecord,
962        value: &JsObject,
963        context: &mut Context,
964    ) -> JsResult<JsString> {
965        // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
966        if !state.stack_set.insert(value.clone()) {
967            return Err(JsNativeError::typ()
968                .with_message("cyclic object value")
969                .into());
970        }
971
972        // 3. Let stepback be state.[[Indent]].
973        let stepback = state.indent.clone();
974
975        // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
976        state.indent = js_string!(&state.indent, &state.gap);
977
978        // 5. If state.[[PropertyList]] is not undefined, then
979        let k = if let Some(p) = &state.property_list {
980            // a. Let K be state.[[PropertyList]].
981            p.clone()
982        // 6. Else,
983        } else {
984            // a. Let K be ? EnumerableOwnPropertyNames(value, key).
985            let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context);
986            // 11. Remove value from state.[[Stack]] on error.
987            let keys = match keys {
988                Ok(k) => k,
989                Err(e) => {
990                    let removed = state.stack_set.remove(value);
991                    debug_assert!(removed);
992                    return Err(e);
993                }
994            };
995            // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values.
996            match keys
997                .iter()
998                .map(|v| {
999                    v.to_string(context)
1000                        .js_expect("EnumerableOwnPropertyNames only returns strings")
1001                })
1002                .collect::<Result<Vec<_>, _>>()
1003            {
1004                Ok(k) => k,
1005                Err(e) => {
1006                    let removed = state.stack_set.remove(value);
1007                    debug_assert!(removed);
1008                    return Err(e.into());
1009                }
1010            }
1011        };
1012
1013        // 7. Let partial be a new empty List.
1014        let mut partial = Vec::with_capacity(k.len());
1015
1016        // 8. For each element P of K, do
1017        for p in &k {
1018            // a. Let strP be ? SerializeJSONProperty(state, P, value).
1019            let str_p = Self::serialize_json_property(state, p.clone(), value, context);
1020            let str_p = match str_p {
1021                Ok(v) => v,
1022                Err(e) => {
1023                    let removed = state.stack_set.remove(value);
1024                    debug_assert!(removed);
1025                    return Err(e);
1026                }
1027            };
1028
1029            // b. If strP is not undefined, then
1030            if let Some(str_p) = str_p {
1031                // i. Let member be QuoteJSONString(P).
1032                let mut member = Self::quote_json_string(p).iter().collect::<Vec<_>>();
1033
1034                // ii. Set member to the string-concatenation of member and ":".
1035                member.push(':' as u16);
1036
1037                // iii. If state.[[Gap]] is not the empty String, then
1038                if !state.gap.is_empty() {
1039                    // 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE).
1040                    member.push(' ' as u16);
1041                }
1042
1043                // iv. Set member to the string-concatenation of member and strP.
1044                member.extend(str_p.iter());
1045
1046                // v. Append member to partial.
1047                partial.push(member);
1048            }
1049        }
1050
1051        // 9. If partial is empty, then
1052        let r#final = if partial.is_empty() {
1053            // a. Let final be "{}".
1054            js_string!("{}")
1055        // 10. Else,
1056        } else {
1057            // a. If state.[[Gap]] is the empty String, then
1058            if state.gap.is_empty() {
1059                // i. Let properties be the String value formed by concatenating all the element Strings of partial
1060                //    with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
1061                //    A comma is not inserted either before the first String or after the last String.
1062                // ii. Let final be the string-concatenation of "{", properties, and "}".
1063                let separator = utf16!(",");
1064                let result = once(utf16!("{"))
1065                    .chain(Itertools::intersperse(
1066                        partial.iter().map(Vec::as_slice),
1067                        separator,
1068                    ))
1069                    .chain(once(utf16!("}")))
1070                    .flatten()
1071                    .copied()
1072                    .collect::<Vec<_>>();
1073                js_string!(&result[..])
1074            // b. Else,
1075            } else {
1076                // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
1077                //    the code unit 0x000A (LINE FEED), and state.[[Indent]].
1078                let mut separator = utf16!(",\n").to_vec();
1079                separator.extend(state.indent.iter());
1080                // ii. Let properties be the String value formed by concatenating all the element Strings of partial
1081                //     with each adjacent pair of Strings separated with separator.
1082                //     The separator String is not inserted either before the first String or after the last String.
1083                // iii. Let final be the string-concatenation of "{", the code
1084                //      unit 0x000A (LINE FEED), state.[[Indent]], properties,
1085                //      the code unit 0x000A (LINE FEED), stepback, and "}".
1086                let indent_vec = state.indent.to_vec();
1087                let stepback_vec = stepback.to_vec();
1088                let result = [utf16!("{\n"), &indent_vec[..]]
1089                    .into_iter()
1090                    .chain(Itertools::intersperse(
1091                        partial.iter().map(Vec::as_slice),
1092                        &separator,
1093                    ))
1094                    .chain([utf16!("\n"), &stepback_vec[..], utf16!("}")])
1095                    .flatten()
1096                    .copied()
1097                    .collect::<Vec<_>>();
1098                js_string!(&result[..])
1099            }
1100        };
1101
1102        // 12. Set state.[[Indent]] to stepback.
1103        state.indent = stepback;
1104
1105        // 11. Remove value from state.[[Stack]].
1106        let removed = state.stack_set.remove(value);
1107        debug_assert!(removed);
1108
1109        // 13. Return final.
1110        Ok(r#final)
1111    }
1112
1113    /// `25.5.2.5 SerializeJSONArray ( state, value )`
1114    ///
1115    /// More information:
1116    ///  - [ECMAScript reference][spec]
1117    ///
1118    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonarray
1119    fn serialize_json_array(
1120        state: &mut StateRecord,
1121        value: &JsObject,
1122        context: &mut Context,
1123    ) -> JsResult<JsString> {
1124        // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
1125        if !state.stack_set.insert(value.clone()) {
1126            return Err(JsNativeError::typ()
1127                .with_message("cyclic object value")
1128                .into());
1129        }
1130
1131        // 3. Let stepback be state.[[Indent]].
1132        let stepback = state.indent.clone();
1133
1134        // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
1135        state.indent = js_string!(&state.indent, &state.gap);
1136
1137        // 6. Let len be ? LengthOfArrayLike(value).
1138        let len = match value.length_of_array_like(context) {
1139            Ok(n) => n,
1140            Err(e) => {
1141                let removed = state.stack_set.remove(value);
1142                debug_assert!(removed);
1143                return Err(e);
1144            }
1145        };
1146
1147        // 5. Let partial be a new empty List.
1148        let mut partial = Vec::with_capacity(len as usize);
1149
1150        // 7. Let index be 0.
1151        let mut index = 0;
1152
1153        // 8. Repeat, while index < len,
1154        while index < len {
1155            // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value).
1156            let str_p = Self::serialize_json_property(state, index.into(), value, context);
1157            let str_p = match str_p {
1158                Ok(v) => v,
1159                Err(e) => {
1160                    let removed = state.stack_set.remove(value);
1161                    debug_assert!(removed);
1162                    return Err(e);
1163                }
1164            };
1165
1166            // b. If strP is undefined, then
1167            if let Some(str_p) = str_p {
1168                // i. Append strP to partial.
1169                partial.push(Cow::Owned(str_p.iter().collect::<_>()));
1170            // c. Else,
1171            } else {
1172                // i. Append "null" to partial.
1173                partial.push(Cow::Borrowed(utf16!("null")));
1174            }
1175
1176            // d. Set index to index + 1.
1177            index += 1;
1178        }
1179
1180        // 9. If partial is empty, then
1181        let r#final = if partial.is_empty() {
1182            // a. Let final be "[]".
1183            js_string!("[]")
1184        // 10. Else,
1185        } else {
1186            // a. If state.[[Gap]] is the empty String, then
1187            if state.gap.is_empty() {
1188                // i. Let properties be the String value formed by concatenating all the element Strings of partial
1189                //    with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
1190                //    A comma is not inserted either before the first String or after the last String.
1191                // ii. Let final be the string-concatenation of "[", properties, and "]".
1192                let separator = utf16!(",");
1193                let result = once(utf16!("["))
1194                    .chain(Itertools::intersperse(
1195                        partial.iter().map(Cow::as_ref),
1196                        separator,
1197                    ))
1198                    .chain(once(utf16!("]")))
1199                    .flatten()
1200                    .copied()
1201                    .collect::<Vec<_>>();
1202                js_string!(&result[..])
1203            // b. Else,
1204            } else {
1205                // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
1206                //    the code unit 0x000A (LINE FEED), and state.[[Indent]].
1207                let mut separator = utf16!(",\n").to_vec();
1208                separator.extend(state.indent.iter());
1209                // ii. Let properties be the String value formed by concatenating all the element Strings of partial
1210                //     with each adjacent pair of Strings separated with separator.
1211                //     The separator String is not inserted either before the first String or after the last String.
1212                // iii. Let final be the string-concatenation of "[", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "]".
1213                let indent_vec = state.indent.to_vec();
1214                let stepback_vec = stepback.to_vec();
1215                let result = [utf16!("[\n"), &indent_vec[..]]
1216                    .into_iter()
1217                    .chain(Itertools::intersperse(
1218                        partial.iter().map(Cow::as_ref),
1219                        &separator,
1220                    ))
1221                    .chain([utf16!("\n"), &stepback_vec[..], utf16!("]")])
1222                    .flatten()
1223                    .copied()
1224                    .collect::<Vec<_>>();
1225                js_string!(&result[..])
1226            }
1227        };
1228
1229        // 12. Set state.[[Indent]] to stepback.
1230        state.indent = stepback;
1231
1232        // 11. Remove value from state.[[Stack]].
1233        let removed = state.stack_set.remove(value);
1234        debug_assert!(removed);
1235
1236        // 13. Return final.
1237        Ok(r#final)
1238    }
1239}
1240
1241struct StateRecord {
1242    replacer_function: Option<JsObject>,
1243    stack_set: FxHashSet<JsObject>,
1244    indent: JsString,
1245    gap: JsString,
1246    property_list: Option<Vec<JsString>>,
1247}