mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: 类构造器属性序修正 + prototype 哨兵键枚举修复
This commit is contained in:
@@ -55,8 +55,7 @@ pub fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
}
|
||||
for id in shape_ids.iter().rev() {
|
||||
if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) {
|
||||
if shape.property_name != 0
|
||||
&& !is_symbol_key(shape.property_name)
|
||||
if !is_symbol_key(shape.property_name)
|
||||
&& !is_private_name_key(shape.property_name)
|
||||
{
|
||||
// 绝对存储索引:数组命名属性位于元素区之后。
|
||||
@@ -66,17 +65,47 @@ pub fn walk_own_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
keys.sort_by(|(a_si, _), (b_si, _)| {
|
||||
// 整数键(含 INT 区间键)以数值升序排在普通字符串键之前。
|
||||
let a_idx = int_or_string_index(vm, *a_si);
|
||||
let b_idx = int_or_string_index(vm, *b_si);
|
||||
match (a_idx, b_idx) {
|
||||
(Some(ai), Some(bi)) => ai.cmp(&bi),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
// 类构造器属性序修正:shape 链构建序为 prototype→length→name(叶→根),
|
||||
// 规范要求的插入序为 length→name→prototype。按规范重排字符串键。
|
||||
if obj.is_class_constructor() {
|
||||
let perm = vm.kernel_core().perm_interner();
|
||||
let canonical = ["length", "name", "prototype"];
|
||||
let mut canonical_map: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||
for (i, &name) in canonical.iter().enumerate() {
|
||||
canonical_map.insert(name, i);
|
||||
}
|
||||
});
|
||||
// 按规范序重新排列字符串键,整数键保持原位。
|
||||
keys.sort_by(|(a_si, _), (b_si, _)| {
|
||||
let a_idx = int_or_string_index(vm, *a_si);
|
||||
let b_idx = int_or_string_index(vm, *b_si);
|
||||
match (a_idx, b_idx) {
|
||||
(Some(ai), Some(bi)) => ai.cmp(&bi),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => {
|
||||
// 两者均为字符串键:查规范序。
|
||||
let a_name = perm.lookup(*a_si).unwrap_or("");
|
||||
let b_name = perm.lookup(*b_si).unwrap_or("");
|
||||
let a_rank = canonical_map.get(a_name).copied().unwrap_or(usize::MAX);
|
||||
let b_rank = canonical_map.get(b_name).copied().unwrap_or(usize::MAX);
|
||||
// 规范键按序排,其余保持原序。
|
||||
a_rank.cmp(&b_rank)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
keys.sort_by(|(a_si, _), (b_si, _)| {
|
||||
// 整数键(含 INT 区间键)以数值升序排在普通字符串键之前。
|
||||
let a_idx = int_or_string_index(vm, *a_si);
|
||||
let b_idx = int_or_string_index(vm, *b_si);
|
||||
match (a_idx, b_idx) {
|
||||
(Some(ai), Some(bi)) => ai.cmp(&bi),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
@@ -132,7 +161,7 @@ pub(crate) fn walk_own_symbol_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u3
|
||||
if let Some(shape) = vm.kernel_core().shape_forge().get_shape(*id) {
|
||||
// Symbol 键节点按绝对槽位回传(数组命名属性位于元素区之后)。
|
||||
let store = if obj.is_array() { obj.array_prop_count + pos } else { pos };
|
||||
if shape.property_name != 0 && is_symbol_key(shape.property_name) {
|
||||
if is_symbol_key(shape.property_name) {
|
||||
keys.push((shape.property_name, store));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use oxide_kernel::{kernel_error, kernel_info};
|
||||
use oxide_log::{Level, SUBSYSTEM_COUNT};
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_types::object::JsObject;
|
||||
use oxide_types::private_key::{is_private_name_key, is_symbol_key};
|
||||
use oxide_vm::vm::Vm;
|
||||
use oxide_vm::vm_error;
|
||||
use oxide_vm::vm_pool::VmPool;
|
||||
@@ -249,7 +250,8 @@ fn format_object(
|
||||
let mut pos: u32 = 0;
|
||||
for id in shape_ids.iter().rev() {
|
||||
if let Some(shape) = shape_forge.get_shape(*id) {
|
||||
if shape.property_name != 0 {
|
||||
// 跳过 Symbol/私有名键,仅展示字符串属性名。
|
||||
if !is_symbol_key(shape.property_name) && !is_private_name_key(shape.property_name) {
|
||||
let prop_val = obj.get_prop_at(pos);
|
||||
if prop_val.is_undefined() {
|
||||
pos += 1;
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Vm {
|
||||
let mut session = KernelSession::new(&core);
|
||||
bindings::init_kernel_builtins(&core, &mut session);
|
||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
||||
// 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。
|
||||
let length_si = core.perm_interner().intern("length").0;
|
||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
|
||||
@@ -172,7 +172,7 @@ impl Vm {
|
||||
let mut session = KernelSession::new(&core);
|
||||
bindings::init_kernel_builtins(&core, &mut session);
|
||||
// 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到
|
||||
// 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。
|
||||
// 稳定的 id,用于运行期常见属性(如函数 length 槽)的快路径。
|
||||
let length_si = core.perm_interner().intern("length").0;
|
||||
let obj_proto = P::clone(&session.builtin_world().object_proto);
|
||||
// 提前缓存执行期字符串 GC 初始水位(构造后 config 不再变化)。
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! 回归测试:si-0 哨兵键("prototype")枚举正确性。
|
||||
//!
|
||||
//! 根因:`builtin_labels` 将 "prototype" intern 为 si 0,但三处
|
||||
//! `property_name != 0` 守卫将其排除在枚举之外,导致 delete 重建、
|
||||
//! getOwnPropertyNames 等路径丢失 "prototype" 键。
|
||||
//!
|
||||
//! 修复:删除守卫,si 0 成为一等属性键。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxide_compiler::compiler::Compiler;
|
||||
use oxide_parser::Allocator;
|
||||
use oxide_types::value::JsValue;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn eval(source: &str) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let program = match oxide_parser::parse(&allocator, source) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return format!("parse error: {}", e[0].message),
|
||||
};
|
||||
let module = match Compiler::new().compile(&program) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return format!("compile error: {e}"),
|
||||
};
|
||||
let mut vm = Vm::new();
|
||||
match vm.run(&Arc::new(module)) {
|
||||
Ok(result) => js_value_to_str(result),
|
||||
Err(e) => format!("vm error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_json(source: &str) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let program = match oxide_parser::parse(&allocator, source) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return format!("parse error: {}", e[0].message),
|
||||
};
|
||||
let module = match Compiler::new().compile(&program) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return format!("compile error: {e}"),
|
||||
};
|
||||
let mut vm = Vm::new();
|
||||
match vm.run(&Arc::new(module)) {
|
||||
Ok(result) => js_value_to_str(result),
|
||||
Err(e) => format!("vm error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 VM 返回值转为 JS 可见字符串:字符串值直接取文本,整型/布尔/undefined
|
||||
/// 走 Debug display,对象走 Debug display(测试中不用于对象比较)。
|
||||
fn js_value_to_str(v: JsValue) -> String {
|
||||
if v.is_string() {
|
||||
// SAFETY: 字符串值指向 VM arena(调用方保持 vm 存活)。
|
||||
unsafe { &*v.as_string_ptr() }.to_owned_string()
|
||||
} else {
|
||||
format!("{v}")
|
||||
}
|
||||
}
|
||||
|
||||
// ── 机制 A:delete 重建后 "prototype" 不丢失 ──
|
||||
|
||||
#[test]
|
||||
fn delete_others_preserves_prototype() {
|
||||
// {prototype:{m:1},x:2}; delete o.x; typeof o.prototype → "object"
|
||||
assert_eq!(eval("var o={prototype:{m:1},x:2}; delete o.x; typeof o.prototype"), "object");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_class_prototype_method_preserves_prototype_prop() {
|
||||
// class C{prototype(){}x(){}} delete C.prototype.x; typeof C.prototype.prototype → "function"
|
||||
assert_eq!(
|
||||
eval("class C{prototype(){}x(){}} delete C.prototype.x; typeof C.prototype.prototype"),
|
||||
"function"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Object.keys / getOwnPropertyNames 包含 "prototype" ──
|
||||
|
||||
#[test]
|
||||
fn object_keys_contains_prototype_key() {
|
||||
// Object.keys({prototype:1}) 应返回 ["prototype"]
|
||||
let r = eval("var o={prototype:1}; Object.keys(o).length");
|
||||
assert_eq!(r, "1", "Object.keys should include prototype key");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_own_property_names_class_constructor_has_prototype() {
|
||||
// Object.getOwnPropertyNames(class A{}) 应包含 "prototype"
|
||||
let r = eval("Object.getOwnPropertyNames(class A{}).length");
|
||||
assert_eq!(r, "3", "class constructor should have length, name, prototype");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_own_property_names_object_literal_has_prototype() {
|
||||
// Object.getOwnPropertyNames({prototype:1}) 应包含 "prototype"
|
||||
let r = eval("Object.getOwnPropertyNames({prototype:1}).length");
|
||||
assert_eq!(r, "1", "object literal should have prototype key");
|
||||
}
|
||||
|
||||
// ── 类构造器属性序:[length, name, prototype] ──
|
||||
|
||||
#[test]
|
||||
fn class_constructor_property_order() {
|
||||
// 类构造器属性序应为 [length, name, prototype]
|
||||
let r = eval("Object.getOwnPropertyNames(class A{}).join(',')");
|
||||
assert_eq!(r, "length,name,prototype", "class constructor property order");
|
||||
}
|
||||
|
||||
// ── JSON.stringify 包含 "prototype" ──
|
||||
|
||||
#[test]
|
||||
fn json_stringify_includes_prototype() {
|
||||
// JSON.stringify({prototype:1}) → "{\"prototype\":1}"
|
||||
let r = eval_json("JSON.stringify({prototype:1})");
|
||||
assert_eq!(r, "{\"prototype\":1}", "JSON.stringify should include prototype key");
|
||||
}
|
||||
|
||||
// ── Reflect.ownKeys 包含 "prototype" ──
|
||||
|
||||
#[test]
|
||||
fn reflect_own_keys_includes_prototype() {
|
||||
let r = eval("Reflect.ownKeys({prototype:1}).length");
|
||||
assert_eq!(r, "1", "Reflect.ownKeys should include prototype key");
|
||||
}
|
||||
|
||||
// ── for-in 不产出 "prototype"(不可枚举,修后仍正确)──
|
||||
|
||||
#[test]
|
||||
fn for_in_excludes_non_enumerable_prototype() {
|
||||
// 构造器 "prototype" 不可枚举,for-in 不应产出
|
||||
let r = eval("var c=0; class A{} for(var k in A) c++; c");
|
||||
assert_eq!(r, "0", "for-in should not enumerate non-enumerable prototype on class constructor");
|
||||
}
|
||||
|
||||
// ── 存在性检查不受影响 ──
|
||||
|
||||
#[test]
|
||||
fn has_own_property_prototype_true() {
|
||||
let r = eval("Object.getOwnPropertyDescriptor(class A{}, 'prototype') !== undefined");
|
||||
assert_eq!(r, "true", "hasOwnProperty should return true for prototype");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_operator_prototype_true() {
|
||||
let r = eval("'prototype' in function f(){}");
|
||||
assert_eq!(r, "true", "'prototype' in function should be true");
|
||||
}
|
||||
|
||||
// ── 对象字面量 "prototype" 属性读写 ──
|
||||
|
||||
#[test]
|
||||
fn object_literal_prototype_read_write() {
|
||||
let r = eval("var o={prototype:42}; o.prototype");
|
||||
assert_eq!(r, "42", "should read prototype property from object literal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_prototype_delete() {
|
||||
let r = eval("var o={prototype:42,x:1}; delete o.x; o.prototype");
|
||||
assert_eq!(r, "42", "delete other prop should not affect prototype property");
|
||||
}
|
||||
Reference in New Issue
Block a user