fix: 模块顶层路径归一绝对口径,自导入不再误判为外部依赖

This commit is contained in:
2026-09-14 20:39:58 +00:00
parent fa67988ab4
commit 647fb31af7
2 changed files with 87 additions and 1 deletions
+10 -1
View File
@@ -128,11 +128,20 @@ fn is_hoisted_function_decl(stmt: &Statement) -> bool {
impl Emitter {
/// 编译 ES module:顶层 body + 递归依赖。
/// `module_path` 为模块文件的规范路径(依赖解析基准 = 其父目录)。
/// `module_path` 为模块文件路径;入口即归一为规范绝对路径(依赖解析基准 =
/// 其父目录),与加载器 resolve 的绝对规范口径对齐,使自导入身份比较稳定。
pub fn emit_program_module(
&self, program: &oxide_parser::Program, module_path: &str, loader: &mut dyn ModuleSourceLoader,
) -> Result<IRFunction, String> {
crate::emit_debug!("emit_program_module: {} stmts", program.body.len());
// 顶层模块路径归一为规范绝对路径:自导入身份比较(resolved.path ==
// module_path)与 base_dir 需与加载器 resolve 的 canonicalize 绝对口径
// 一致——若保留相对发现路径,自导入比较恒 false、自导入被当外部依赖
// 重编译。文件不存在(虚拟/内存模块)时 canonicalize 失败,保留原路径。
let canonical = std::fs::canonicalize(module_path)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| module_path.to_string());
let module_path = canonical.as_str();
let mut ctx = CompileCtx::new();
// 模块顶层 var/function 不写全局对象(模块作用域绑定)。
ctx.is_global_scope = false;
@@ -0,0 +1,77 @@
//! 自导入路径口径:顶层模块相对发现路径 vs 加载器 resolve 的 canonicalize 绝对
//! 路径,二者须同口径,自导入才按自身命名空间绑定、整模块只求值一次。
//! 误判为外部依赖会把自导入重新编译求值一遍,模块被求值两次。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule};
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
/// 自导入模块源:`import * as self from './selfm.js'` 导入自身,每次求值给
/// global 计数加一,导出 self 命名空间引用以实化自导入绑定。
const MODULE_SRC: &str = "import * as self from './selfm.js';\n \
globalThis.__evals = (globalThis.__evals || 0) + 1;\n \
export const self_ref = self;";
/// 复刻 test262 加载器:base_dir/specifier join 后 canonicalize 为绝对规范路径
/// 并读源——与顶层相对发现路径构成「绝对 vs 相对」口径差,正是本面复现条件。
struct AbsLoader;
impl ModuleSourceLoader for AbsLoader {
fn resolve(
&mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)],
) -> Result<ResolvedModule, String> {
let full = std::path::Path::new(base_dir).join(specifier);
let canonical = full.canonicalize().map_err(|e| format!("cannot resolve {specifier}: {e}"))?;
let source = std::fs::read_to_string(&canonical).map_err(|e| format!("cannot read {specifier}: {e}"))?;
Ok(ResolvedModule {
source,
path: canonical.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(vm: &mut Vm, source: &str) -> JsValue {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
vm.run(&Arc::new(module)).expect("probe run")
}
#[test]
fn self_import_module_evaluated_once() {
// 临时模块落在 CWD 相对路径下,使顶层发现路径可为相对形式、canonicalize
// 又可达——与 runner「相对发现路径 + 绝对 resolve」的口径差同构。
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__selfimport_repro__");
let file = dir.join("selfm.js");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(&file, MODULE_SRC).expect("write module");
let _cleanup = Cleanup(dir.clone());
let allocator = oxide_parser::Allocator::default();
let source = std::fs::read_to_string(&file).expect("read module");
let program = oxide_parser::parse_module(&allocator, &source).expect("parse module");
// 顶层模块路径用相对发现路径(不 canonicalize),复刻 runner 侧口径。
let module = Compiler::new()
.compile_module(&program, "__selfimport_repro__/selfm.js", &mut AbsLoader)
.expect("compile module");
let mut vm = Vm::new();
vm.run(&Arc::new(module)).expect("module run");
// 自导入须绑定自身命名空间,整模块只求值一次;误判为外部依赖会二次求值。
let r = probe(&mut vm, "globalThis.__evals");
assert!(r.is_int() && r.as_int() == 1, "自导入模块应只求值一次,实际 __evals={r:?}");
}