修复脚本分支与回调参数快照

在条件测试完成后分别冻结两侧 callable identity。

按回调参数自身起点解析身份,保留前置参数副作用。

新增两类求值顺序回归并同步技术方案与决策记录。
This commit is contained in:
2026-08-04 14:07:41 +08:00
parent 4bed4a149a
commit 4f9df30dba
3 changed files with 46 additions and 3 deletions
@@ -1476,8 +1476,14 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
self.callable_values(&parenthesized.expression, captured_at)
}
JavascriptExpression::ConditionalExpression(conditional) => {
let mut values = self.callable_values(&conditional.consequent, captured_at);
values.extend(self.callable_values(&conditional.alternate, captured_at));
let mut values = self.callable_values(
&conditional.consequent,
conditional.consequent.span().start as usize,
);
values.extend(self.callable_values(
&conditional.alternate,
conditional.alternate.span().start as usize,
));
values
}
JavascriptExpression::CallExpression(call) => call
@@ -2277,7 +2283,7 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> {
.get(*index)
.and_then(|argument| argument.as_expression())
{
self.record_expression(expression, resolve_at, invoke_at);
self.record_expression(expression, expression.span().start as usize, invoke_at);
}
}
oxc_ast_visit::walk::walk_call_expression(self, call);
@@ -10430,6 +10436,20 @@ mod javascript_projection_reachability_tests {
}
}
#[test]
fn conditional_callable_captures_each_branch_after_the_test() {
let content = "function original() { return import('./before-test.mjs'); } function replacement() { return import('./after-test.mjs'); } let source = original; function swap() { source = replacement; return true; } const alias = swap() ? source : original; alias();";
let ranges = named_javascript_function_ranges(content);
let replacement = content
.find("./after-test.mjs")
.expect("find callable installed by the conditional test");
assert!(
javascript_position_is_reachable(content, &ranges, replacement),
"a conditional branch must capture callable state after its test side effects",
);
}
#[test]
fn local_function_alias_preserves_the_call_edge() {
let content = "function start() { return import('./aliased-function.mjs'); } const alias = start; alias();";
@@ -10642,6 +10662,27 @@ mod javascript_projection_reachability_tests {
}
}
#[test]
fn controlled_callback_captures_after_earlier_arguments() {
let content = "function original() { return import('./listener-before-argument.mjs'); } function replacement() { return import('./listener-after-argument.mjs'); } let listener = original; function swap() { listener = replacement; return 'click'; } document.addEventListener(swap(), listener);";
let ranges = named_javascript_function_ranges(content);
let original = content
.find("./listener-before-argument.mjs")
.expect("find callback before argument side effect");
let replacement = content
.find("./listener-after-argument.mjs")
.expect("find callback after argument side effect");
assert!(
!javascript_position_is_reachable(content, &ranges, original),
"a callback must not retain the identity replaced by an earlier argument",
);
assert!(
javascript_position_is_reachable(content, &ranges, replacement),
"a callback must capture the identity installed by an earlier argument",
);
}
#[test]
fn controlled_callback_apis_unwrap_parenthesized_callees_and_receivers() {
assert_callback_reachability(