fix: sloppy 模式属性赋值静默 no-op——写失败按写方严格模式分派,内置写路径一律按严格

This commit is contained in:
2026-09-09 17:48:33 +08:00
parent 64f582d34b
commit c8bcff83f6
18 changed files with 529 additions and 89 deletions
@@ -33,17 +33,30 @@ fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
}
}
// ── 向不可写自身属性赋值抛 TypeError ──
// ── 严格模式向不可写自身属性赋值抛 TypeError ──
#[test]
fn assign_to_non_writable_throws() {
fn assign_to_non_writable_throws_in_strict() {
let result = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
"obj.x = 2",
"(function() { 'use strict'; obj.x = 2 })()",
]);
assert_err_contains(result, "read-only");
}
// ── sloppy 向不可写自身属性赋值静默失败(值不变、不抛) ──
#[test]
fn assign_to_non_writable_silently_fails_in_sloppy() {
let r = eval_many(&[
"var obj = {}",
"Object.defineProperty(obj, 'x', {value: 1, writable: false})",
"obj.x = 2",
"obj.x",
])
.unwrap();
assert!(r.is_int() && r.as_int() == 1, "sloppy 只读属性写应静默 no-op,值保持 1,实际 {:?}", r);
}
// ── 向可写自身属性赋值成功 ──
#[test]
fn assign_to_writable_succeeds() {
@@ -57,46 +70,87 @@ fn assign_to_writable_succeeds() {
assert!(r.is_int() && r.as_int() == 2, "assign to writable should set value to 2, got {:?}", r);
}
// ── 向原型上不可写的继承属性赋值抛错 ──
// ── 严格模式向原型上不可写的继承属性赋值抛错 ──
#[test]
fn assign_to_inherited_non_writable_throws() {
fn assign_to_inherited_non_writable_throws_in_strict() {
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",
"(function() { 'use strict'; child.x = 2 })()",
]);
assert_err_contains(result, "read-only");
}
// ── 原型属性不可写但子对象无自有属性时,赋值会在子对象上创建自有属性
// ponytail: spec allows shadowing when proto prop is configurable; engine doesn't yet.
// 引擎对不可写原型属性正确抛错;完整遮蔽作为后续工作。
// ── 继承只读数据属性:sloppy 赋值静默失败,不创建自有属性(不遮蔽) ──
#[test]
fn assign_non_writable_proto_configurable_prop_throws() {
fn assign_to_inherited_non_writable_silently_fails_in_sloppy() {
let (_vm, r) = eval_keep_vm(
&[
"var proto = {}",
"Object.defineProperty(proto, 'x', {value: 1, writable: false, enumerable: true, configurable: true})",
"var child = Object.create(proto)",
"child.x = 99",
"[child.x, child.hasOwnProperty('x')]",
]
.join("; "),
)
.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,
"sloppy 写只读继承属性应静默 no-op,值保持 1"
);
assert!(
arr.get_prop_at(1).is_bool() && !arr.get_prop_at(1).as_bool(),
"静默失败不得在子对象上创建自有属性"
);
}
// ── 严格模式:原型只读可配置属性赋值抛错(不可写数据属性不可遮蔽) ──
#[test]
fn assign_non_writable_proto_configurable_prop_throws_in_strict() {
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",
"(function() { 'use strict'; child.x = 99 })()",
]);
assert_err_contains(result, "read-only");
}
// ── Object.freeze 后写已有属性抛 TypeError ──
// ── 严格模式 Object.freeze 后写已有属性抛 TypeError ──
#[test]
fn frozen_write_existing_prop_throws() {
let result = eval_many(&["var o = Object.freeze({a: 1})", "o.a = 2"]);
fn frozen_write_existing_prop_throws_in_strict() {
let result = eval_many(&["var o = Object.freeze({a: 1})", "(function() { 'use strict'; o.a = 2 })()"]);
assert_err_contains(result, "read-only");
}
// ── Object.freeze 后写新属性抛 TypeError ──
// ── sloppy Object.freeze 后写已有属性静默失败 ──
#[test]
fn frozen_write_new_prop_throws() {
let result = eval_many(&["var o = Object.freeze({a: 1})", "o.b = 2"]);
fn frozen_write_existing_prop_silently_fails_in_sloppy() {
let r = eval_many(&["var o = Object.freeze({a: 1})", "o.a = 2", "o.a"]).unwrap();
assert!(r.is_int() && r.as_int() == 1, "sloppy 冻结对象写应静默 no-op,值保持 1,实际 {:?}", r);
}
// ── 严格模式 Object.freeze 后写新属性抛 TypeError ──
#[test]
fn frozen_write_new_prop_throws_in_strict() {
let result = eval_many(&["var o = Object.freeze({a: 1})", "(function() { 'use strict'; o.b = 2 })()"]);
assert_err_contains(result, "not extensible");
}
// ── sloppy Object.freeze 后写新属性静默失败(属性不创建) ──
#[test]
fn frozen_write_new_prop_silently_fails_in_sloppy() {
let r = eval_many(&["var o = Object.freeze({a: 1})", "o.b = 2", "o.b === undefined"]).unwrap();
assert!(
r.is_bool() && r.as_bool(),
"sloppy 冻结对象写新属性应静默 no-op,属性不创建,实际 {:?}",
r
);
}
// ── Object.freeze 后 defineProperty 抛 TypeError ──
#[test]
fn frozen_define_property_throws() {
@@ -104,13 +158,14 @@ fn frozen_define_property_throws() {
assert_err_contains(result, "not extensible");
}
// ── Object.seal 后写新属性抛错、删属性返回 false、改值生效 ──
// ── 严格模式 Object.seal 后写新属性抛错、删属性返回 false、改值生效 ──
#[test]
fn sealed_semantics() {
let (_vm, r) = eval_keep_vm(
&[
"var o = Object.seal({a: 1})",
"var new_throws = false; try { o.b = 2 } catch (e) { new_throws = true }",
"var new_throws = false",
"(function() { 'use strict'; try { o.b = 2 } catch (e) { new_throws = true } })()",
"var del = delete o.a",
"o.a = 5",
"[new_throws, del, o.a, o.hasOwnProperty('a')]",
@@ -121,30 +176,30 @@ fn sealed_semantics() {
let arr = unsafe { &*r.as_js_object_ptr() };
assert!(
arr.get_prop_at(0).is_bool() && arr.get_prop_at(0).as_bool(),
"new prop write should throw"
"strict new prop write should throw"
);
assert!(arr.get_prop_at(1).is_bool() && !arr.get_prop_at(1).as_bool(), "delete should be false");
assert!(arr.get_prop_at(2).is_int() && arr.get_prop_at(2).as_int() == 5, "value write should work");
assert!(arr.get_prop_at(3).is_bool() && arr.get_prop_at(3).as_bool(), "prop should remain own");
}
// ── Object.preventExtensions 后新属性不生效(sloppy 抛错),已有属性可写 ──
// ── Object.preventExtensions 后新属性写静默失效(sloppy),已有属性可写 ──
#[test]
fn prevent_extensions_semantics() {
let (_vm, r) = eval_keep_vm(
&[
"var o = Object.preventExtensions({a: 1})",
"var new_throws = false; try { o.b = 3 } catch (e) { new_throws = true }",
"var new_threw = false; try { o.b = 3 } catch (e) { new_threw = true }",
"o.a = 2",
"[new_throws, o.a, o.b === undefined]",
"[new_threw, o.a, o.b === undefined]",
]
.join("; "),
)
.unwrap();
let arr = unsafe { &*r.as_js_object_ptr() };
assert!(
arr.get_prop_at(0).is_bool() && arr.get_prop_at(0).as_bool(),
"new prop write should throw"
arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(),
"sloppy new prop write should silently fail"
);
assert!(
arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 2,
@@ -153,15 +208,15 @@ fn prevent_extensions_semantics() {
assert!(arr.get_prop_at(2).is_bool() && arr.get_prop_at(2).as_bool(), "new prop should be absent");
}
// ── Object.freeze 数组:元素写与 length 收缩均被拦截 ──
// ── 严格模式 Object.freeze 数组:元素写与 length 收缩均抛错 ──
#[test]
fn frozen_array_write_and_length_throws() {
fn frozen_array_write_and_length_throws_in_strict() {
let (_vm, r) = eval_keep_vm(
&[
"var a = Object.freeze([1, 2])",
"var e1 = false, e2 = false",
"try { a[0] = 9 } catch (e) { e1 = true }",
"try { a.length = 0 } catch (e) { e2 = true }",
"(function() { 'use strict'; try { a[0] = 9 } catch (e) { e1 = true } })()",
"(function() { 'use strict'; try { a.length = 0 } catch (e) { e2 = true } })()",
"[e1, e2, a[0], a.length, Object.getOwnPropertyDescriptor(a, '0').writable]",
]
.join("; "),
@@ -178,6 +233,27 @@ fn frozen_array_write_and_length_throws() {
);
}
// ── sloppy Object.freeze 数组:元素写静默失败(length 冻结检查仍抛错) ──
#[test]
fn frozen_array_element_write_silently_fails_in_sloppy() {
let (_vm, r) = eval_keep_vm(
&[
"var a = Object.freeze([1, 2])",
"var e1 = false",
"try { a[0] = 9 } catch (e) { e1 = true }",
"[e1, a[0]]",
]
.join("; "),
)
.unwrap();
let arr = unsafe { &*r.as_js_object_ptr() };
assert!(
arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(),
"sloppy 冻结数组元素写应静默 no-op"
);
assert!(arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 1, "element value unchanged");
}
// ── isFrozen/isSealed:手动 defineProperty 全部冻结(未调 freeze)也应判 true ──
#[test]
fn is_frozen_manual_define_props() {
@@ -212,19 +288,40 @@ fn reflect_ops_on_frozen_return_false() {
}
}
// ── freeze/seal 后 IC 直写路径失效:循环写仍被拦截 ──
// ── 严格模式 freeze 后 IC 直写路径失效:循环写仍被拦截 ──
#[test]
fn frozen_ic_path_still_blocks_write() {
fn frozen_ic_path_still_blocks_write_in_strict() {
let r = eval_many(&[
"var o = Object.freeze({a: 1})",
"var threw = false",
"for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } }",
"(function() { 'use strict'; for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } } })()",
"threw",
])
.unwrap();
assert!(r.is_bool() && r.as_bool(), "IC path should still throw on frozen write");
}
// ── sloppy freeze 后循环写静默失效(值不变、不抛) ──
#[test]
fn frozen_ic_path_silently_fails_in_sloppy() {
let (_vm, r) = eval_keep_vm(
&[
"var o = Object.freeze({a: 1})",
"var threw = false",
"for (var i = 0; i < 3; i++) { try { o.a = i } catch (e) { threw = true } }",
"[threw, o.a]",
]
.join("; "),
)
.unwrap();
let arr = unsafe { &*r.as_js_object_ptr() };
assert!(
arr.get_prop_at(0).is_bool() && !arr.get_prop_at(0).as_bool(),
"sloppy frozen write should silently fail"
);
assert!(arr.get_prop_at(1).is_int() && arr.get_prop_at(1).as_int() == 1, "value unchanged");
}
// ── sealed 对象 IC 路径:已有属性写正常 ──
#[test]
fn sealed_ic_path_allows_existing_write() {