fix: delete 增加 configurable 检查,defineProperty 修复部分更新值覆盖,枚举排序增加整数索引优先和非枚举过滤

This commit is contained in:
爱特每羊队
2026-06-27 16:34:48 +08:00
parent efb5f328f5
commit 6e74881cde
7 changed files with 597 additions and 11 deletions
+8
View File
@@ -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");
}