boa/builtins/json/
mod.rs

1//! This module implements the 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 crate::{
17    builtins::{
18        string::{is_leading_surrogate, is_trailing_surrogate},
19        BuiltIn,
20    },
21    object::{JsObject, ObjectInitializer, RecursionLimiter},
22    property::{Attribute, PropertyNameKind},
23    symbol::WellKnownSymbols,
24    value::IntegerOrInfinity,
25    BoaProfiler, Context, JsResult, JsString, JsValue,
26};
27use serde_json::{self, Value as JSONValue};
28
29use super::JsArgs;
30
31#[cfg(test)]
32mod tests;
33
34/// JavaScript `JSON` global object.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub(crate) struct Json;
37
38impl BuiltIn for Json {
39    const NAME: &'static str = "JSON";
40
41    fn attribute() -> Attribute {
42        Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
43    }
44
45    fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
46        let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
47
48        let to_string_tag = WellKnownSymbols::to_string_tag();
49        let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
50
51        let json_object = ObjectInitializer::new(context)
52            .function(Self::parse, "parse", 2)
53            .function(Self::stringify, "stringify", 3)
54            .property(to_string_tag, Self::NAME, attribute)
55            .build();
56
57        (Self::NAME, json_object.into(), Self::attribute())
58    }
59}
60
61impl Json {
62    /// `JSON.parse( text[, reviver] )`
63    ///
64    /// This `JSON` method parses a JSON string, constructing the JavaScript value or object described by the string.
65    ///
66    /// An optional `reviver` function can be provided to perform a transformation on the resulting object before it is returned.
67    ///
68    /// More information:
69    ///  - [ECMAScript reference][spec]
70    ///  - [MDN documentation][mdn]
71    ///
72    /// [spec]: https://tc39.es/ecma262/#sec-json.parse
73    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
74    pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
75        // 1. Let jsonString be ? ToString(text).
76        let json_string = args
77            .get(0)
78            .cloned()
79            .unwrap_or_default()
80            .to_string(context)?;
81
82        // 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404.
83        //    Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification.
84        if let Err(e) = serde_json::from_str::<JSONValue>(&json_string) {
85            return context.throw_syntax_error(e.to_string());
86        }
87
88        // 3. Let scriptString be the string-concatenation of "(", jsonString, and ");".
89        let script_string = JsString::concat_array(&["(", json_string.as_str(), ");"]);
90
91        // 4. Let script be ParseText(! StringToCodePoints(scriptString), Script).
92        // 5. NOTE: The early error rules defined in 13.2.5.1 have special handling for the above invocation of ParseText.
93        // 6. Assert: script is a Parse Node.
94        // 7. Let completion be the result of evaluating script.
95        // 8. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation.
96        // 9. Let unfiltered be completion.[[Value]].
97        // 10. Assert: unfiltered is either a String, Number, Boolean, Null, or an Object that is defined by either an ArrayLiteral or an ObjectLiteral.
98        let unfiltered = context.eval(script_string.as_bytes())?;
99
100        match args.get(1).cloned().unwrap_or_default().as_object() {
101            // 11. If IsCallable(reviver) is true, then
102            Some(obj) if obj.is_callable() => {
103                // a. Let root be ! OrdinaryObjectCreate(%Object.prototype%).
104                let root = context.construct_object();
105
106                // b. Let rootName be the empty String.
107                // c. Perform ! CreateDataPropertyOrThrow(root, rootName, unfiltered).
108                root.create_data_property_or_throw("", unfiltered, context)
109                    .expect("CreateDataPropertyOrThrow should never throw here");
110
111                // d. Return ? InternalizeJSONProperty(root, rootName, reviver).
112                Self::internalize_json_property(root, "".into(), obj, context)
113            }
114            // 12. Else,
115            // a. Return unfiltered.
116            _ => Ok(unfiltered),
117        }
118    }
119
120    /// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )`
121    ///
122    /// More information:
123    ///  - [ECMAScript reference][spec]
124    ///
125    /// [spec]: https://tc39.es/ecma262/#sec-internalizejsonproperty
126    fn internalize_json_property(
127        holder: JsObject,
128        name: JsString,
129        reviver: JsObject,
130        context: &mut Context,
131    ) -> JsResult<JsValue> {
132        // 1. Let val be ? Get(holder, name).
133        let val = holder.get(name.clone(), context)?;
134
135        // 2. If Type(val) is Object, then
136        if let Some(obj) = val.as_object() {
137            // a. Let isArray be ? IsArray(val).
138            // b. If isArray is true, then
139            if obj.is_array() {
140                // i. Let I be 0.
141                // ii. Let len be ? LengthOfArrayLike(val).
142                // iii. Repeat, while I < len,
143                let len = obj.length_of_array_like(context)? as i64;
144                for i in 0..len {
145                    // 1. Let prop be ! ToString(𝔽(I)).
146                    // 2. Let newElement be ? InternalizeJSONProperty(val, prop, reviver).
147                    let new_element = Self::internalize_json_property(
148                        obj.clone(),
149                        i.to_string().into(),
150                        reviver.clone(),
151                        context,
152                    )?;
153
154                    // 3. If newElement is undefined, then
155                    if new_element.is_undefined() {
156                        // a. Perform ? val.[[Delete]](prop).
157                        obj.__delete__(&i.into(), context)?;
158                    }
159                    // 4. Else,
160                    else {
161                        // a. Perform ? CreateDataProperty(val, prop, newElement).
162                        obj.create_data_property(i, new_element, context)?;
163                    }
164                }
165            }
166            // c. Else,
167            else {
168                // i. Let keys be ? EnumerableOwnPropertyNames(val, key).
169                let keys = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?;
170
171                // ii. For each String P of keys, do
172                for p in keys {
173                    // This is safe, because EnumerableOwnPropertyNames with 'key' type only returns strings.
174                    let p = p.as_string().unwrap();
175
176                    // 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver).
177                    let new_element = Self::internalize_json_property(
178                        obj.clone(),
179                        p.clone(),
180                        reviver.clone(),
181                        context,
182                    )?;
183
184                    // 2. If newElement is undefined, then
185                    if new_element.is_undefined() {
186                        // a. Perform ? val.[[Delete]](P).
187                        obj.__delete__(&p.clone().into(), context)?;
188                    }
189                    // 3. Else,
190                    else {
191                        // a. Perform ? CreateDataProperty(val, P, newElement).
192                        obj.create_data_property(p.as_str(), new_element, context)?;
193                    }
194                }
195            }
196        }
197
198        // 3. Return ? Call(reviver, holder, « name, val »).
199        reviver.call(&holder.into(), &[name.into(), val], context)
200    }
201
202    /// `JSON.stringify( value[, replacer[, space]] )`
203    ///
204    /// This `JSON` method converts a JavaScript object or value to a JSON string.
205    ///
206    /// This method optionally replaces values if a `replacer` function is specified or
207    /// optionally including only the specified properties if a replacer array is specified.
208    ///
209    /// An optional `space` argument can be supplied of type `String` or `Number` that's used to insert
210    /// white space into the output JSON string for readability purposes.
211    ///
212    /// More information:
213    ///  - [ECMAScript reference][spec]
214    ///  - [MDN documentation][mdn]
215    ///
216    /// [spec]: https://tc39.es/ecma262/#sec-json.stringify
217    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
218    pub(crate) fn stringify(
219        _: &JsValue,
220        args: &[JsValue],
221        context: &mut Context,
222    ) -> JsResult<JsValue> {
223        // 1. Let stack be a new empty List.
224        let stack = Vec::new();
225
226        // 2. Let indent be the empty String.
227        let indent = JsString::new("");
228
229        // 3. Let PropertyList and ReplacerFunction be undefined.
230        let mut property_list = None;
231        let mut replacer_function = None;
232
233        let replacer = args.get_or_undefined(1);
234
235        // 4. If Type(replacer) is Object, then
236        if let Some(replacer_obj) = replacer.as_object() {
237            // a. If IsCallable(replacer) is true, then
238            if replacer_obj.is_callable() {
239                // i. Set ReplacerFunction to replacer.
240                replacer_function = Some(replacer_obj)
241            // b. Else,
242            } else {
243                // i. Let isArray be ? IsArray(replacer).
244                // ii. If isArray is true, then
245                if replacer_obj.is_array() {
246                    // 1. Set PropertyList to a new empty List.
247                    let mut property_set = indexmap::IndexSet::new();
248
249                    // 2. Let len be ? LengthOfArrayLike(replacer).
250                    let len = replacer_obj.length_of_array_like(context)?;
251
252                    // 3. Let k be 0.
253                    let mut k = 0;
254
255                    // 4. Repeat, while k < len,
256                    while k < len {
257                        // a. Let prop be ! ToString(𝔽(k)).
258                        // b. Let v be ? Get(replacer, prop).
259                        let v = replacer_obj.get(k, context)?;
260
261                        // c. Let item be undefined.
262                        // d. If Type(v) is String, set item to v.
263                        // e. Else if Type(v) is Number, set item to ! ToString(v).
264                        // f. Else if Type(v) is Object, then
265                        // g. If item is not undefined and item is not currently an element of PropertyList, then
266                        // i. Append item to the end of PropertyList.
267                        if let Some(s) = v.as_string() {
268                            property_set.insert(s.to_owned());
269                        } else if v.is_number() {
270                            property_set.insert(
271                                v.to_string(context)
272                                    .expect("ToString cannot fail on number value"),
273                            );
274                        } else if let Some(obj) = v.as_object() {
275                            // i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v).
276                            if obj.is_string() || obj.is_number() {
277                                property_set.insert(v.to_string(context)?);
278                            }
279                        }
280
281                        // h. Set k to k + 1.
282                        k += 1;
283                    }
284                    property_list = Some(property_set.into_iter().collect());
285                }
286            }
287        }
288
289        let mut space = args.get_or_undefined(2).clone();
290
291        // 5. If Type(space) is Object, then
292        if let Some(space_obj) = space.as_object() {
293            // a. If space has a [[NumberData]] internal slot, then
294            if space_obj.is_number() {
295                // i. Set space to ? ToNumber(space).
296                space = space.to_number(context)?.into();
297            }
298            // b. Else if space has a [[StringData]] internal slot, then
299            else if space_obj.is_string() {
300                // i. Set space to ? ToString(space).
301                space = space.to_string(context)?.into();
302            }
303        }
304
305        // 6. If Type(space) is Number, then
306        let gap = if space.is_number() {
307            // a. Let spaceMV be ! ToIntegerOrInfinity(space).
308            // b. Set spaceMV to min(10, spaceMV).
309            // 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).
310            match space
311                .to_integer_or_infinity(context)
312                .expect("ToIntegerOrInfinity cannot fail on number")
313            {
314                IntegerOrInfinity::PositiveInfinity => JsString::new("          "),
315                IntegerOrInfinity::NegativeInfinity => JsString::new(""),
316                IntegerOrInfinity::Integer(i) if i < 1 => JsString::new(""),
317                IntegerOrInfinity::Integer(i) => {
318                    let mut s = String::new();
319                    let i = std::cmp::min(10, i);
320                    for _ in 0..i {
321                        s.push(' ');
322                    }
323                    s.into()
324                }
325            }
326        // 7. Else if Type(space) is String, then
327        } else if let Some(s) = space.as_string() {
328            // 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.
329            String::from_utf16_lossy(&s.encode_utf16().take(10).collect::<Vec<u16>>()).into()
330        // 8. Else,
331        } else {
332            // a. Let gap be the empty String.
333            JsString::new("")
334        };
335
336        // 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%).
337        let wrapper = context.construct_object();
338
339        // 10. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value).
340        wrapper
341            .create_data_property_or_throw("", args.get_or_undefined(0).clone(), context)
342            .expect("CreateDataPropertyOrThrow should never fail here");
343
344        // 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }.
345        let mut state = StateRecord {
346            replacer_function,
347            stack,
348            indent,
349            gap,
350            property_list,
351        };
352
353        // 12. Return ? SerializeJSONProperty(state, the empty String, wrapper).
354        Ok(
355            Self::serialize_json_property(&mut state, JsString::new(""), wrapper, context)?
356                .map(|s| s.into())
357                .unwrap_or_default(),
358        )
359    }
360
361    /// `25.5.2.1 SerializeJSONProperty ( state, key, holder )`
362    ///
363    /// More information:
364    ///  - [ECMAScript reference][spec]
365    ///
366    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonproperty
367    fn serialize_json_property(
368        state: &mut StateRecord,
369        key: JsString,
370        holder: JsObject,
371        context: &mut Context,
372    ) -> JsResult<Option<JsString>> {
373        // 1. Let value be ? Get(holder, key).
374        let mut value = holder.get(key.clone(), context)?;
375
376        // 2. If Type(value) is Object or BigInt, then
377        if value.is_object() || value.is_bigint() {
378            // a. Let toJSON be ? GetV(value, "toJSON").
379            let to_json = value.get_field("toJSON", context)?;
380
381            // b. If IsCallable(toJSON) is true, then
382            if let Some(obj) = to_json.as_object() {
383                if obj.is_callable() {
384                    // i. Set value to ? Call(toJSON, value, « key »).
385                    value = obj.call(&value, &[key.clone().into()], context)?;
386                }
387            }
388        }
389
390        // 3. If state.[[ReplacerFunction]] is not undefined, then
391        if let Some(obj) = &state.replacer_function {
392            // a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »).
393            value = obj.call(&holder.into(), &[key.into(), value], context)?
394        }
395
396        // 4. If Type(value) is Object, then
397        if let Some(obj) = value.as_object() {
398            // a. If value has a [[NumberData]] internal slot, then
399            if obj.is_number() {
400                // i. Set value to ? ToNumber(value).
401                value = value.to_number(context)?.into();
402            }
403            // b. Else if value has a [[StringData]] internal slot, then
404            else if obj.is_string() {
405                // i. Set value to ? ToString(value).
406                value = value.to_string(context)?.into();
407            }
408            // c. Else if value has a [[BooleanData]] internal slot, then
409            else if let Some(boolean) = obj.borrow().as_boolean() {
410                // i. Set value to value.[[BooleanData]].
411                value = boolean.into()
412            }
413            // d. Else if value has a [[BigIntData]] internal slot, then
414            else if let Some(bigint) = obj.borrow().as_bigint() {
415                // i. Set value to value.[[BigIntData]].
416                value = bigint.clone().into()
417            }
418        }
419
420        // 5. If value is null, return "null".
421        if value.is_null() {
422            return Ok(Some(JsString::new("null")));
423        }
424
425        // 6. If value is true, return "true".
426        // 7. If value is false, return "false".
427        if value.is_boolean() {
428            return match value.to_boolean() {
429                true => Ok(Some(JsString::new("true"))),
430                false => Ok(Some(JsString::new("false"))),
431            };
432        }
433
434        // 8. If Type(value) is String, return QuoteJSONString(value).
435        if let Some(s) = value.as_string() {
436            return Ok(Some(Self::quote_json_string(s)));
437        }
438
439        // 9. If Type(value) is Number, then
440        if let Some(n) = value.as_number() {
441            // a. If value is finite, return ! ToString(value).
442            if n.is_finite() {
443                return Ok(Some(
444                    value
445                        .to_string(context)
446                        .expect("ToString should never fail here"),
447                ));
448            }
449
450            // b. Return "null".
451            return Ok(Some(JsString::new("null")));
452        }
453
454        // 10. If Type(value) is BigInt, throw a TypeError exception.
455        if value.is_bigint() {
456            return Err(context.construct_type_error("cannot serialize bigint to JSON"));
457        }
458
459        // 11. If Type(value) is Object and IsCallable(value) is false, then
460        if let Some(obj) = value.as_object() {
461            if !obj.is_callable() {
462                // a. Let isArray be ? IsArray(value).
463                // b. If isArray is true, return ? SerializeJSONArray(state, value).
464                // c. Return ? SerializeJSONObject(state, value).
465                return if obj.is_array() {
466                    Ok(Some(Self::serialize_json_array(state, obj, context)?))
467                } else {
468                    Ok(Some(Self::serialize_json_object(state, obj, context)?))
469                };
470            }
471        }
472
473        // 12. Return undefined.
474        Ok(None)
475    }
476
477    /// `25.5.2.2 QuoteJSONString ( value )`
478    ///
479    /// More information:
480    ///  - [ECMAScript reference][spec]
481    ///
482    /// [spec]: https://tc39.es/ecma262/#sec-quotejsonstring
483    fn quote_json_string(value: &JsString) -> JsString {
484        // 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK).
485        let mut product = String::from('"');
486
487        // 2. For each code point C of ! StringToCodePoints(value), do
488        for code_point in value.encode_utf16() {
489            match code_point {
490                // a. If C is listed in the “Code Point” column of Table 73, then
491                // i. Set product to the string-concatenation of product and the escape sequence for C as specified in the “Escape Sequence” column of the corresponding row.
492                0x8 => product.push_str("\\b"),
493                0x9 => product.push_str("\\t"),
494                0xA => product.push_str("\\n"),
495                0xC => product.push_str("\\f"),
496                0xD => product.push_str("\\r"),
497                0x22 => product.push_str("\\\""),
498                0x5C => product.push_str("\\\\"),
499                // b. Else if C has a numeric value less than 0x0020 (SPACE), or if C has the same numeric value as a leading surrogate or trailing surrogate, then
500                code_point
501                    if is_leading_surrogate(code_point) || is_trailing_surrogate(code_point) =>
502                {
503                    // i. Let unit be the code unit whose numeric value is that of C.
504                    // ii. Set product to the string-concatenation of product and UnicodeEscape(unit).
505                    product.push_str(&format!("\\\\uAA{:x}", code_point));
506                }
507                // c. Else,
508                code_point => {
509                    // i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C).
510                    product.push(
511                        char::from_u32(code_point as u32)
512                            .expect("char from code point cannot fail here"),
513                    );
514                }
515            }
516        }
517
518        // 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK).
519        product.push('"');
520
521        // 4. Return product.
522        product.into()
523    }
524
525    /// `25.5.2.4 SerializeJSONObject ( state, value )`
526    ///
527    /// More information:
528    ///  - [ECMAScript reference][spec]
529    ///
530    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonobject
531    fn serialize_json_object(
532        state: &mut StateRecord,
533        value: JsObject,
534        context: &mut Context,
535    ) -> JsResult<JsString> {
536        // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
537        let limiter = RecursionLimiter::new(&value);
538        if limiter.live {
539            return Err(context.construct_type_error("cyclic object value"));
540        }
541
542        // 2. Append value to state.[[Stack]].
543        state.stack.push(value.clone().into());
544
545        // 3. Let stepback be state.[[Indent]].
546        let stepback = state.indent.clone();
547
548        // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
549        state.indent = JsString::concat(&state.indent, &state.gap);
550
551        // 5. If state.[[PropertyList]] is not undefined, then
552        let k = if let Some(p) = &state.property_list {
553            // a. Let K be state.[[PropertyList]].
554            p.clone()
555        // 6. Else,
556        } else {
557            // a. Let K be ? EnumerableOwnPropertyNames(value, key).
558            let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?;
559            // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values.
560            keys.iter().map(|v| v.to_string(context).unwrap()).collect()
561        };
562
563        // 7. Let partial be a new empty List.
564        let mut partial = Vec::new();
565
566        // 8. For each element P of K, do
567        for p in &k {
568            // a. Let strP be ? SerializeJSONProperty(state, P, value).
569            let str_p = Self::serialize_json_property(state, p.clone(), value.clone(), context)?;
570
571            // b. If strP is not undefined, then
572            if let Some(str_p) = str_p {
573                // i. Let member be QuoteJSONString(P).
574                // ii. Set member to the string-concatenation of member and ":".
575                // iii. If state.[[Gap]] is not the empty String, then
576                // 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE).
577                // iv. Set member to the string-concatenation of member and strP.
578                let member = if state.gap.is_empty() {
579                    format!("{}:{}", Self::quote_json_string(p).as_str(), str_p.as_str())
580                } else {
581                    format!(
582                        "{}: {}",
583                        Self::quote_json_string(p).as_str(),
584                        str_p.as_str()
585                    )
586                };
587
588                // v. Append member to partial.
589                partial.push(member);
590            }
591        }
592
593        // 9. If partial is empty, then
594        let r#final = if partial.is_empty() {
595            // a. Let final be "{}".
596            JsString::new("{}")
597        // 10. Else,
598        } else {
599            // a. If state.[[Gap]] is the empty String, then
600            if state.gap.is_empty() {
601                // i. Let properties be the String value formed by concatenating all the element Strings of partial
602                //    with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
603                //    A comma is not inserted either before the first String or after the last String.
604                // ii. Let final be the string-concatenation of "{", properties, and "}".
605                format!("{{{}}}", partial.join(",")).into()
606            // b. Else,
607            } else {
608                // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
609                //    the code unit 0x000A (LINE FEED), and state.[[Indent]].
610                let separator = format!(",{}{}", '\u{A}', state.indent.as_str());
611                // ii. Let properties be the String value formed by concatenating all the element Strings of partial
612                //     with each adjacent pair of Strings separated with separator.
613                //     The separator String is not inserted either before the first String or after the last String.
614                let properties = partial.join(&separator);
615                // 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 "}".
616                format!(
617                    "{{{}{}{}{}{}}}",
618                    '\u{A}',
619                    state.indent.as_str(),
620                    &properties,
621                    '\u{A}',
622                    stepback.as_str()
623                )
624                .into()
625            }
626        };
627
628        // 11. Remove the last element of state.[[Stack]].
629        state.stack.pop();
630
631        // 12. Set state.[[Indent]] to stepback.
632        state.indent = stepback;
633
634        // 13. Return final.
635        Ok(r#final)
636    }
637
638    /// `25.5.2.5 SerializeJSONArray ( state, value )`
639    ///
640    /// More information:
641    ///  - [ECMAScript reference][spec]
642    ///
643    /// [spec]: https://tc39.es/ecma262/#sec-serializejsonarray
644    fn serialize_json_array(
645        state: &mut StateRecord,
646        value: JsObject,
647        context: &mut Context,
648    ) -> JsResult<JsString> {
649        // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
650        let limiter = RecursionLimiter::new(&value);
651        if limiter.live {
652            return Err(context.construct_type_error("cyclic object value"));
653        }
654
655        // 2. Append value to state.[[Stack]].
656        state.stack.push(value.clone().into());
657
658        // 3. Let stepback be state.[[Indent]].
659        let stepback = state.indent.clone();
660
661        // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
662        state.indent = JsString::concat(&state.indent, &state.gap);
663
664        // 5. Let partial be a new empty List.
665        let mut partial = Vec::new();
666
667        // 6. Let len be ? LengthOfArrayLike(value).
668        let len = value.length_of_array_like(context)?;
669
670        // 7. Let index be 0.
671        let mut index = 0;
672
673        // 8. Repeat, while index < len,
674        while index < len {
675            // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value).
676            let str_p = Self::serialize_json_property(
677                state,
678                index.to_string().into(),
679                value.clone(),
680                context,
681            )?;
682
683            // b. If strP is undefined, then
684            if let Some(str_p) = str_p {
685                // i. Append strP to partial.
686                partial.push(str_p)
687            // c. Else,
688            } else {
689                // i. Append "null" to partial.
690                partial.push("null".into())
691            }
692
693            // d. Set index to index + 1.
694            index += 1;
695        }
696
697        // 9. If partial is empty, then
698        let r#final = if partial.is_empty() {
699            // a. Let final be "[]".
700            JsString::from("[]")
701        // 10. Else,
702        } else {
703            // a. If state.[[Gap]] is the empty String, then
704            if state.gap.is_empty() {
705                // i. Let properties be the String value formed by concatenating all the element Strings of partial
706                //    with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
707                //    A comma is not inserted either before the first String or after the last String.
708                // ii. Let final be the string-concatenation of "[", properties, and "]".
709                format!("[{}]", partial.join(",")).into()
710            // b. Else,
711            } else {
712                // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
713                //    the code unit 0x000A (LINE FEED), and state.[[Indent]].
714                let separator = format!(",{}{}", '\u{A}', state.indent.as_str());
715                // ii. Let properties be the String value formed by concatenating all the element Strings of partial
716                //     with each adjacent pair of Strings separated with separator.
717                //     The separator String is not inserted either before the first String or after the last String.
718                let properties = partial.join(&separator);
719                // 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 "]".
720                format!(
721                    "[{}{}{}{}{}]",
722                    '\u{A}',
723                    state.indent.as_str(),
724                    &properties,
725                    '\u{A}',
726                    stepback.as_str()
727                )
728                .into()
729            }
730        };
731
732        // 11. Remove the last element of state.[[Stack]].
733        state.stack.pop();
734
735        // 12. Set state.[[Indent]] to stepback.
736        state.indent = stepback;
737
738        // 13. Return final.
739        Ok(r#final)
740    }
741}
742
743struct StateRecord {
744    replacer_function: Option<JsObject>,
745    stack: Vec<JsValue>,
746    indent: JsString,
747    gap: JsString,
748    property_list: Option<Vec<JsString>>,
749}