修复动态模块赋值根与直接成员需求
将导出 live binding、对象成员与 prototype 安装的 callable 纳入精确投影可达根 记录直接 await import namespace 成员的 occurrence 级 export demand 补充静态实例隔离、恒假边界与传递动态依赖回归 同步 AI 游戏创作技术方案与共享决策
This commit is contained in:
+376
-6
@@ -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);";
|
||||
|
||||
+60
@@ -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 root,class 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
Reference in New Issue
Block a user