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