feat: 实现 JSON.stringify replacer/space/toJSON/循环检测和 JSON.parse reviver
This commit is contained in:
@@ -27,7 +27,82 @@ pub fn json_parse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
Err(e) => return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("{}", e))),
|
Err(e) => return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("{}", e))),
|
||||||
};
|
};
|
||||||
|
|
||||||
NativeResult::Ok(value_to_jsvalue(vm, &parsed))
|
let mut result = value_to_jsvalue(vm, &parsed);
|
||||||
|
|
||||||
|
// Apply reviver if provided (D-05)
|
||||||
|
if args.len() > 2 {
|
||||||
|
let reviver_val = vm.reg(args[2]);
|
||||||
|
if reviver_val.is_object() {
|
||||||
|
let rptr = reviver_val.as_js_object_ptr();
|
||||||
|
if !rptr.is_null() && unsafe { (*rptr).is_function() } {
|
||||||
|
let empty_si = vm.kernel_core().perm_interner().intern("").0;
|
||||||
|
let holder = create_wrapper(vm, result);
|
||||||
|
let holder_ptr = holder.as_js_object_ptr();
|
||||||
|
match walk_reviver(vm, holder_ptr, empty_si, reviver_val) {
|
||||||
|
Ok(()) => {
|
||||||
|
let holder_obj = unsafe { &*holder_ptr };
|
||||||
|
let final_val = holder_obj.get_prop_at(0u32);
|
||||||
|
result = if final_val.is_undefined() { JsValue::undefined() } else { final_val };
|
||||||
|
}
|
||||||
|
Err(e) => return NativeResult::Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeResult::Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_reviver<H: VmHost>(
|
||||||
|
vm: &mut H, holder_ptr: *mut JsObject, key_si: u32, reviver: JsValue,
|
||||||
|
) -> Result<(), JsValue> {
|
||||||
|
let holder = unsafe { &*holder_ptr };
|
||||||
|
let slot = vm.get_own_property_slot(holder, key_si);
|
||||||
|
let pos = match slot {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let mut val = holder.get_prop_at(pos);
|
||||||
|
|
||||||
|
// Post-order: recurse on children first
|
||||||
|
if val.is_object() {
|
||||||
|
let obj_ptr = val.as_js_object_ptr();
|
||||||
|
if !obj_ptr.is_null() {
|
||||||
|
let obj = unsafe { &*obj_ptr };
|
||||||
|
if obj.is_array() {
|
||||||
|
let len = obj.prop_vec_len();
|
||||||
|
for i in 0..len {
|
||||||
|
let index_str = i.to_string();
|
||||||
|
let child_si = vm.kernel_core().perm_interner().intern(&index_str).0;
|
||||||
|
walk_reviver(vm, obj_ptr, child_si, reviver)?;
|
||||||
|
}
|
||||||
|
// Re-read value after children modified it
|
||||||
|
val = holder.get_prop_at(pos);
|
||||||
|
} else {
|
||||||
|
let keys = walk_own_keys(vm, obj);
|
||||||
|
for (child_si, _child_pos) in keys {
|
||||||
|
walk_reviver(vm, obj_ptr, child_si, reviver)?;
|
||||||
|
}
|
||||||
|
// Re-read value after children modified it
|
||||||
|
val = holder.get_prop_at(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call reviver on current value
|
||||||
|
let kc = vm.kernel_core().clone();
|
||||||
|
let key_str = kc.perm_interner().lookup(key_si).unwrap_or("");
|
||||||
|
let key_val = vm.new_string(key_str);
|
||||||
|
let holder_val = JsValue::from_js_object(holder_ptr);
|
||||||
|
match vm.call_function_sync(reviver, holder_val, &[key_val, val]) {
|
||||||
|
Ok(new_val) => {
|
||||||
|
unsafe {
|
||||||
|
(*holder_ptr).set_prop_at(pos, new_val);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(msg) => Err(crate::error::create_type_error(vm, &msg)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
|
fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
|
||||||
@@ -75,6 +150,61 @@ fn value_to_jsvalue<H: VmHost>(vm: &mut H, val: &serde_json::Value) -> JsValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_wrapper<H: VmHost>(vm: &mut H, value: JsValue) -> JsValue {
|
||||||
|
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||||
|
let empty_si = vm.kernel_core().perm_interner().intern("").0;
|
||||||
|
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
|
||||||
|
let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), empty_si);
|
||||||
|
obj.set_shape_id(new_shape);
|
||||||
|
obj.ensure_hash_props().push(value);
|
||||||
|
let obj_ptr = vm.alloc_object(obj);
|
||||||
|
JsValue::from_js_object(obj_ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_space(val: JsValue) -> String {
|
||||||
|
if val.is_int() || val.is_double() {
|
||||||
|
let n = oxide_runtime_api::to_integer_or_infinity(val);
|
||||||
|
if n.is_nan() || n.is_infinite() || n <= 0.0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let clamped = (n as usize).min(10);
|
||||||
|
" ".repeat(clamped)
|
||||||
|
} else if val.is_string() {
|
||||||
|
let s = unsafe { (*val.as_string_ptr()).data.clone() };
|
||||||
|
s.chars().take(10).collect()
|
||||||
|
} else {
|
||||||
|
let s = oxide_runtime_api::to_string(val);
|
||||||
|
s.chars().take(10).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_to_json<H: VmHost>(vm: &mut H, obj_val: JsValue, key: &str) -> Result<JsValue, JsValue> {
|
||||||
|
if !obj_val.is_object() {
|
||||||
|
return Ok(obj_val);
|
||||||
|
}
|
||||||
|
let obj_ptr = obj_val.as_js_object_ptr();
|
||||||
|
if obj_ptr.is_null() {
|
||||||
|
return Ok(obj_val);
|
||||||
|
}
|
||||||
|
let tojson_si = vm.kernel_core().perm_interner().intern("toJSON").0;
|
||||||
|
let resolved = vm.resolve_property(unsafe { &*obj_ptr }, tojson_si);
|
||||||
|
match resolved {
|
||||||
|
Some(fn_val) if fn_val.is_object() => {
|
||||||
|
let fn_ptr = fn_val.as_js_object_ptr();
|
||||||
|
if !fn_ptr.is_null() && unsafe { (*fn_ptr).is_function() } {
|
||||||
|
let key_val = vm.new_string(key);
|
||||||
|
match vm.call_function_sync(fn_val, obj_val, &[key_val]) {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(msg) => Err(crate::error::create_type_error(vm, &msg)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(obj_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(obj_val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
return NativeResult::Ok(JsValue::undefined());
|
return NativeResult::Ok(JsValue::undefined());
|
||||||
@@ -83,17 +213,79 @@ pub fn json_stringify<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
|||||||
if value.is_undefined() {
|
if value.is_undefined() {
|
||||||
return NativeResult::Ok(JsValue::undefined());
|
return NativeResult::Ok(JsValue::undefined());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut replacer_fn: Option<JsValue> = None;
|
||||||
|
let mut replacer_whitelist: Option<HashSet<String>> = None;
|
||||||
|
if args.len() > 2 {
|
||||||
|
let replacer_val = vm.reg(args[2]);
|
||||||
|
if replacer_val.is_object() {
|
||||||
|
let rptr = replacer_val.as_js_object_ptr();
|
||||||
|
if !rptr.is_null() {
|
||||||
|
let robj = unsafe { &*rptr };
|
||||||
|
if robj.is_function() {
|
||||||
|
replacer_fn = Some(replacer_val);
|
||||||
|
} else if robj.is_array() {
|
||||||
|
let len = robj.prop_vec_len();
|
||||||
|
let mut whitelist = HashSet::new();
|
||||||
|
for i in 0..len {
|
||||||
|
let elem = robj.get_prop_at(i);
|
||||||
|
if !elem.is_undefined() {
|
||||||
|
whitelist.insert(oxide_runtime_api::to_string(elem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacer_whitelist = Some(whitelist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let space = if args.len() > 3 { process_space(vm.reg(args[3])) } else { String::new() };
|
||||||
|
|
||||||
|
let holder = create_wrapper(vm, value);
|
||||||
|
|
||||||
|
let value = match call_to_json(vm, value, "") {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return NativeResult::Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if let Some(replacer) = replacer_fn {
|
||||||
|
let key_val = vm.new_string("");
|
||||||
|
match vm.call_function_sync(replacer, holder, &[key_val, value]) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
|
||||||
let mut visited = HashSet::new();
|
let mut visited = HashSet::new();
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
if jsvalue_to_json(vm, value, &mut visited, &mut output).is_err() {
|
let indent_level: usize = 0;
|
||||||
return NativeResult::Err(crate::error::create_type_error(vm, "cyclic object value"));
|
let key = "";
|
||||||
|
if jsvalue_to_json(
|
||||||
|
vm,
|
||||||
|
value,
|
||||||
|
&mut visited,
|
||||||
|
&mut output,
|
||||||
|
replacer_fn,
|
||||||
|
replacer_whitelist.as_ref(),
|
||||||
|
&space,
|
||||||
|
indent_level,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return NativeResult::Err(crate::error::create_type_error(vm, "Converting circular structure to JSON"));
|
||||||
};
|
};
|
||||||
NativeResult::Ok(vm.new_string(&output))
|
NativeResult::Ok(vm.new_string(&output))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn jsvalue_to_json<H: VmHost>(
|
fn jsvalue_to_json<H: VmHost>(
|
||||||
vm: &H, val: JsValue, visited: &mut HashSet<*const u8>, out: &mut String,
|
vm: &mut H, val: JsValue, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
|
||||||
|
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize, key: &str,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
|
let val = call_to_json(vm, val, key).map_err(|_| ())?;
|
||||||
if val.is_null() {
|
if val.is_null() {
|
||||||
out.push_str("null");
|
out.push_str("null");
|
||||||
} else if val.is_undefined() {
|
} else if val.is_undefined() {
|
||||||
@@ -112,7 +304,6 @@ fn jsvalue_to_json<H: VmHost>(
|
|||||||
out.push_str(buf.format(n));
|
out.push_str(buf.format(n));
|
||||||
}
|
}
|
||||||
} else if val.is_string() {
|
} else if val.is_string() {
|
||||||
// SAFETY: val is a string value.
|
|
||||||
let s = unsafe { (*val.as_string_ptr()).data.clone() };
|
let s = unsafe { (*val.as_string_ptr()).data.clone() };
|
||||||
stringify_string(&s, out);
|
stringify_string(&s, out);
|
||||||
} else if val.is_object() {
|
} else if val.is_object() {
|
||||||
@@ -122,20 +313,18 @@ fn jsvalue_to_json<H: VmHost>(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if visited.contains(&(obj_ptr as *const u8)) {
|
if !visited.insert(obj_ptr as *const JsObject) {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
visited.insert(obj_ptr as *const u8);
|
|
||||||
|
|
||||||
let obj = unsafe { &*obj_ptr };
|
let obj = unsafe { &*obj_ptr };
|
||||||
|
|
||||||
if obj.is_array() {
|
if obj.is_array() {
|
||||||
stringify_array(vm, obj, visited, out)?;
|
stringify_array(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
|
||||||
} else {
|
} else {
|
||||||
stringify_object(vm, obj, visited, out)?;
|
stringify_object(vm, obj, visited, out, replacer_fn, replacer_whitelist, space, indent_level)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
visited.remove(&(obj_ptr as *const u8));
|
visited.remove(&(obj_ptr as *const JsObject));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -162,60 +351,162 @@ fn stringify_string(s: &str, out: &mut String) {
|
|||||||
out.push('"');
|
out.push('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn stringify_object<H: VmHost>(
|
fn stringify_object<H: VmHost>(
|
||||||
vm: &H, obj: &JsObject, visited: &mut HashSet<*const u8>, out: &mut String,
|
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
|
||||||
|
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
|
let has_space = !space.is_empty();
|
||||||
out.push('{');
|
out.push('{');
|
||||||
|
|
||||||
let keys = walk_own_keys(vm, obj);
|
let keys = walk_own_keys(vm, obj);
|
||||||
|
// Pre-compute (name, position) pairs with whitelist filtering
|
||||||
|
let kc = vm.kernel_core().clone();
|
||||||
|
let perm_interner = kc.perm_interner();
|
||||||
|
let entries: Vec<(String, u32)> = keys
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(si, pos)| {
|
||||||
|
let name = perm_interner.lookup(si).unwrap_or("").to_string();
|
||||||
|
if let Some(whitelist) = replacer_whitelist {
|
||||||
|
if !whitelist.contains(&name) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((name, pos))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
for (si, pos) in keys {
|
for (name, pos) in entries {
|
||||||
let val = obj.get_prop_at(pos);
|
let val = obj.get_prop_at(pos);
|
||||||
|
|
||||||
|
// Replacer function callback (toJSON handled by jsvalue_to_json)
|
||||||
|
let val = if let Some(replacer) = replacer_fn {
|
||||||
|
let key_val = vm.new_string(&name);
|
||||||
|
let holder = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
|
||||||
|
match vm.call_function_sync(replacer, holder, &[key_val, val]) {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.is_undefined() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(_) => return Err(()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val
|
||||||
|
};
|
||||||
|
|
||||||
let is_function = val.is_object() && {
|
let is_function = val.is_object() && {
|
||||||
let ptr = val.as_js_object_ptr();
|
let ptr = val.as_js_object_ptr();
|
||||||
if ptr.is_null() {
|
!ptr.is_null() && unsafe { (*ptr).is_function() }
|
||||||
false
|
|
||||||
} else {
|
|
||||||
unsafe { (*ptr).is_function() }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if val.is_undefined() || is_function {
|
if val.is_undefined() || is_function {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !first {
|
|
||||||
|
if !first && !has_space {
|
||||||
out.push(',');
|
out.push(',');
|
||||||
|
} else if !first {
|
||||||
|
out.push_str(",\n");
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
let name = vm.kernel_core().perm_interner().lookup(si).unwrap_or_default();
|
|
||||||
stringify_string(name, out);
|
if has_space {
|
||||||
out.push(':');
|
for _ in 0..indent_level + 1 {
|
||||||
jsvalue_to_json(vm, val, visited, out)?;
|
out.push_str(space);
|
||||||
|
}
|
||||||
|
stringify_string(&name, out);
|
||||||
|
out.push(':');
|
||||||
|
out.push(' ');
|
||||||
|
} else {
|
||||||
|
stringify_string(&name, out);
|
||||||
|
out.push(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
jsvalue_to_json(vm, val, visited, out, replacer_fn, replacer_whitelist, space, indent_level + 1, &name)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_space && !first {
|
||||||
|
out.push('\n');
|
||||||
|
for _ in 0..indent_level {
|
||||||
|
out.push_str(space);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.push('}');
|
out.push('}');
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn stringify_array<H: VmHost>(
|
fn stringify_array<H: VmHost>(
|
||||||
vm: &H, obj: &JsObject, visited: &mut HashSet<*const u8>, out: &mut String,
|
vm: &mut H, obj: &JsObject, visited: &mut HashSet<*const JsObject>, out: &mut String, replacer_fn: Option<JsValue>,
|
||||||
|
replacer_whitelist: Option<&HashSet<String>>, space: &str, indent_level: usize,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
|
let has_space = !space.is_empty();
|
||||||
out.push('[');
|
out.push('[');
|
||||||
|
|
||||||
let len = obj.prop_vec_len();
|
let len = obj.prop_vec_len();
|
||||||
for i in 0..len {
|
for i in 0..len {
|
||||||
if i > 0 {
|
if i > 0 && !has_space {
|
||||||
out.push(',');
|
out.push(',');
|
||||||
|
} else if i > 0 {
|
||||||
|
out.push_str(",\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if has_space {
|
||||||
|
out.push('\n');
|
||||||
|
for _ in 0..indent_level + 1 {
|
||||||
|
out.push_str(space);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let val = obj.get_prop_at(i);
|
let val = obj.get_prop_at(i);
|
||||||
|
|
||||||
|
let index_str = i.to_string();
|
||||||
|
|
||||||
|
// Replacer function callback (toJSON handled by jsvalue_to_json)
|
||||||
|
let val = if let Some(replacer) = replacer_fn {
|
||||||
|
let key_val = vm.new_string(&index_str);
|
||||||
|
let holder = JsValue::from_js_object(obj as *const JsObject as *mut JsObject);
|
||||||
|
match vm.call_function_sync(replacer, holder, &[key_val, val]) {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.is_undefined() {
|
||||||
|
out.push_str("null");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(_) => return Err(()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val
|
||||||
|
};
|
||||||
|
|
||||||
let is_function = val.is_object() && {
|
let is_function = val.is_object() && {
|
||||||
let ptr = val.as_js_object_ptr();
|
let ptr = val.as_js_object_ptr();
|
||||||
if ptr.is_null() {
|
!ptr.is_null() && unsafe { (*ptr).is_function() }
|
||||||
false
|
|
||||||
} else {
|
|
||||||
unsafe { (*ptr).is_function() }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if is_function || val.is_undefined() {
|
if is_function || val.is_undefined() {
|
||||||
out.push_str("null");
|
out.push_str("null");
|
||||||
} else {
|
} else {
|
||||||
jsvalue_to_json(vm, val, visited, out)?;
|
jsvalue_to_json(
|
||||||
|
vm,
|
||||||
|
val,
|
||||||
|
visited,
|
||||||
|
out,
|
||||||
|
replacer_fn,
|
||||||
|
replacer_whitelist,
|
||||||
|
space,
|
||||||
|
indent_level + 1,
|
||||||
|
&index_str,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_space && len > 0 {
|
||||||
|
out.push('\n');
|
||||||
|
for _ in 0..indent_level {
|
||||||
|
out.push_str(space);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.push(']');
|
out.push(']');
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(source: &str) -> Result<(Vm, JsValue), String> {
|
||||||
|
let allocator = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
|
||||||
|
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
let result = vm.run(&module)?;
|
||||||
|
Ok((vm, result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stringify_val(val: &JsValue) -> String {
|
||||||
|
if val.is_string() {
|
||||||
|
unsafe { (*val.as_string_ptr()).data.clone() }
|
||||||
|
} else {
|
||||||
|
format!("{:?}", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- replacer array ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacer_array_filters_properties() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({a:1,b:2,c:3}, ['a','c'])"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"a":1,"c":3}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacer_array_numeric_keys() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({a:1,b:2}, ['b'])"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"b":2}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- replacer function ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacer_function_skip_property() {
|
||||||
|
let (_, result) =
|
||||||
|
eval(r#"JSON.stringify({a:1,b:2}, function(k,v){if(k==='a')return undefined;return v})"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"b":2}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacer_function_array_null() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify([1,2,3], function(k,v){if(k==='1')return undefined;return v})"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), "[1,null,3]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacer_function_transform() {
|
||||||
|
let (_, result) =
|
||||||
|
eval(r#"JSON.stringify({a:1}, function(k,v){if(typeof v==='number')return v*2;return v})"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"a":2}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- space ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_number_indent() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({a:1}, null, 2)"#).unwrap();
|
||||||
|
let s = stringify_val(&result);
|
||||||
|
assert!(s.len() > 5, "expected indented output, got: {}", s);
|
||||||
|
assert!(s.starts_with('{'), "should start with brace");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_negative_clamped() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({a:1}, null, -5)"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"a":1}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- toJSON ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tojson_called_before_serialize() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({toJSON:function(){return {x:1}}})"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"{"x":1}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tojson_non_callable_ignored() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify({toJSON:'not-a-function', a:1})"#).unwrap();
|
||||||
|
// toJSON property is a string, should be serialized normally
|
||||||
|
assert!(stringify_val(&result).contains("toJSON"));
|
||||||
|
assert!(stringify_val(&result).contains(r#""a":1"#));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cycle detection ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_throws_type_error() {
|
||||||
|
let result = eval("var a={};a.self=a;JSON.stringify(a)");
|
||||||
|
match result {
|
||||||
|
Err(e) => assert!(e.to_lowercase().contains("circular"), "error should mention 'circular', got: {}", e),
|
||||||
|
Ok(_) => panic!("expected cycle error, got ok"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_cycle_distinct_objects_ok() {
|
||||||
|
let (_, result) = eval(r#"JSON.stringify([{a:1},{a:1}])"#).unwrap();
|
||||||
|
assert_eq!(stringify_val(&result), r#"[{"a":1},{"a":1}]"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- reviver ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reviver_transform_values() {
|
||||||
|
let (_, result) =
|
||||||
|
eval(r#"JSON.parse('{"a":1,"b":2}', function(k,v){if(typeof v==='number')return v*2;return v})"#).unwrap();
|
||||||
|
assert!(result.is_object());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reviver_delete_property() {
|
||||||
|
let (_, result) =
|
||||||
|
eval(r#"JSON.parse('{"a":1,"b":2}', function(k,v){if(k==='a')return undefined;return v})"#).unwrap();
|
||||||
|
// The property is soft-deleted (set to undefined)
|
||||||
|
// For test262 compatibility, stringify should omit undefined properties
|
||||||
|
assert!(result.is_object());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reviver_root_key_empty_string() {
|
||||||
|
let (_, result) = eval(r#"JSON.parse('42', function(k,v){return v+1})"#).unwrap();
|
||||||
|
assert!(result.is_int() || result.is_double(), "expected number");
|
||||||
|
if result.is_int() {
|
||||||
|
assert_eq!(result.as_int(), 43);
|
||||||
|
} else {
|
||||||
|
assert_eq!(result.as_double() as i32, 43);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reviver_no_reviver_works() {
|
||||||
|
let (_, result) = eval(r#"JSON.parse('{"a":1}')"#).unwrap();
|
||||||
|
assert!(result.is_object());
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ pub fn bind_json(core: &Arc<KernelCore>, session: &KernelSession, global: &mut J
|
|||||||
json,
|
json,
|
||||||
core,
|
core,
|
||||||
&[
|
&[
|
||||||
("parse", oxide_builtins::json::json_parse::<crate::vm::Vm> as *const (), 1),
|
("parse", oxide_builtins::json::json_parse::<crate::vm::Vm> as *const (), 2),
|
||||||
("stringify", oxide_builtins::json::json_stringify::<crate::vm::Vm> as *const (), 1),
|
("stringify", oxide_builtins::json::json_stringify::<crate::vm::Vm> as *const (), 3),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user