修复动态模块赋值根与直接成员需求

将导出 live binding、对象成员与 prototype 安装的 callable 纳入精确投影可达根

记录直接 await import namespace 成员的 occurrence 级 export demand

补充静态实例隔离、恒假边界与传递动态依赖回归

同步 AI 游戏创作技术方案与共享决策
This commit is contained in:
2026-08-04 14:46:11 +08:00
parent ac002098f7
commit d4f5d9ce6d
4 changed files with 439 additions and 8 deletions
@@ -6048,8 +6048,12 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> {
fn javascript_string_import_source(
expression: &JavascriptExpression<'_>,
) -> Option<(String, usize)> {
let JavascriptExpression::ImportExpression(import) = expression else {
return None;
let import = match expression {
JavascriptExpression::ImportExpression(import) => import,
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
return javascript_string_import_source(&parenthesized.expression);
}
_ => return None,
};
let JavascriptExpression::StringLiteral(source) = &import.source else {
return None;
@@ -6057,6 +6061,20 @@ fn javascript_string_import_source(
Some((source.value.to_string(), import.span.start as usize))
}
fn javascript_awaited_import_source(
expression: &JavascriptExpression<'_>,
) -> Option<(String, usize)> {
match expression {
JavascriptExpression::AwaitExpression(awaited) => {
javascript_string_import_source(&awaited.argument)
}
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
javascript_awaited_import_source(&parenthesized.expression)
}
_ => None,
}
}
impl JavascriptDynamicImportUsageCollector<'_> {
fn record_namespace_source(
&mut self,
@@ -6130,6 +6148,12 @@ impl JavascriptDynamicImportUsageCollector<'_> {
.collect();
}
expression => {
if let Some((source, import_position)) =
javascript_awaited_import_source(expression)
{
path.reverse();
return vec![(source, import_position, path)];
}
let Some(next) = expression.as_member_expression() else {
return Vec::new();
};
@@ -6998,6 +7022,206 @@ fn javascript_projected_dynamic_import_position_is_reachable(
const JAVASCRIPT_STATIC_MEMBER_DEMAND: &str = "\0agc-static-member";
const JAVASCRIPT_INSTANCE_MEMBER_DEMAND: &str = "\0agc-instance-member";
fn javascript_expression_root_symbol_and_member_path(
expression: &JavascriptExpression<'_>,
scoping: &JavascriptScoping,
) -> Option<(JavascriptSymbolId, Vec<String>)> {
match expression {
JavascriptExpression::Identifier(identifier) => identifier
.reference_id
.get()
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
.map(|symbol_id| (symbol_id, Vec::new())),
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
javascript_expression_root_symbol_and_member_path(&parenthesized.expression, scoping)
}
_ => {
let member = expression.as_member_expression()?;
let property = member.static_property_name()?.to_string();
let (symbol_id, mut path) =
javascript_expression_root_symbol_and_member_path(member.object(), scoping)?;
path.push(property);
Some((symbol_id, path))
}
}
}
fn javascript_assignment_target_root_symbol_and_member_path(
target: &oxc_ast::ast::AssignmentTarget<'_>,
scoping: &JavascriptScoping,
) -> Option<(JavascriptSymbolId, Vec<String>)> {
match target {
oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => identifier
.reference_id
.get()
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
.map(|symbol_id| (symbol_id, Vec::new())),
_ => {
let member = target.as_member_expression()?;
let property = member.static_property_name()?.to_string();
let (symbol_id, mut path) =
javascript_expression_root_symbol_and_member_path(member.object(), scoping)?;
path.push(property);
Some((symbol_id, path))
}
}
}
fn javascript_callable_expression_function_indices(
expression: &JavascriptExpression<'_>,
ranges: &[NamedJavascriptFunctionRange],
scoping: &JavascriptScoping,
) -> BTreeSet<usize> {
match expression {
JavascriptExpression::FunctionExpression(_)
| JavascriptExpression::ArrowFunctionExpression(_) => {
let span = expression.span();
ranges
.iter()
.enumerate()
.filter(|(_, range)| {
range.start == span.start as usize && range.end == span.end as usize
})
.map(|(index, _)| index)
.collect()
}
JavascriptExpression::Identifier(identifier) => identifier
.reference_id
.get()
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
.map(|symbol_id| scoping.symbol_span(symbol_id).start as usize)
.into_iter()
.flat_map(|binding_start| {
ranges
.iter()
.enumerate()
.filter(move |(_, range)| range.binding_start == Some(binding_start))
.map(|(index, _)| index)
})
.collect(),
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
javascript_callable_expression_function_indices(
&parenthesized.expression,
ranges,
scoping,
)
}
JavascriptExpression::ConditionalExpression(conditional) => {
let mut indices = javascript_callable_expression_function_indices(
&conditional.consequent,
ranges,
scoping,
);
indices.extend(javascript_callable_expression_function_indices(
&conditional.alternate,
ranges,
scoping,
));
indices
}
_ => BTreeSet::new(),
}
}
fn javascript_projected_assignment_root_function_indices(
content: &str,
ranges: &[NamedJavascriptFunctionRange],
root_binding: &str,
required_static: Option<bool>,
member_path: &[String],
) -> BTreeSet<usize> {
let allocator = JavascriptAllocator::default();
let parsed = JavascriptParser::new(&allocator, content, javascript_source_type(true)).parse();
if parsed.panicked || !parsed.diagnostics.is_empty() {
return BTreeSet::new();
}
let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program);
if !semantic.diagnostics.is_empty() {
return BTreeSet::new();
}
let scoping = semantic.semantic.scoping();
let Some(root_symbol) = scoping
.get_bindings(scoping.root_scope_id())
.get(root_binding)
.copied()
else {
return BTreeSet::new();
};
let method_ranges = javascript_member_method_ranges(content);
let mut roots = BTreeSet::new();
for statement in &parsed.program.body {
let JavascriptStatement::ExpressionStatement(statement) = statement else {
continue;
};
let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else {
continue;
};
if !assignment.operator.is_assign() {
continue;
}
let Some((target_symbol, mut target_path)) =
javascript_assignment_target_root_symbol_and_member_path(&assignment.left, scoping)
else {
continue;
};
if target_symbol != root_symbol {
continue;
}
match required_static {
Some(false) => {
if target_path.first().map(String::as_str) != Some("prototype") {
continue;
}
target_path.remove(0);
}
Some(true) if target_path.first().map(String::as_str) == Some("prototype") => {
continue;
}
_ => {}
}
if target_path == member_path {
roots.extend(javascript_callable_expression_function_indices(
&assignment.right,
ranges,
scoping,
));
continue;
}
let Some(suffix) = member_path.strip_prefix(target_path.as_slice()) else {
continue;
};
if suffix.is_empty() {
continue;
}
let assignment_span = assignment.right.span();
roots.extend(
method_ranges
.get(suffix)
.into_iter()
.flat_map(|methods| methods.iter())
.filter(|method| {
assignment_span.start as usize <= method.range.start
&& method.range.end <= assignment_span.end as usize
&& required_static.is_none_or(|required| {
method
.is_static
.is_none_or(|is_static| is_static == required)
})
})
.flat_map(|method| {
ranges
.iter()
.enumerate()
.filter(move |(_, range)| {
method.range.start == range.start && method.range.end == range.end
})
.map(|(index, _)| index)
}),
);
}
roots
}
fn javascript_projected_root_function_indices(
content: &str,
ranges: &[NamedJavascriptFunctionRange],
@@ -7016,7 +7240,7 @@ fn javascript_projected_root_function_indices(
_ => (None, member_path),
};
let methods = javascript_member_method_ranges(content);
return methods
let mut roots = methods
.get(actual_member_path)
.into_iter()
.flat_map(|method_ranges| method_ranges.iter())
@@ -7038,7 +7262,15 @@ fn javascript_projected_root_function_indices(
})
.map(|(index, _)| index)
})
.collect();
.collect::<BTreeSet<_>>();
roots.extend(javascript_projected_assignment_root_function_indices(
content,
ranges,
root_binding,
required_static,
actual_member_path,
));
return roots;
}
let top_level = ranges
.iter()
@@ -7058,11 +7290,19 @@ fn javascript_projected_root_function_indices(
})
.map(|(index, _)| index)
.collect::<Vec<_>>();
top_level
let mut roots = top_level
.iter()
.copied()
.filter(|index| ranges[*index].name == root_binding)
.collect()
.collect::<BTreeSet<_>>();
roots.extend(javascript_projected_assignment_root_function_indices(
content,
ranges,
root_binding,
None,
&[],
));
roots
}
#[derive(Default)]
@@ -10968,6 +11208,136 @@ mod javascript_projection_reachability_tests {
);
}
#[test]
fn projected_export_assignment_callables_are_reachability_roots() {
for (content, root, member_path, source) in [
(
"export let start; start = () => import('./assigned-root.mjs');",
"start",
Vec::new(),
"./assigned-root.mjs",
),
(
"export const api = {}; api.run = function() { return import('./assigned-member.mjs'); };",
"api",
vec![
JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(),
"run".to_string(),
],
"./assigned-member.mjs",
),
(
"export class Game {} Game.prototype.run = () => import('./assigned-prototype.mjs');",
"Game",
vec![
JAVASCRIPT_INSTANCE_MEMBER_DEMAND.to_string(),
"run".to_string(),
],
"./assigned-prototype.mjs",
),
(
"function implementation() { return import('./assigned-function-alias.mjs'); } export let start; start = implementation;",
"start",
Vec::new(),
"./assigned-function-alias.mjs",
),
(
"const implementation = () => import('./assigned-member-alias.mjs'); export const api = {}; api.run = implementation;",
"api",
vec![
JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(),
"run".to_string(),
],
"./assigned-member-alias.mjs",
),
] {
let ranges = named_javascript_function_ranges(content);
let roots = javascript_projected_root_function_indices(
content,
&ranges,
root,
&member_path,
);
let position = content.find(source).expect("find assigned dynamic import");
assert!(
javascript_projected_dynamic_import_position_is_reachable(
content, &ranges, &roots, position,
),
"a projected exported assignment must execute its selected callable: {content}",
);
}
let class_members = "export class Game {} Game.run = () => import('./assigned-static.mjs'); Game.prototype.run = () => import('./assigned-instance.mjs');";
let ranges = named_javascript_function_ranges(class_members);
let static_roots = javascript_projected_root_function_indices(
class_members,
&ranges,
"Game",
&[
JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(),
"run".to_string(),
],
);
let instance_roots = javascript_projected_root_function_indices(
class_members,
&ranges,
"Game",
&[
JAVASCRIPT_INSTANCE_MEMBER_DEMAND.to_string(),
"run".to_string(),
],
);
let static_import = class_members
.find("./assigned-static.mjs")
.expect("find static assignment import");
let instance_import = class_members
.find("./assigned-instance.mjs")
.expect("find instance assignment import");
assert!(javascript_projected_dynamic_import_position_is_reachable(
class_members,
&ranges,
&static_roots,
static_import,
));
assert!(!javascript_projected_dynamic_import_position_is_reachable(
class_members,
&ranges,
&static_roots,
instance_import,
));
assert!(javascript_projected_dynamic_import_position_is_reachable(
class_members,
&ranges,
&instance_roots,
instance_import,
));
assert!(!javascript_projected_dynamic_import_position_is_reachable(
class_members,
&ranges,
&instance_roots,
static_import,
));
}
#[test]
fn awaited_dynamic_namespace_member_records_its_export_demand() {
let content =
"export async function start() { return (await import('./dependency.mjs')).run(); }";
let analysis = javascript_module_analysis(content, true).expect("analyze awaited import");
let demand = analysis
.dynamic_import_demands
.iter()
.find(|((source, _), _)| source == "./dependency.mjs")
.map(|(_, demands)| demands);
assert!(
demand
.is_some_and(|demands| { demands.contains_key(&("run".to_string(), Vec::new())) }),
"a direct awaited namespace call must demand dependency::run: {:#?}",
analysis.dynamic_import_demands,
);
}
#[test]
fn imported_custom_map_does_not_execute_a_local_callback() {
let content = "import { scheduler } from './scheduler.mjs'; function decoy() { return import('./missing-decoy.mjs'); } scheduler.map(decoy);";
@@ -3722,6 +3722,66 @@ fn javascript_dynamic_projection_keeps_occurrence_member_and_constructor_identit
.any(|unit| unit.contains("function finishLocalAlias()")));
}
#[test]
fn javascript_dynamic_projection_reaches_export_assignments_and_direct_await_members() {
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
for (label, origin, importer, dependency, final_source, final_binding) in [
(
"exported live binding",
"export let start; start = () => import('./dependency.mjs').then(({ run }) => run());",
"import { start } from './origin.mjs'; start();",
"export function run() { return import('./final.mjs').then(({ finish }) => finish()); } export function poison() { return import('./missing-poison.mjs'); }",
"export function finish() { return 'live-binding'; }",
"finish",
),
(
"exported object member installation",
"export const api = {}; api.start = function() { return import('./dependency.mjs').then(({ runMember }) => runMember()); };",
"import { api } from './origin.mjs'; api.start();",
"export function runMember() { return import('./final.mjs').then(({ finishMember }) => finishMember()); } export function poison() { return import('./missing-poison.mjs'); }",
"export function finishMember() { return 'member'; }",
"finishMember",
),
(
"exported prototype member installation",
"export class Game {} Game.prototype.start = () => import('./dependency.mjs').then(({ runPrototype }) => runPrototype());",
"import { Game } from './origin.mjs'; new Game().start();",
"export function runPrototype() { return import('./final.mjs').then(({ finishPrototype }) => finishPrototype()); } export function poison() { return import('./missing-poison.mjs'); }",
"export function finishPrototype() { return 'prototype'; }",
"finishPrototype",
),
(
"direct awaited namespace member",
"export async function start() { if (false) (await import('./missing-false.mjs')).run(); return (await import('./dependency.mjs')).run(); }",
"import { start } from './origin.mjs'; start();",
"export function run() { return import('./final.mjs').then(({ finishAwaited }) => finishAwaited()); } export function poison() { return import('./missing-poison.mjs'); }",
"export function finishAwaited() { return 'awaited'; }",
"finishAwaited",
),
] {
let temporary = tempfile::tempdir().expect("create dynamic 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"), origin).expect("write projected origin");
fs::write(root.join("game/main.mjs"), importer).expect("write projected importer");
fs::write(root.join("game/dependency.mjs"), dependency)
.expect("write selected dynamic dependency");
fs::write(root.join("game/final.mjs"), final_source)
.expect("write selected transitive dependency");
let modules = read_external_gameplay_javascript_at(root, html)
.unwrap_or_else(|error| panic!("project {label}: {error}"));
assert!(
modules
.module_units()
.iter()
.any(|unit| unit.contains(&format!("function {final_binding}()"))),
"{label} must demand the selected dynamic export and its closure: {:#?}",
modules.module_units(),
);
}
}
#[test]
fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
let temporary = tempfile::tempdir().expect("create projection identity project");
@@ -5994,6 +5994,7 @@
- JavaScript callable 分支与参数快照补充:conditional expression 必须在 test 求值完成后,分别于 consequent / alternate 自身起点冻结 callable identity;受控 callback 参数按该参数自身起点解析,前置参数产生的 alias 副作用先于后续 callback identity 生效,callback 的执行边仍保留在注册调用完成位置。
- JavaScript / ESM live binding 投影补充:被选 export root 的直接顶层 assignment 及其 RHS 依赖必须与原声明共同投影,覆盖 `export let x; x = impl`、导出对象成员安装和 class prototype 安装;assignment target 以 semantic root symbol 归属,函数体写入、嵌套控制流和无关 root 写入不得因同名文本进入投影。共享 declaration 的写入按原始源码位置合并,继续参与 canonical 重命名、循环去重和既有 `2 MiB / 32 MiB` 门禁;全部依赖声明必须先于延后的初始化写入输出,不能因 projection traversal 产生 TDZ。
- JavaScript callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只建立可达调用边,闭包读取的外层 alias 状态选取注册所在同步作用域收尾点,不能冻结在注册点;函数体写副作用仍不得同步提交到注册调用末尾。数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 按函数对象身份形成成员 callable 状态,普通函数别名共享同一对象覆盖,`.bind()` 结果保持独立对象身份;存在覆盖时禁止回退到 Function.prototype 语义。`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。
- JavaScript / ESM assignment root 与直接动态 namespace 补充:被选 export 的顶层 live-binding function / arrow assignment、对象成员安装和 class prototype 安装同时成为对应 root / member 的 projected reachability rootclass static 与 instance assignment 不得串线;`(await import('./dep.mjs')).run()` 及等价静态 computed member 直接记录 occurrence-scoped `run` export demand,恒假分支、未调用函数与其它既有可达性边界继续生效。
## 2026-08-04 静态视觉门脚本与 ESM 求值顺序
File diff suppressed because one or more lines are too long