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}