修复ESM解构投影与模块完成性
按源码顺序投影副作用依赖声明并按声明身份去重重命名 支持对象和数组解构写入导出绑定及最终可调用根传播 按可达绑定使用建立动态导入解构需求 扩展顶层 Promise await 的静态不完成识别与回归 同步技术方案与共享决策记录
This commit is contained in:
+454
-56
File diff suppressed because it is too large
Load Diff
+207
@@ -3951,6 +3951,139 @@ fn javascript_side_effect_imports_precede_importers_without_linking_local_decoys
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_side_effect_projection_keeps_required_local_declarations_in_source_order() {
|
||||
let temporary = tempfile::tempdir().expect("create ordered side-effect dependency project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/setup.mjs"),
|
||||
"const makeRenderer = () => () => 1; globalThis.renderFrame = makeRenderer(); const unrelated = 'decoy';",
|
||||
)
|
||||
.expect("write side-effect dependency with a local helper");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import './setup.mjs'; window.renderFrame();",
|
||||
)
|
||||
.expect("write side-effect importer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("project the side effect together with its local dependency");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("window.renderFrame()") && unit.contains("makeRenderer"))
|
||||
.expect("the ordered side-effect projection must join the importer");
|
||||
let helper = projected
|
||||
.find("const makeRenderer")
|
||||
.expect("the side-effect projection must include its local declaration");
|
||||
let side_effect = projected
|
||||
.find("globalThis.renderFrame = makeRenderer()")
|
||||
.expect("the side-effect projection must include the observable write");
|
||||
assert!(
|
||||
helper < side_effect,
|
||||
"a required local declaration must remain before the side effect that reads it: {projected}",
|
||||
);
|
||||
assert!(
|
||||
!projected.contains("const unrelated"),
|
||||
"an unrelated dependency-local declaration must not leak into the projection: {projected}",
|
||||
);
|
||||
|
||||
fs::write(
|
||||
root.join("game/setup.mjs"),
|
||||
"export const makeRenderer = () => () => 1; globalThis.renderFrame = makeRenderer();",
|
||||
)
|
||||
.expect("write a side-effect helper that is also imported");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { makeRenderer } from './setup.mjs'; window.renderFrame(); makeRenderer()();",
|
||||
)
|
||||
.expect("write an importer that also reads the side-effect helper");
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("deduplicate a declaration shared by side-effect and binding projection");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| {
|
||||
unit.contains("globalThis.renderFrame = makeRenderer()")
|
||||
&& unit.contains("window.renderFrame()")
|
||||
&& unit.contains("makeRenderer()()")
|
||||
})
|
||||
.expect("the shared helper projection must join its importer");
|
||||
assert_eq!(
|
||||
projected.matches("const makeRenderer").count(),
|
||||
1,
|
||||
"a helper shared by side-effect and binding demand must be declared once: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_destructuring_assignments_project_the_final_exported_callable() {
|
||||
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
|
||||
for (label, assignments) in [
|
||||
(
|
||||
"object",
|
||||
"({ start } = { start: () => import('./old.mjs').then(({ oldRun }) => oldRun()) }); ({ start } = { start: () => import('./dependency.mjs').then(({ run }) => run()) });",
|
||||
),
|
||||
(
|
||||
"array",
|
||||
"[start] = [() => import('./old.mjs').then(({ oldRun }) => oldRun())]; [start] = [() => import('./dependency.mjs').then(({ run }) => run())];",
|
||||
),
|
||||
] {
|
||||
let temporary = tempfile::tempdir().expect("create destructuring live-binding project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
format!("export let start; {assignments}"),
|
||||
)
|
||||
.expect("write destructuring live-binding assignments");
|
||||
fs::write(
|
||||
root.join("game/old.mjs"),
|
||||
"export function oldRun() { return import('./missing-old.mjs'); }",
|
||||
)
|
||||
.expect("write obsolete destructured callable");
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"export function run() { return import('./final.mjs').then(({ finish }) => finish()); }",
|
||||
)
|
||||
.expect("write final destructured callable dependency");
|
||||
fs::write(
|
||||
root.join("game/final.mjs"),
|
||||
"export function finish() { return 'done'; }",
|
||||
)
|
||||
.expect("write final destructured callable closure");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './origin.mjs'; start();",
|
||||
)
|
||||
.expect("write destructured callable consumer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(root, html)
|
||||
.unwrap_or_else(|error| panic!("project {label} destructuring assignment: {error}"));
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("start();") && unit.contains("function finish()"))
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{label} destructuring assignment must project its final callable closure: {:#?}",
|
||||
modules.module_units()
|
||||
)
|
||||
});
|
||||
assert!(
|
||||
!projected.contains("function oldRun()"),
|
||||
"only the final destructuring assignment may define the exported callable root: {projected}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_projection_preserves_dependency_declaration_source_order() {
|
||||
let temporary = tempfile::tempdir().expect("create declaration-order project");
|
||||
@@ -4087,6 +4220,80 @@ fn javascript_top_level_await_fails_closed_only_when_statically_non_completing()
|
||||
.expect("write completing top-level await with nested decoy");
|
||||
read_external_gameplay_javascript_at(root, html)
|
||||
.expect("a completing top-level await and uncalled nested decoy remain valid");
|
||||
|
||||
for source in [
|
||||
"await new Promise(() => 42); export function start() { return 1; }",
|
||||
"await new Promise((resolve) => console.log(resolve.name)); export function start() { return 1; }",
|
||||
"await new Promise((resolve) => { function nested(resolve) { resolve(); } }); export function start() { return 1; }",
|
||||
] {
|
||||
fs::write(root.join("game/dependency.mjs"), source)
|
||||
.expect("write a non-settling Promise executor");
|
||||
let error = read_external_gameplay_javascript_at(root, html)
|
||||
.expect_err("an executor that never invokes its resolver must not complete");
|
||||
assert!(
|
||||
error.contains("top-level await") || error.contains("顶层 await"),
|
||||
"{error}",
|
||||
);
|
||||
}
|
||||
|
||||
for source in [
|
||||
"await new Promise((resolve) => resolve(1)); export function start() { return 1; }",
|
||||
"await new Promise((_resolve, reject) => reject(new Error('done'))).catch(() => {}); export function start() { return 1; }",
|
||||
"const Promise = class { constructor() {} }; await new Promise(() => 42); export function start() { return 1; }",
|
||||
"async function nested() { await new Promise(() => 42); } await Promise.resolve(); export function start() { return 1; }",
|
||||
] {
|
||||
fs::write(root.join("game/dependency.mjs"), source)
|
||||
.expect("write a completing or out-of-scope Promise case");
|
||||
read_external_gameplay_javascript_at(root, html)
|
||||
.expect("resolver calls, Promise shadowing and nested awaits must remain valid");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_dynamic_then_destructuring_requires_a_reachable_binding_use() {
|
||||
let temporary = tempfile::tempdir().expect("create dynamic then demand project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"export function run() { return import('./missing-unused.mjs'); }",
|
||||
)
|
||||
.expect("write demand-sensitive dynamic export");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
"export function start() { return import('./dependency.mjs').then(({ run }) => { if (false) run(); }); }",
|
||||
)
|
||||
.expect("write unused dynamic destructuring callback");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './origin.mjs'; start();",
|
||||
)
|
||||
.expect("write dynamic then consumer");
|
||||
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
|
||||
read_external_gameplay_javascript_at(root, html)
|
||||
.expect("pure destructuring and unreachable uses must not demand the export callable");
|
||||
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"export function run() { return import('./final.mjs').then(({ finish }) => finish()); }",
|
||||
)
|
||||
.expect("write reachable dynamic export");
|
||||
fs::write(
|
||||
root.join("game/final.mjs"),
|
||||
"export function finish() { return 'used'; }",
|
||||
)
|
||||
.expect("write reachable dynamic export closure");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
"export function start() { return import('./dependency.mjs').then(({ run }) => run()); }",
|
||||
)
|
||||
.expect("write used dynamic destructuring callback");
|
||||
let modules = read_external_gameplay_javascript_at(root, html)
|
||||
.expect("a reachable destructured binding call must demand its export closure");
|
||||
assert!(modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.any(|unit| unit.contains("start();") && unit.contains("function finish()")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6010,3 +6010,10 @@
|
||||
|
||||
- ESM 最终绑定与 occurrence 补充:外部 callable root 只认模块初始化完成时同一 live binding / member 的最后一次直接顶层 assignment,旧 RHS 不得加载;直接 awaited namespace member 在赋值、受控 callback、constructor 和深层静态 member path 中仍按 `(source, occurrence)` 传播并替换首段 export。assignment class expression 的 static / instance member demand 分离。
|
||||
- ESM 初始化与阻塞补充:projection declaration 保持原模块源码顺序,dependency origin 保持 importer 声明顺序;循环 canonical 去重同时删除声明和对应初始化写入。side-effect static import 只联结按 dependency-before-importer 排列的直接 `globalThis / window` 顶层 effect,不暴露 dependency local binding;静态可判定永不完成的 top-level await 至少对未遮蔽全局 `await new Promise(() => {})` 失败关闭,嵌套函数同形 decoy 和可完成 await 保持允许。
|
||||
|
||||
## 2026-08-04 ESM 解构写入与模块完成性收口
|
||||
|
||||
- side-effect static import 若读取 dependency-local declaration,组合投影必须把该声明及其 semantic 依赖按原源码位置放在 effect 前;同一声明也被 importer 使用时按 origin declaration identity 去重并统一做 collision-safe canonical 重命名,既不泄露无关 local,也不引入 TDZ 或重复声明。
|
||||
- exported live binding 的顶层 object / array destructuring assignment 纳入 projection;最终一次写入中与目标 root 对应的属性或槽位 callable 才作为 dynamic demand root,更早写入和相邻 decoy 不得回流。
|
||||
- 未遮蔽全局 top-level `await new Promise(executor)` 的 executor 若不调用或传递 resolve / reject、也不显式 throw,则返回值不参与 Promise settle,静态门按不完成失败关闭;resolver 按 semantic symbol 识别,Promise 遮蔽与嵌套函数内 await decoy 继续保留。
|
||||
- dynamic import `.then` callback 的对象解构只建立 occurrence-scoped binding,不再仅因读取 export 属性就形成 callable demand;只有该 binding 的可达引用或调用才向 export 内部传播动态依赖,未使用和恒假引用保持关闭。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user