修复ESM导出绑定初始化投影
将导出根的直接顶层赋值与原声明共同投影。 按语义根符号纳入对象成员与原型安装的RHS依赖。 新增live binding回归并同步技术方案与决策记录。
This commit is contained in:
+187
-23
@@ -6944,6 +6944,107 @@ fn javascript_top_level_declarations(content: &str) -> std::collections::BTreeMa
|
||||
declarations
|
||||
}
|
||||
|
||||
fn javascript_expression_root_symbol(
|
||||
expression: &JavascriptExpression<'_>,
|
||||
scoping: &JavascriptScoping,
|
||||
) -> Option<JavascriptSymbolId> {
|
||||
match expression {
|
||||
JavascriptExpression::Identifier(identifier) => identifier
|
||||
.reference_id
|
||||
.get()
|
||||
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()),
|
||||
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
|
||||
javascript_expression_root_symbol(&parenthesized.expression, scoping)
|
||||
}
|
||||
_ => expression
|
||||
.as_member_expression()
|
||||
.and_then(|member| javascript_expression_root_symbol(member.object(), scoping)),
|
||||
}
|
||||
}
|
||||
|
||||
fn javascript_assignment_target_root_symbol(
|
||||
target: &oxc_ast::ast::AssignmentTarget<'_>,
|
||||
scoping: &JavascriptScoping,
|
||||
) -> Option<JavascriptSymbolId> {
|
||||
match target {
|
||||
oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => identifier
|
||||
.reference_id
|
||||
.get()
|
||||
.and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()),
|
||||
_ => target
|
||||
.as_member_expression()
|
||||
.and_then(|member| javascript_expression_root_symbol(member.object(), scoping)),
|
||||
}
|
||||
}
|
||||
|
||||
fn javascript_top_level_assignment_writes(content: &str) -> BTreeMap<String, Vec<(usize, String)>> {
|
||||
let allocator = JavascriptAllocator::default();
|
||||
let parsed = JavascriptParser::new(&allocator, content, javascript_source_type(true)).parse();
|
||||
if parsed.panicked || !parsed.diagnostics.is_empty() {
|
||||
return BTreeMap::new();
|
||||
}
|
||||
let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program);
|
||||
if !semantic.diagnostics.is_empty() {
|
||||
return BTreeMap::new();
|
||||
}
|
||||
let scoping = semantic.semantic.scoping();
|
||||
let root_names = scoping
|
||||
.get_bindings(scoping.root_scope_id())
|
||||
.iter()
|
||||
.map(|(name, symbol_id)| (*symbol_id, name.to_string()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut writes = BTreeMap::<String, Vec<(usize, String)>>::new();
|
||||
for statement in &parsed.program.body {
|
||||
let JavascriptStatement::ExpressionStatement(statement) = statement else {
|
||||
continue;
|
||||
};
|
||||
let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else {
|
||||
continue;
|
||||
};
|
||||
let Some(symbol_id) = javascript_assignment_target_root_symbol(&assignment.left, scoping)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(name) = root_names.get(&symbol_id) else {
|
||||
continue;
|
||||
};
|
||||
let span = statement.span;
|
||||
let Some(source) = content.get(span.start as usize..span.end as usize) else {
|
||||
continue;
|
||||
};
|
||||
writes
|
||||
.entry(name.clone())
|
||||
.or_default()
|
||||
.push((span.start as usize, source.to_string()));
|
||||
}
|
||||
writes
|
||||
}
|
||||
|
||||
fn javascript_projection_fragment_dependencies(
|
||||
fragment: &str,
|
||||
declarations: &BTreeMap<String, String>,
|
||||
) -> Option<Vec<String>> {
|
||||
let allocator = JavascriptAllocator::default();
|
||||
let parsed = JavascriptParser::new(&allocator, fragment, javascript_source_type(true)).parse();
|
||||
if parsed.panicked || !parsed.diagnostics.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program);
|
||||
if !semantic.diagnostics.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
semantic
|
||||
.semantic
|
||||
.scoping()
|
||||
.root_unresolved_references()
|
||||
.keys()
|
||||
.map(|dependency| dependency.as_str().to_string())
|
||||
.filter(|dependency| declarations.contains_key(dependency))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn javascript_module_binding_projection(
|
||||
content: &str,
|
||||
imported: &BTreeSet<String>,
|
||||
@@ -6952,6 +7053,7 @@ pub(in crate::agent) fn javascript_module_binding_projection(
|
||||
return String::new();
|
||||
};
|
||||
let mut declarations = javascript_top_level_declarations(content);
|
||||
let assignment_writes = javascript_top_level_assignment_writes(content);
|
||||
declarations.extend(
|
||||
analysis
|
||||
.synthetic_declarations
|
||||
@@ -6971,6 +7073,8 @@ pub(in crate::agent) fn javascript_module_binding_projection(
|
||||
.collect::<Vec<_>>();
|
||||
let mut included = BTreeSet::new();
|
||||
let mut included_declarations = BTreeSet::new();
|
||||
let mut included_write_bindings = BTreeSet::new();
|
||||
let mut selected_writes = BTreeMap::<usize, String>::new();
|
||||
let mut projection = String::new();
|
||||
while let Some(name) = pending.pop() {
|
||||
if !included.insert(name.clone()) {
|
||||
@@ -6979,33 +7083,42 @@ pub(in crate::agent) fn javascript_module_binding_projection(
|
||||
let Some(declaration) = declarations.get(&name) else {
|
||||
continue;
|
||||
};
|
||||
if !included_declarations.insert(declaration.clone()) {
|
||||
continue;
|
||||
if included_declarations.insert(declaration.clone()) {
|
||||
projection.push_str(declaration);
|
||||
projection.push('\n');
|
||||
let Some(dependencies) =
|
||||
javascript_projection_fragment_dependencies(declaration, &declarations)
|
||||
else {
|
||||
return String::new();
|
||||
};
|
||||
pending.extend(
|
||||
dependencies
|
||||
.into_iter()
|
||||
.filter(|dependency| !included.contains(dependency)),
|
||||
);
|
||||
}
|
||||
projection.push_str(declaration);
|
||||
projection.push('\n');
|
||||
let allocator = JavascriptAllocator::default();
|
||||
let parsed =
|
||||
JavascriptParser::new(&allocator, declaration, javascript_source_type(true)).parse();
|
||||
if parsed.panicked || !parsed.diagnostics.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program);
|
||||
if !semantic.diagnostics.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
for dependency in semantic
|
||||
.semantic
|
||||
.scoping()
|
||||
.root_unresolved_references()
|
||||
.keys()
|
||||
{
|
||||
let dependency = dependency.as_str().to_string();
|
||||
if declarations.contains_key(&dependency) && !included.contains(&dependency) {
|
||||
pending.push(dependency);
|
||||
if included_write_bindings.insert(name.clone()) {
|
||||
for (position, write) in assignment_writes.get(&name).into_iter().flatten() {
|
||||
selected_writes
|
||||
.entry(*position)
|
||||
.or_insert_with(|| write.clone());
|
||||
let Some(dependencies) =
|
||||
javascript_projection_fragment_dependencies(write, &declarations)
|
||||
else {
|
||||
return String::new();
|
||||
};
|
||||
pending.extend(
|
||||
dependencies
|
||||
.into_iter()
|
||||
.filter(|dependency| !included.contains(dependency)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for write in selected_writes.into_values() {
|
||||
projection.push_str(&write);
|
||||
projection.push('\n');
|
||||
}
|
||||
projection
|
||||
}
|
||||
|
||||
@@ -10259,6 +10372,57 @@ mod javascript_projection_reachability_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exported_roots_keep_direct_top_level_initialization_writes() {
|
||||
for (content, exported, assignment, dependency) in [
|
||||
(
|
||||
"const implementation = () => rotateBoard(); function rotateBoard() { return import('./live-binding.mjs'); } export let rotatePiece; rotatePiece = implementation; const decoy = {}; decoy.rotatePiece = () => import('./decoy.mjs');",
|
||||
"rotatePiece",
|
||||
"rotatePiece = implementation",
|
||||
"const implementation =",
|
||||
),
|
||||
(
|
||||
"const implementation = () => import('./object-member.mjs'); export const api = {}; api.rotatePiece = implementation;",
|
||||
"api",
|
||||
"api.rotatePiece = implementation",
|
||||
"const implementation =",
|
||||
),
|
||||
(
|
||||
"const implementation = function() { return import('./prototype-member.mjs'); }; export class Game {} Game.prototype.rotatePiece = implementation;",
|
||||
"Game",
|
||||
"Game.prototype.rotatePiece = implementation",
|
||||
"const implementation =",
|
||||
),
|
||||
] {
|
||||
let projection = javascript_module_binding_projection(
|
||||
content,
|
||||
&BTreeSet::from([exported.to_string()]),
|
||||
);
|
||||
assert!(
|
||||
projection.contains(assignment),
|
||||
"an exported root must retain its direct module-initialization write: {projection}",
|
||||
);
|
||||
assert!(
|
||||
projection.contains(dependency),
|
||||
"the initialization RHS dependency must join the projection: {projection}",
|
||||
);
|
||||
assert!(
|
||||
projection.find(dependency) < projection.find(assignment),
|
||||
"dependencies must be declared before deferred initialization writes: {projection}",
|
||||
);
|
||||
assert!(javascript_is_syntactically_valid(&projection, true));
|
||||
}
|
||||
|
||||
let projection = javascript_module_binding_projection(
|
||||
"export const api = {}; const decoy = {}; decoy.rotatePiece = () => import('./decoy.mjs');",
|
||||
&BTreeSet::from(["api".to_string()]),
|
||||
);
|
||||
assert!(
|
||||
!projection.contains("decoy.rotatePiece"),
|
||||
"writes rooted at an unrelated binding must remain outside the projection",
|
||||
);
|
||||
}
|
||||
|
||||
#[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);";
|
||||
|
||||
@@ -5992,3 +5992,4 @@
|
||||
- JavaScript / ESM alias 求值顺序补充:receiver alias 保存赋值完成时刻并在该时刻解析 source owner,后续 source 重赋值不得倒灌。调用事件按内层参数 / RHS 先于外层调用 / assignment 生效;`switch case/default` 赋值一律保留跳过与各分支可能状态,普通函数内无条件 `return / throw` 截断之后的 alias 副作用。恒真 / 恒假关键字大小写敏感,可能被局部或参数遮蔽的 `undefined` 不再作为文本恒假值。
|
||||
- JavaScript / ESM callee 与终止顺序补充:identifier callee 和 `new C(args)` 的 constructor / instance owner 在实参前冻结,invocation effect 保留在实参之后;callable assignment 到 RHS 完成后才生效,`start = start()` 继续调用旧值。`return / throw` 表达式中的 assignment / call 先执行,截断点取表达式之后的 AST statement end;函数体使用 Oxc body span,不从默认参数或解构参数中的首个 `{` 猜测。未知 guard clause 后续与 `catch` 体一律按 conditional effect 合并旧状态。
|
||||
- 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。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user