Files
OxideJS-New/crates/oxide_vm/tests/module_namespace_tests.rs
T

318 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 模块命名空间 @@toStringTag Symbol 键与自身键枚举口径:
//! 命名空间标签以 well-known symbol 键写入,`Reflect.ownKeys` 的 Symbol 段跟在
//! 字符串段之后,字符串枚举面(getOwnPropertyNames / Object.keys)不受污染。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule};
use oxide_vm::vm::Vm;
/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。
struct FileLoader;
impl ModuleSourceLoader for FileLoader {
fn resolve(
&mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)],
) -> Result<ResolvedModule, String> {
let joined = std::path::Path::new(base_dir).join(specifier);
// 解析为规范绝对路径:自导入身份比较要求 loader 的 path 与入口模块的
// 规范路径逐字对齐(emit 侧入口路径已 canonicalize)。
let full = std::fs::canonicalize(&joined).map_err(|e| format!("cannot read {specifier}: {e}"))?;
let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?;
Ok(ResolvedModule {
source,
path: full.to_string_lossy().into_owned(),
kind: ModuleKind::Js,
})
}
}
/// 测试结束删除临时目录,避免在 crate 树残留文件。
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// 同一 VM 续跑脚本并返回字符串完成值(读 global 探针用)。
fn probe_str(vm: &mut Vm, source: &str) -> String {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let value = vm.run(&Arc::new(module)).expect("probe run");
vm.lookup_str(value).unwrap_or_default()
}
/// 编译并运行带依赖的模块,返回其探针字符串。
fn run_namespace_module(dir: &std::path::Path) -> String {
let allocator = oxide_parser::Allocator::default();
let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main");
let program = oxide_parser::parse_module(&allocator, &source).expect("parse module");
let module = Compiler::new()
.compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut FileLoader)
.expect("compile module");
let mut vm = Vm::new();
vm.run(&Arc::new(module)).expect("module run");
probe_str(&mut vm, "globalThis.__ns")
}
#[test]
fn namespace_to_string_tag_symbol_key_and_own_keys() {
// 命名空间标签按 Symbol.toStringTag Symbol 键暴露:描述符值/属性正确,
// 字符串导出按 UTF-16 单元序排序,Symbol 段排在字符串段之后,
// 字符串枚举面不含标签。
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("dep.mjs"), "export const b = 1;\nexport const a = 2;\nexport default 3;")
.expect("write dep");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './dep.mjs';\n \
var desc = Object.getOwnPropertyDescriptor(ns, Symbol.toStringTag);\n \
globalThis.__ns = [\n \
ns[Symbol.toStringTag],\n \
typeof ns[Symbol.toStringTag],\n \
Object.prototype.toString.call(ns),\n \
Object.getOwnPropertySymbols(ns).length,\n \
Object.getOwnPropertySymbols(ns)[0] === Symbol.toStringTag,\n \
Object.getOwnPropertyNames(ns).length,\n \
Object.getOwnPropertyNames(ns).join(','),\n \
Reflect.ownKeys(ns).length,\n \
Reflect.ownKeys(ns).indexOf(Symbol.toStringTag),\n \
Symbol.toStringTag in ns,\n \
Reflect.has(ns, Symbol.toStringTag),\n \
Object.prototype.hasOwnProperty.call(ns, Symbol.toStringTag),\n \
desc.writable,\n \
desc.enumerable,\n \
desc.configurable,\n \
Object.getOwnPropertyNames(ns).indexOf('@@toStringTag'),\n \
Object.keys(ns).length,\n \
].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(
result, "Module|string|[object Module]|1|true|3|a,b,default|4|3|true|true|true|false|false|false|-1|3",
"命名空间标签 Symbol 键与自身键枚举面不符"
);
}
#[test]
fn namespace_exotic_semantics() {
// 模块命名空间 exotic 四语义:创建起不可扩展、导出 writable:true、
// [[Set]] 恒 false、[[DefineOwnProperty]] 收窄、Object.freeze 恒抛。
// 用自导入在 body 内观测扩展性(此时 body 尚未执行到 seal)。
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_exotic__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
export const x = 1;\n \
export default 2;\n \
const d = Object.getOwnPropertyDescriptor(ns, 'x');\n \
function threw(fn) { try { fn(); return false; } catch (e) { return e instanceof TypeError; } }\n \
globalThis.__ns = [\n \
Object.isExtensible(ns),\n \
d.writable === true && d.enumerable === true && d.configurable === false,\n \
Reflect.set(ns, 'x', 9),\n \
threw(function () { ns.x = 9; }),\n \
Reflect.set(ns, Symbol('s'), 1),\n \
Reflect.defineProperty(ns, 'x', {}),\n \
Reflect.defineProperty(ns, 'x', { value: 9 }),\n \
threw(function () { Object.defineProperty(ns, 'x', { value: 9 }); }),\n \
Reflect.defineProperty(ns, 'x', { writable: false }),\n \
Reflect.defineProperty(ns, 'newKey', {}),\n \
threw(function () { Object.freeze(ns); }),\n \
Object.isFrozen(ns),\n \
Object.seal(ns) === ns,\n \
ns.default,\n \
].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(
result, "false|true|false|true|false|true|false|true|false|false|true|false|true|2",
"命名空间 exotic 语义不符"
);
}
/// live 自导入 ns 的条目语义:预注册后初始化前读抛 ReferenceError,`in` /
/// `Reflect.has` 已为 true;初始化后读到活值;枚举未初始化名不抛错且键数正确。
/// 循环读 `ns.y` 同时钉住 IC 慢路径与普通路径读结果一致。
#[test]
fn namespace_self_import_live_entries() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_live__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
const init = [];\n \
for (const k of ['x', 'y', 'default']) {\n \
try { ns[k]; init.push('value'); } catch (e) { init.push(e instanceof ReferenceError); }\n \
}\n \
const hasBefore = ['x', 'y', 'default'].map(k => k in ns).join(',');\n \
const namesBefore = Object.getOwnPropertyNames(ns).join(',');\n \
const ownKeysCount = Reflect.ownKeys(ns).length;\n \
export let x = 'sx';\n \
export var y = 2;\n \
export default 3;\n \
let sum = 0;\n \
for (let i = 0; i < 128; i++) { sum += ns.y; }\n \
globalThis.__ns = [\n \
init.join('|'),\n \
hasBefore,\n \
namesBefore,\n \
ownKeysCount,\n \
ns.x,\n \
sum,\n \
ns.default,\n \
].join(';');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(
result, "true|true|true;true,true,true;default,x,y;4;sx;256;3",
"live 自导入 ns 的未初始化读 / 键存在性 / 活值读不符"
);
}
/// 未初始化导出在枚举面与描述符面经 `? [[GetOwnProperty]]` 抛 ReferenceError;
/// `[[OwnPropertyKeys]]` 面(getOwnPropertyNames / Reflect.ownKeys)不读值故不抛。
#[test]
fn namespace_uninitialized_enumeration_throws_reference_error() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_uninit_enum__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
const threw = fn => { try { fn(); return false; } catch (e) { return e instanceof ReferenceError; } };\n \
const names = Object.getOwnPropertyNames(ns).join(',');\n \
const ownKeysCount = Reflect.ownKeys(ns).length;\n \
globalThis.__ns = [\n \
threw(() => Object.keys(ns)),\n \
threw(() => Object.values(ns)),\n \
threw(() => Object.entries(ns)),\n \
threw(() => Object.assign({}, ns)),\n \
threw(() => Object.prototype.hasOwnProperty.call(ns, 'default')),\n \
threw(() => Object.getOwnPropertyDescriptor(ns, 'default')),\n \
threw(() => Object.prototype.propertyIsEnumerable.call(ns, 'default')),\n \
threw(() => { for (const k in ns) {} }),\n \
names,\n \
ownKeysCount,\n \
].join('|');\n \
export default 0;",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(
result, "true|true|true|true|true|true|true|true|default|2",
"未初始化导出的枚举/描述符抛错或 ownKeys 反向守卫不符"
);
}
/// 顶层对导出源绑定的重赋写穿命名空间:一个源绑定背两个导出名时同值更新,
/// 复合赋值路径与简单赋值路径一致。
#[test]
fn namespace_assignment_write_through_multiple_export_names() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_write_through__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
export let a = 1;\n \
export { a as b };\n \
export let c = 1;\n \
a = 2;\n \
c += 2;\n \
globalThis.__ns = [ns.a, ns.b, ns.c].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(result, "2|2|3", "导出源绑定重赋未写穿命名空间活值");
}
/// 写穿以解析后的绑定身份为准:块级/catch 同名遮蔽的赋值不得污染模块导出。
/// 遮蔽名解析到内层绑定槽,与导出源绑定槽不同,写穿须按槽位过滤而非仅按名字。
#[test]
fn namespace_write_through_respects_block_shadowing() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_shadow__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
export let x = 1;\n \
{ let x = 2; x = 3; }\n \
try { throw 0; } catch (x) { x = 4; }\n \
globalThis.__ns = [ns.x, x].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(result, "1|1", "块级/catch 同名遮蔽的赋值不得写穿命名空间条目");
}
/// 写穿覆盖 Update 表达式、逻辑赋值与解构赋值三条发射路径,与简单/二元复合
/// 赋值一致地把新值同步到命名空间条目。
#[test]
fn namespace_write_through_covers_update_logical_destructuring() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_write_paths__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './main.mjs';\n \
export let a = 1;\n \
export let b = 0;\n \
export let c = 1;\n \
a++;\n \
b ||= 7;\n \
[c] = [9];\n \
globalThis.__ns = [ns.a, ns.b, ns.c].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(result, "2|7|9", "Update/逻辑/解构赋值的写穿缺失");
}
/// G2 反向守卫:外部导入的命名空间无条目表(非 live),枚举与 for-in 行为不变。
#[test]
fn external_namespace_enumeration_stays_unaffected() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_namespace_external__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("dep.mjs"), "export const z = 1;").expect("write dep");
std::fs::write(
dir.join("main.mjs"),
"import * as ns from './dep.mjs';\n \
let n = 0;\n \
for (const k in ns) n++;\n \
globalThis.__ns = [Object.keys(ns).join(','), n, Object.values(ns).join(',')].join('|');",
)
.expect("write main");
let _cleanup = Cleanup(dir.clone());
let result = run_namespace_module(&dir);
assert_eq!(result, "z|1|1", "非 live 外部命名空间枚举行为不应变化");
}