fix: delete 增加 configurable 检查,defineProperty 修复部分更新值覆盖,枚举排序增加整数索引优先和非枚举过滤
This commit is contained in:
@@ -6,6 +6,13 @@ use oxide_types::value::JsValue;
|
||||
|
||||
use oxide_runtime_api::{NativeResult, VmHost};
|
||||
|
||||
fn is_integer_index(key: &str) -> bool {
|
||||
if key.is_empty() || key.len() > 1 && key.as_bytes()[0] == b'0' {
|
||||
return false;
|
||||
}
|
||||
key.bytes().all(|b| b.is_ascii_digit()) && key.parse::<u64>().unwrap_or(u64::MAX) < (1u64 << 32) - 1
|
||||
}
|
||||
|
||||
pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
let mut keys: Vec<(u32, u32)> = Vec::new();
|
||||
let shape_id = obj.shape_id();
|
||||
@@ -33,6 +40,16 @@ pub(crate) fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
keys.sort_by(|(a_si, _), (b_si, _)| {
|
||||
let a_str = vm.kernel_core().perm_interner().lookup(*a_si).unwrap_or("");
|
||||
let b_str = vm.kernel_core().perm_interner().lookup(*b_si).unwrap_or("");
|
||||
match (is_integer_index(a_str), is_integer_index(b_str)) {
|
||||
(true, true) => a_str.parse::<u32>().unwrap().cmp(&b_str.parse::<u32>().unwrap()),
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
(false, false) => std::cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
keys
|
||||
}
|
||||
|
||||
@@ -51,12 +68,20 @@ pub fn object_keys<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
{
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
let keys = walk_own_keys(vm, obj);
|
||||
key_names = keys
|
||||
let owned_keys: Vec<(u32, u32)> = keys
|
||||
.into_iter()
|
||||
.filter(|(_si, offset)| {
|
||||
obj.prop_meta_at(*offset)
|
||||
.map(|m| m.attributes.enumerable())
|
||||
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||
})
|
||||
.collect();
|
||||
key_names = owned_keys
|
||||
.iter()
|
||||
.map(|(si, _offset)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
// keys
|
||||
let n = key_names.len();
|
||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let arr = vm.alloc_object(JsObject::new_array(
|
||||
@@ -202,10 +227,33 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
|
||||
));
|
||||
}
|
||||
|
||||
let existing_pos = {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
vm.get_own_property_slot(obj, si)
|
||||
};
|
||||
let existing_meta = existing_pos.and_then(|pos| {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
obj.prop_meta_at(pos)
|
||||
});
|
||||
let existing_value = existing_pos.map_or(JsValue::undefined(), |pos| {
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
obj.get_prop_at(pos)
|
||||
});
|
||||
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
if has_accessor {
|
||||
let get = get_field.unwrap_or(JsValue::undefined());
|
||||
let set = set_field.unwrap_or(JsValue::undefined());
|
||||
let get = get_field.unwrap_or_else(|| {
|
||||
existing_meta
|
||||
.filter(|m| m.is_accessor)
|
||||
.map(|m| m.get)
|
||||
.unwrap_or(JsValue::undefined())
|
||||
});
|
||||
let set = set_field.unwrap_or_else(|| {
|
||||
existing_meta
|
||||
.filter(|m| m.is_accessor)
|
||||
.map(|m| m.set)
|
||||
.unwrap_or(JsValue::undefined())
|
||||
});
|
||||
if (!get.is_undefined() && !is_callable(get)) || (!set.is_undefined() && !is_callable(set)) {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
@@ -218,15 +266,46 @@ pub fn object_define_property<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResul
|
||||
{
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||
}
|
||||
} else {
|
||||
let value = value_field.unwrap_or(JsValue::undefined());
|
||||
let writable = writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false);
|
||||
} else if has_data {
|
||||
let value = if existing_pos.is_some() {
|
||||
value_field.unwrap_or(existing_value)
|
||||
} else {
|
||||
value_field.unwrap_or(JsValue::undefined())
|
||||
};
|
||||
let writable = if existing_pos.is_some() {
|
||||
writable_field
|
||||
.map(oxide_runtime_api::to_boolean)
|
||||
.unwrap_or_else(|| existing_meta.map(|m| m.attributes.writable()).unwrap_or(false))
|
||||
} else {
|
||||
writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false)
|
||||
};
|
||||
if vm
|
||||
.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable))
|
||||
.is_err()
|
||||
{
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||
}
|
||||
} else {
|
||||
if existing_pos.is_none() {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"Cannot define new property without value or accessor",
|
||||
));
|
||||
}
|
||||
let is_accessor = existing_meta.map(|m| m.is_accessor).unwrap_or(false);
|
||||
let writable = existing_meta.map(|m| m.attributes.writable()).unwrap_or(true);
|
||||
let attrs = PropAttributes::new(writable, enumerable, configurable);
|
||||
if is_accessor {
|
||||
let get = existing_meta.map(|m| m.get).unwrap_or(JsValue::undefined());
|
||||
let set = existing_meta.map(|m| m.set).unwrap_or(JsValue::undefined());
|
||||
if vm.define_accessor_property(obj, si, get, set, attrs).is_err() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||
}
|
||||
} else {
|
||||
if vm.define_data_property(obj, si, existing_value, attrs).is_err() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot define property"));
|
||||
}
|
||||
}
|
||||
}
|
||||
NativeResult::Ok(obj_val)
|
||||
}
|
||||
@@ -666,7 +745,15 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let obj_ptr = native_try!(require_obj_arg(vm, args, "entries"));
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
let keys = walk_own_keys(vm, obj);
|
||||
let n = keys.len();
|
||||
let owned_keys: Vec<(u32, u32)> = keys
|
||||
.into_iter()
|
||||
.filter(|(_si, offset)| {
|
||||
obj.prop_meta_at(*offset)
|
||||
.map(|m| m.attributes.enumerable())
|
||||
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||
})
|
||||
.collect();
|
||||
let n = owned_keys.len();
|
||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let arr = vm.alloc_object(JsObject::new_array(
|
||||
EMPTY_SHAPE_ID,
|
||||
@@ -674,7 +761,7 @@ pub fn object_entries<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
n,
|
||||
vm.epoch().bump(),
|
||||
));
|
||||
for (i, (si, offset)) in keys.iter().enumerate() {
|
||||
for (i, (si, offset)) in owned_keys.iter().enumerate() {
|
||||
let key_str = vm.kernel_core().perm_interner().lookup(*si).unwrap_or_default();
|
||||
let key_val = vm.new_string(key_str);
|
||||
let val = obj.get_prop_at(*offset);
|
||||
@@ -701,7 +788,15 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let obj_ptr = native_try!(require_obj_arg(vm, args, "values"));
|
||||
let obj = unsafe { &*obj_ptr };
|
||||
let keys = walk_own_keys(vm, obj);
|
||||
let n = keys.len();
|
||||
let owned_keys: Vec<(u32, u32)> = keys
|
||||
.into_iter()
|
||||
.filter(|(_si, offset)| {
|
||||
obj.prop_meta_at(*offset)
|
||||
.map(|m| m.attributes.enumerable())
|
||||
.unwrap_or(PropAttributes::DEFAULT_DATA.enumerable())
|
||||
})
|
||||
.collect();
|
||||
let n = owned_keys.len();
|
||||
let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||
let arr = vm.alloc_object(JsObject::new_array(
|
||||
EMPTY_SHAPE_ID,
|
||||
@@ -709,7 +804,7 @@ pub fn object_values<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
n,
|
||||
vm.epoch().bump(),
|
||||
));
|
||||
for (i, (_si, offset)) in keys.iter().enumerate() {
|
||||
for (i, (_si, offset)) in owned_keys.iter().enumerate() {
|
||||
let val = obj.get_prop_at(*offset);
|
||||
unsafe {
|
||||
(*arr).set_prop_at(i, val);
|
||||
|
||||
@@ -389,6 +389,10 @@ impl Vm {
|
||||
};
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||
if !configurable {
|
||||
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||
}
|
||||
obj.set_prop_at(pos, JsValue::undefined());
|
||||
}
|
||||
self.regs[rd] = JsValue::bool(true);
|
||||
@@ -403,6 +407,10 @@ impl Vm {
|
||||
};
|
||||
let obj = unsafe { &mut *obj_ptr };
|
||||
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
|
||||
let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true);
|
||||
if !configurable {
|
||||
return self.raise_type_error("cannot delete non-configurable property").map(|()| false);
|
||||
}
|
||||
obj.set_prop_at(pos, JsValue::undefined());
|
||||
}
|
||||
self.regs[rd] = JsValue::bool(true);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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 eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Partial writable update preserves existing value --
|
||||
#[test]
|
||||
fn define_property_partial_writable_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{writable:false}); obj.x").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Mixed descriptor (value+get) throws TypeError --
|
||||
#[test]
|
||||
fn define_property_mixed_value_get_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{value:1,get:function(){}});");
|
||||
assert_err_contains(result, "mix");
|
||||
}
|
||||
|
||||
// -- Mixed descriptor (writable+set) throws TypeError --
|
||||
#[test]
|
||||
fn define_property_mixed_writable_set_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{writable:true,set:function(){}});");
|
||||
assert_err_contains(result, "mix");
|
||||
}
|
||||
|
||||
// -- Partial enumerable-only update preserves value --
|
||||
#[test]
|
||||
fn define_property_partial_enumerable_only_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=42; Object.defineProperty(obj,'x',{enumerable:true}); obj.x").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "value should be preserved at 42, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- getOwnPropertyDescriptor returns data fields (no get/set) --
|
||||
#[test]
|
||||
fn get_own_property_descriptor_data_fields() {
|
||||
let r = eval_str(
|
||||
"var obj={x:1}; var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.value,d.writable,d.enumerable,d.configurable,d.get,d.set]",
|
||||
)
|
||||
.unwrap();
|
||||
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||
let v = obj.get_prop_at(0);
|
||||
assert!(v.is_int() && v.as_int() == 1, "value should be 1, got {:?}", v);
|
||||
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "writable should be true");
|
||||
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||
assert!(
|
||||
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||
"configurable should be true"
|
||||
);
|
||||
assert!(obj.get_prop_at(4).is_undefined(), "get should be undefined");
|
||||
assert!(obj.get_prop_at(5).is_undefined(), "set should be undefined");
|
||||
}
|
||||
|
||||
// -- getOwnPropertyDescriptor returns accessor fields (no value/writable) --
|
||||
#[test]
|
||||
fn get_own_property_descriptor_accessor_fields() {
|
||||
let r = eval_str(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{get:function(){return 1;},enumerable:true,configurable:true}); var d=Object.getOwnPropertyDescriptor(obj,'x'); [d.get!==undefined,d.set===undefined,d.enumerable,d.configurable,d.value===undefined,d.writable===undefined]",
|
||||
)
|
||||
.unwrap();
|
||||
let obj = unsafe { &*r.as_js_object_ptr() };
|
||||
assert!(obj.get_prop_at(0).is_bool() && obj.get_prop_at(0).as_bool(), "get should be defined");
|
||||
assert!(obj.get_prop_at(1).is_bool() && obj.get_prop_at(1).as_bool(), "set should be undefined");
|
||||
assert!(obj.get_prop_at(2).is_bool() && obj.get_prop_at(2).as_bool(), "enumerable should be true");
|
||||
assert!(
|
||||
obj.get_prop_at(3).is_bool() && obj.get_prop_at(3).as_bool(),
|
||||
"configurable should be true"
|
||||
);
|
||||
assert!(obj.get_prop_at(4).is_bool() && obj.get_prop_at(4).as_bool(), "value should be undefined");
|
||||
assert!(
|
||||
obj.get_prop_at(5).is_bool() && obj.get_prop_at(5).as_bool(),
|
||||
"writable should be undefined"
|
||||
);
|
||||
}
|
||||
|
||||
// -- New property without value or accessor throws --
|
||||
#[test]
|
||||
fn define_property_new_without_value_throws() {
|
||||
let result = eval_str("var obj={}; Object.defineProperty(obj,'x',{enumerable:true})");
|
||||
assert_err_contains(result, "Cannot define new property without value or accessor");
|
||||
}
|
||||
|
||||
// -- Partial configurable on existing property preserves value --
|
||||
#[test]
|
||||
fn define_property_partial_configurable_preserves_value() {
|
||||
let r = eval_str("var obj={}; obj.x=1; Object.defineProperty(obj,'x',{configurable:false}); [obj.x]").unwrap();
|
||||
let arr = unsafe { &*r.as_js_object_ptr() };
|
||||
assert!(arr.get_prop_at(0).is_int() && arr.get_prop_at(0).as_int() == 1, "value should be 1");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<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();
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||
let source = lines.join("; ");
|
||||
eval(&source)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Delete configurable property succeeds --
|
||||
#[test]
|
||||
fn delete_configurable_property_succeeds_and_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, configurable: true})",
|
||||
"delete obj.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete configurable should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete non-configurable property throws --
|
||||
#[test]
|
||||
fn delete_non_configurable_throws_type_error() {
|
||||
let result = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, configurable: false})",
|
||||
"delete obj.x",
|
||||
]);
|
||||
assert_err_contains(result, "cannot delete non-configurable property");
|
||||
}
|
||||
|
||||
// -- Delete non-existent property returns true --
|
||||
#[test]
|
||||
fn delete_non_existent_returns_true() {
|
||||
let r = eval_many(&["var obj = {}", "delete obj.x"]).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete non-existent should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete default (no explicit defineProperty) configurable succeeds --
|
||||
#[test]
|
||||
fn delete_default_configurable_succeeds() {
|
||||
let r = eval_many(&["var obj = {}", "obj.x = 1", "delete obj.x"]).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "delete default configurable should return true, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Delete non-configurable proto property on child: child doesn't own it, so returns true --
|
||||
#[test]
|
||||
fn delete_non_configurable_proto_property_on_child_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var proto = {}",
|
||||
"proto.x = 1",
|
||||
"Object.defineProperty(proto, 'x', {configurable: false})",
|
||||
"var child = Object.create(proto)",
|
||||
"delete child.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(
|
||||
r.is_bool() && r.as_bool(),
|
||||
"delete on child for non-configurable proto prop should return true (not own), got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
|
||||
// -- Delete configurable proto-level property through child --
|
||||
#[test]
|
||||
fn delete_configurable_proto_property_on_child_returns_true() {
|
||||
let r = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, configurable: true, enumerable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"delete child.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(
|
||||
r.is_bool() && r.as_bool(),
|
||||
"delete configurable proto prop from child should return true (not own), got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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 eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
fn keys_to_strings(result: JsValue) -> Vec<String> {
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
let len = obj.prop_count();
|
||||
let mut keys = Vec::new();
|
||||
for i in 0..len {
|
||||
let v = obj.get_prop_at(i);
|
||||
if v.is_string() {
|
||||
// SAFETY: string value pointing into VM arena (caller keeps vm alive)
|
||||
let s = unsafe { &*v.as_string_ptr() };
|
||||
keys.push(s.data.clone());
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
// -- Integer keys in ascending order --
|
||||
#[test]
|
||||
fn keys_integer_order_ascending() {
|
||||
let r = eval_str("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj).length").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 3, "should have 3 keys, got {:?}", r);
|
||||
let (_vm, r2) = eval("var obj={}; obj[3]='c'; obj[1]='a'; obj[2]='b'; Object.keys(obj)").unwrap();
|
||||
let keys = keys_to_strings(r2);
|
||||
assert_eq!(keys, vec!["1", "2", "3"], "integer keys should be ascending, got {:?}", keys);
|
||||
}
|
||||
|
||||
// -- Mixed keys: integers first, then strings in creation order --
|
||||
#[test]
|
||||
fn keys_mixed_order_integers_first() {
|
||||
let (_vm, r) = eval("var obj={}; obj[2]='b'; obj['1']='a'; obj['a']=1; Object.keys(obj)").unwrap();
|
||||
let keys = keys_to_strings(r);
|
||||
assert!(keys[0] == "1" || keys[0] == "2", "first key should be integer, got {:?}", keys);
|
||||
let str_idx = keys.iter().position(|k| k == "a").unwrap();
|
||||
let int_positions: Vec<usize> = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, k)| k.parse::<u32>().is_ok())
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
for ip in &int_positions {
|
||||
assert!(*ip < str_idx, "integer at {} should come before string 'a' at {}", ip, str_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Object.keys excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn keys_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.keys(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "keys should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Object.getOwnPropertyNames includes non-enumerable properties --
|
||||
#[test]
|
||||
fn get_own_property_names_includes_non_enumerable() {
|
||||
let r = eval_str("var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.getOwnPropertyNames(obj).length").unwrap();
|
||||
assert!(
|
||||
r.is_int() && r.as_int() == 2,
|
||||
"getOwnPropertyNames should include non-enumerable, got {:?}",
|
||||
r
|
||||
);
|
||||
}
|
||||
|
||||
// -- Object.entries excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn entries_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.entries(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "entries should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Object.values excludes non-enumerable properties --
|
||||
#[test]
|
||||
fn values_excludes_non_enumerable() {
|
||||
let r = eval_str(
|
||||
"var obj={a:1}; Object.defineProperty(obj,'hidden',{value:2,enumerable:false}); Object.values(obj).length",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "values should exclude non-enumerable, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Leading zero strings are NOT integer indices --
|
||||
#[test]
|
||||
fn keys_with_leading_zero_not_integer_index() {
|
||||
let (_vm, r) = eval("Object.keys({'01':'a','1':'b'})").unwrap();
|
||||
let keys = keys_to_strings(r);
|
||||
assert!(keys.contains(&"01".to_string()), "should contain '01' as string key");
|
||||
assert!(keys.contains(&"1".to_string()), "should contain '1' as integer");
|
||||
let idx_01 = keys.iter().position(|k| k == "01").unwrap();
|
||||
let idx_1 = keys.iter().position(|k| k == "1").unwrap();
|
||||
assert!(idx_1 < idx_01, "integer '1' should come before string '01'");
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 eval_str(source: &str) -> Result<JsValue, String> {
|
||||
eval(source).map(|(_vm, v)| v)
|
||||
}
|
||||
|
||||
// Getter on proto sets marker on receiver, then check receiver has marker (not proto)
|
||||
#[test]
|
||||
fn getter_on_proto_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._marker=1; return this._marker; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "getter should set marker on child (receiver), got {:?}", r);
|
||||
}
|
||||
|
||||
// Getter on deep proto chain: grandchild → child → proto. Getter defined on proto.
|
||||
#[test]
|
||||
fn getter_on_proto_deep_chain_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._deep=this; return 99; },enumerable:true,configurable:true}); var child=Object.create(proto); var grandchild=Object.create(child); grandchild.x; grandchild._deep!==undefined"
|
||||
).unwrap();
|
||||
assert!(r.is_bool() && r.as_bool(), "grandchild should have _deep marker, got {:?}", r);
|
||||
}
|
||||
|
||||
// Setter on proto receives receiver as this, sets marker on receiver not proto
|
||||
#[test]
|
||||
fn setter_on_proto_this_is_receiver() {
|
||||
let (_vm, r) = eval(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._set_by_setter=v; },get:function(){ return this._set_by_setter; },enumerable:true,configurable:true}); var child=Object.create(proto); child.x='receiver_test'; child._set_by_setter"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "child should have _set_by_setter, got {:?}", r);
|
||||
}
|
||||
|
||||
// Dynamic getter (obj[expr]) preserves receiver
|
||||
#[test]
|
||||
fn dynamic_getter_on_proto_this_is_receiver() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){ this._dyn=1; return this._dyn; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 1, "dynamic getter should set marker on child, got {:?}", r);
|
||||
}
|
||||
|
||||
// Dynamic setter (obj[expr]=val) preserves receiver
|
||||
#[test]
|
||||
fn dynamic_setter_on_proto_this_is_receiver() {
|
||||
let (_vm, r) = eval(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{set:function(v){ this._dyn_set=v; },get:function(){ return this._dyn_set; },enumerable:true,configurable:true}); var child=Object.create(proto); child['x']='dyn_test'; child._dyn_set"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "child should have _dyn_set after dynamic setter, got {:?}", r);
|
||||
}
|
||||
|
||||
// Own getter on object — this is the object itself
|
||||
#[test]
|
||||
fn own_getter_this_is_self() {
|
||||
let r = eval_str(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{get:function(){ this._own=42; return this._own; },enumerable:true,configurable:true}); obj.x"
|
||||
).unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 42, "own getter should set marker on self, got {:?}", r);
|
||||
}
|
||||
|
||||
// Own setter on object — this is the object itself
|
||||
#[test]
|
||||
fn own_setter_this_is_self() {
|
||||
let (_vm, r) = eval(
|
||||
"var obj={}; Object.defineProperty(obj,'x',{set:function(v){ this._own_set=v; },get:function(){ return this._own_set; },enumerable:true,configurable:true}); obj.x='own'; obj._own_set"
|
||||
).unwrap();
|
||||
assert!(r.is_string(), "own setter should set marker on self, got {:?}", r);
|
||||
}
|
||||
|
||||
// Getter returning undefined does not crash
|
||||
#[test]
|
||||
fn getter_returns_undefined_does_not_crash() {
|
||||
let r = eval_str(
|
||||
"var proto={}; Object.defineProperty(proto,'x',{get:function(){},enumerable:true,configurable:true}); var child=Object.create(proto); child.x"
|
||||
).unwrap();
|
||||
assert!(r.is_undefined(), "getter returning nothing should yield undefined, got {:?}", r);
|
||||
}
|
||||
|
||||
// String .length still works after proto getter is defined (primitive auto-boxing not broken)
|
||||
#[test]
|
||||
fn string_length_still_works_after_proto_getter() {
|
||||
let r = eval_str("'hello'.length").unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 5, "string.length should be 5, got {:?}", r);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> Result<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();
|
||||
vm.run(&module)
|
||||
}
|
||||
|
||||
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
|
||||
let source = lines.join("; ");
|
||||
eval(&source)
|
||||
}
|
||||
|
||||
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
|
||||
match result {
|
||||
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
|
||||
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
|
||||
}
|
||||
}
|
||||
|
||||
// -- Assign to non-writable own property throws TypeError --
|
||||
#[test]
|
||||
fn assign_to_non_writable_throws() {
|
||||
let result = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
|
||||
"obj.x = 2",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
|
||||
// -- Assign to writable own property succeeds --
|
||||
#[test]
|
||||
fn assign_to_writable_succeeds() {
|
||||
let r = eval_many(&[
|
||||
"var obj = {}",
|
||||
"Object.defineProperty(obj, 'x', {value: 1, writable: true})",
|
||||
"obj.x = 2",
|
||||
"obj.x",
|
||||
])
|
||||
.unwrap();
|
||||
assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r);
|
||||
}
|
||||
|
||||
// -- Assign to inherited non-writable property on proto throws --
|
||||
#[test]
|
||||
fn assign_to_inherited_non_writable_throws() {
|
||||
let result = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"child.x = 2",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
|
||||
// -- Assign creates own property on child when proto's is non-writable but child has no own prop
|
||||
// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet.
|
||||
// The engine correctly throws for non-writable proto props. Full shadowing is a follow-up.
|
||||
#[test]
|
||||
fn assign_non_writable_proto_configurable_prop_throws() {
|
||||
let result = eval_many(&[
|
||||
"var proto = {}",
|
||||
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
|
||||
"var child = Object.create(proto)",
|
||||
"child.x = 99",
|
||||
]);
|
||||
assert_err_contains(result, "read-only");
|
||||
}
|
||||
Reference in New Issue
Block a user