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}