diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs index 268ccc010..0f1677fba 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs @@ -1083,6 +1083,7 @@ struct NamedJavascriptFunctionRange { member_static: bool, externally_callable: bool, invocations: Vec, + synchronous_invocations: Vec, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -1149,6 +1150,7 @@ impl JavascriptFunctionDefinitionCollector { member_static, externally_callable: false, invocations: Vec::new(), + synchronous_invocations: Vec::new(), }); } } @@ -1340,6 +1342,8 @@ struct JavascriptMethodReceiverOwnerCollector<'a> { BTreeMap>>, callable_events: BTreeMap>>, + member_callable_events: + BTreeMap<(JavascriptSymbolId, String), Vec>>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, } @@ -1365,6 +1369,10 @@ enum JavascriptCallableValue { name: String, captured_at: usize, }, + Bound { + source: JavascriptSymbolId, + captured_at: usize, + }, } impl JavascriptMethodReceiverOwnerCollector<'_> { @@ -1400,15 +1408,9 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { true, ))] } - JavascriptExpression::NewExpression(new_expression) => self - .referenced_symbol(&new_expression.callee) - .map(|source| JavascriptReceiverOwnerValue::Alias { - source, - force_instance: true, - frozen_at: new_expression.span.start as usize, - }) - .into_iter() - .collect(), + JavascriptExpression::NewExpression(new_expression) => { + self.new_expression_receiver_values(new_expression) + } JavascriptExpression::Identifier(_) => self .referenced_symbol(initializer) .map(|source| JavascriptReceiverOwnerValue::Alias { @@ -1495,7 +1497,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { .into_iter() .flat_map(|bind| { if let Some(source) = self.referenced_symbol(bind.object()) { - return vec![JavascriptCallableValue::Alias { + return vec![JavascriptCallableValue::Bound { source, captured_at, }]; @@ -1519,6 +1521,81 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { } } + fn new_expression_receiver_values( + &self, + expression: &oxc_ast::ast::NewExpression<'_>, + ) -> Vec { + self.receiver_values(&expression.callee, expression.span.start as usize) + .into_iter() + .map(|value| match value { + JavascriptReceiverOwnerValue::Direct((start, end, _)) => { + JavascriptReceiverOwnerValue::Direct((start, end, false)) + } + JavascriptReceiverOwnerValue::Alias { + source, frozen_at, .. + } => JavascriptReceiverOwnerValue::Alias { + source, + force_instance: true, + frozen_at, + }, + }) + .collect() + } + + fn assignment_member_key( + &self, + target: &oxc_ast::ast::AssignmentTarget<'_>, + ) -> Option<(JavascriptSymbolId, String)> { + match target { + oxc_ast::ast::AssignmentTarget::StaticMemberExpression(member) => self + .referenced_symbol(&member.object) + .map(|symbol| (symbol, member.property.name.to_string())), + oxc_ast::ast::AssignmentTarget::ComputedMemberExpression(member) => self + .referenced_symbol(&member.object) + .zip(member.static_property_name().map(|name| name.to_string())), + _ => None, + } + } + + fn record_member_callable_assignment( + &mut self, + receiver: JavascriptSymbolId, + name: String, + expression: &JavascriptExpression<'_>, + captured_at: usize, + position: usize, + ) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + position, + ) { + return; + } + let values = self.callable_values(expression, captured_at); + let conditional = + javascript_position_is_conditionally_executed(self.conditional_ranges, position); + let events = self + .member_callable_events + .entry((receiver, name)) + .or_default(); + if values.is_empty() { + events.push(JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + value: None, + conditional, + }); + } else { + events.extend(values.into_iter().map(|value| JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + value: Some(value), + conditional, + })); + } + } + fn record_callable_assignment( &mut self, symbol_id: JavascriptSymbolId, @@ -1578,15 +1655,12 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { true, ))] } - JavascriptExpression::NewExpression(new_expression) => self - .referenced_symbol(&new_expression.callee) - .map(|source| JavascriptReceiverOwnerValue::Alias { - source, - force_instance: true, - frozen_at: new_expression.span.start as usize, - }) - .into_iter() - .collect(), + JavascriptExpression::NewExpression(new_expression) => { + self.new_expression_receiver_values(new_expression) + } + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.receiver_values(&parenthesized.expression, position) + } JavascriptExpression::Identifier(_) => self .referenced_symbol(initializer) .map(|source| JavascriptReceiverOwnerValue::Alias { @@ -1610,10 +1684,16 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { ) -> ( BTreeMap>>, BTreeMap>>, + BTreeMap<(JavascriptSymbolId, String), Vec>>, Vec<((usize, usize), JavascriptSymbolId, usize)>, ) { let super_owners = self.super_aliases.into_iter().collect(); - (self.owner_events, self.callable_events, super_owners) + ( + self.owner_events, + self.callable_events, + self.member_callable_events, + super_owners, + ) } } @@ -1679,6 +1759,15 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { fn visit_assignment_expression(&mut self, assignment: &oxc_ast::ast::AssignmentExpression<'a>) { if assignment.operator.is_assign() { + if let Some((receiver, name)) = self.assignment_member_key(&assignment.left) { + self.record_member_callable_assignment( + receiver, + name, + &assignment.right, + assignment.right.span().start as usize, + assignment.span.end as usize, + ); + } if let oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left { @@ -1719,6 +1808,8 @@ struct JavascriptFunctionInvocationCollector<'a, 'b> { BTreeMap>>, callable_events: BTreeMap>>, + member_callable_events: + BTreeMap<(JavascriptSymbolId, String), Vec>>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, marker: std::marker::PhantomData<&'a ()>, } @@ -1862,6 +1953,28 @@ fn javascript_known_callback_argument_indices( NONE } +fn javascript_known_callback_is_synchronous(call: &JavascriptCallExpression<'_>) -> bool { + let mut callee = &call.callee; + while let JavascriptExpression::ParenthesizedExpression(parenthesized) = callee { + callee = &parenthesized.expression; + } + let Some(member) = callee.as_member_expression() else { + return false; + }; + let Some(name) = member.static_property_name() else { + return false; + }; + let mut receiver = member.object(); + while let JavascriptExpression::ParenthesizedExpression(parenthesized) = receiver { + receiver = &parenthesized.expression; + } + matches!(receiver, JavascriptExpression::ArrayExpression(_)) + && matches!( + name, + "every" | "filter" | "find" | "forEach" | "map" | "reduce" | "some" + ) +} + fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize { // A nested call's closing delimiter precedes its parent's closing delimiter. Using that // source position models argument/RHS evaluation before the enclosing call or assignment. @@ -1869,6 +1982,60 @@ fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize { } impl JavascriptFunctionInvocationCollector<'_, '_> { + fn push_invocations(&mut self, indices: Vec, at: usize, synchronous: bool) { + for index in indices { + self.ranges[index].invocations.push(at); + if synchronous { + self.ranges[index].synchronous_invocations.push(at); + } + } + } + + fn callable_indices_for_value( + &self, + value: &JavascriptCallableValue, + visiting: &mut BTreeSet, + ) -> Vec { + match value { + JavascriptCallableValue::Direct(span) => self + .expression_ranges + .get(span) + .into_iter() + .flatten() + .copied() + .collect(), + JavascriptCallableValue::Alias { + source, + captured_at, + } => self.callable_indices_for_symbol(*source, *captured_at, visiting), + JavascriptCallableValue::Member { + receiver, + name, + captured_at, + } => self + .receiver_owners_for_symbol(*receiver, *captured_at, &mut BTreeSet::new()) + .into_iter() + .flat_map(|owner| self.method_indices_for_owner(owner, name, &mut BTreeSet::new())) + .collect(), + JavascriptCallableValue::Bound { + source, + captured_at, + } => { + let override_is_conditional_or_absent = self + .member_callable_override_indices_for_key( + &(*source, "bind".to_string()), + *captured_at, + ) + .is_none_or(|(_, conditional)| conditional); + if override_is_conditional_or_absent { + self.callable_indices_for_symbol(*source, *captured_at, visiting) + } else { + Vec::new() + } + } + } + } + fn callable_indices_for_symbol( &self, symbol_id: JavascriptSymbolId, @@ -1902,42 +2069,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { let Some(value) = events[index].value.as_ref() else { continue; }; - match value { - JavascriptCallableValue::Direct(span) => { - indices.extend( - self.expression_ranges - .get(span) - .into_iter() - .flatten() - .copied(), - ); - } - JavascriptCallableValue::Alias { - source, - captured_at, - } => indices.extend(self.callable_indices_for_symbol( - *source, - *captured_at, - visiting, - )), - JavascriptCallableValue::Member { - receiver, - name, - captured_at, - } => { - for owner in self.receiver_owners_for_symbol( - *receiver, - *captured_at, - &mut BTreeSet::new(), - ) { - indices.extend(self.method_indices_for_owner( - owner, - name, - &mut BTreeSet::new(), - )); - } - } - } + indices.extend(self.callable_indices_for_value(value, visiting)); } } } @@ -1987,6 +2119,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { identifier: &oxc_ast::ast::IdentifierReference<'_>, resolve_at: usize, invoke_at: usize, + synchronous: bool, ) { let Some(symbol_id) = identifier .reference_id @@ -1995,9 +2128,11 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { else { return; }; - for index in self.callable_indices_for_symbol(symbol_id, resolve_at, &mut BTreeSet::new()) { - self.ranges[index].invocations.push(invoke_at); - } + self.push_invocations( + self.callable_indices_for_symbol(symbol_id, resolve_at, &mut BTreeSet::new()), + invoke_at, + synchronous, + ); } fn current_owner(&self, at: usize) -> Option<(usize, usize, bool)> { @@ -2179,6 +2314,56 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { indices } + fn member_callable_key( + &self, + member: &oxc_ast::ast::MemberExpression<'_>, + ) -> Option<(JavascriptSymbolId, String)> { + let identifier = member.object().get_identifier_reference()?; + let receiver = identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id())?; + Some((receiver, member.static_property_name()?.to_string())) + } + + fn member_callable_override_indices( + &self, + member: &oxc_ast::ast::MemberExpression<'_>, + at: usize, + ) -> Option<(Vec, bool)> { + let key = self.member_callable_key(member)?; + self.member_callable_override_indices_for_key(&key, at) + } + + fn member_callable_override_indices_for_key( + &self, + key: &(JavascriptSymbolId, String), + at: usize, + ) -> Option<(Vec, bool)> { + let events = self.member_callable_events.get(key)?; + let selection = javascript_alias_event_indices_at( + events, + self.context_ranges, + self.content, + self.literal_false_ranges, + self.conditional_ranges, + at, + &mut BTreeSet::new(), + ); + if !selection.resolved { + return None; + } + let mut indices = Vec::new(); + for index in selection.indices { + if let Some(value) = events[index].value.as_ref() { + indices.extend(self.callable_indices_for_value(value, &mut BTreeSet::new())); + } + } + indices.sort_unstable(); + indices.dedup(); + Some((indices, selection.conditional)) + } + fn super_constructor_indices(&self, at: usize) -> Vec { let mut indices = self .current_owner(at) @@ -2198,13 +2383,19 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { expression: &JavascriptExpression<'_>, resolve_at: usize, invoke_at: usize, + synchronous: bool, ) { match expression { JavascriptExpression::Identifier(identifier) => { - self.record_identifier(identifier, resolve_at, invoke_at) + self.record_identifier(identifier, resolve_at, invoke_at, synchronous) } JavascriptExpression::ParenthesizedExpression(parenthesized) => { - self.record_expression(&parenthesized.expression, resolve_at, invoke_at); + self.record_expression( + &parenthesized.expression, + resolve_at, + invoke_at, + synchronous, + ); } JavascriptExpression::FunctionExpression(_) | JavascriptExpression::ArrowFunctionExpression(_) => { @@ -2213,16 +2404,22 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { .expression_ranges .get(&(span.start as usize, span.end as usize)) { - for index in indices { - self.ranges[*index].invocations.push(invoke_at); - } + self.push_invocations(indices.clone(), invoke_at, synchronous); } } _ => { if let Some(member) = expression.as_member_expression() { - for index in self.method_indices_for_member(member, resolve_at) { - self.ranges[index].invocations.push(invoke_at); - } + let indices = if let Some((mut indices, conditional)) = + self.member_callable_override_indices(member, resolve_at) + { + if conditional { + indices.extend(self.method_indices_for_member(member, resolve_at)); + } + indices + } else { + self.method_indices_for_member(member, resolve_at) + }; + self.push_invocations(indices, invoke_at, synchronous); } } } @@ -2238,10 +2435,17 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { if let JavascriptBindingPattern::BindingIdentifier(identifier) = &declarator.id { if let Some(JavascriptExpression::CallExpression(call)) = &declarator.init { if let Some(bind_member) = call.callee.as_member_expression() { + let override_is_conditional_or_absent = self + .member_callable_override_indices( + bind_member, + declarator.span.start as usize, + ) + .is_none_or(|(_, conditional)| conditional); if bind_member.static_property_name() == Some("bind") && self .method_indices_for_member(bind_member, declarator.span.start as usize) .is_empty() + && override_is_conditional_or_absent { let indices = self.callable_indices_for_expression( bind_member.object(), @@ -2264,28 +2468,40 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { let resolve_at = call.span.start as usize; let invoke_at = javascript_invocation_effect_position(call.span); if matches!(call.callee, JavascriptExpression::Super(_)) { - for index in self.super_constructor_indices(resolve_at) { - self.ranges[index].invocations.push(invoke_at); - } + self.push_invocations(self.super_constructor_indices(resolve_at), invoke_at, true); } else if let Some(member) = call.callee.as_member_expression() { let business_method = self.method_indices_for_member(member, resolve_at); + let override_callable = self.member_callable_override_indices(member, resolve_at); if matches!(member.static_property_name(), Some("call" | "apply")) && business_method.is_empty() { - self.record_expression(member.object(), resolve_at, invoke_at); + if override_callable.is_none() { + self.record_expression(member.object(), resolve_at, invoke_at, true); + } else { + self.record_expression(&call.callee, resolve_at, invoke_at, true); + if override_callable.is_some_and(|(_, conditional)| conditional) { + self.record_expression(member.object(), resolve_at, invoke_at, true); + } + } } else { - self.record_expression(&call.callee, resolve_at, invoke_at); + self.record_expression(&call.callee, resolve_at, invoke_at, true); } } else { - self.record_expression(&call.callee, resolve_at, invoke_at); + self.record_expression(&call.callee, resolve_at, invoke_at, true); } + let synchronous_callback = javascript_known_callback_is_synchronous(call); for index in self.callback_argument_indices(call) { if let Some(expression) = call .arguments .get(*index) .and_then(|argument| argument.as_expression()) { - self.record_expression(expression, expression.span().start as usize, invoke_at); + self.record_expression( + expression, + expression.span().start as usize, + invoke_at, + synchronous_callback, + ); } } oxc_ast_visit::walk::walk_call_expression(self, call); @@ -2307,9 +2523,7 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { .collect::>(); indices.sort_unstable(); indices.dedup(); - for index in indices { - self.ranges[index].invocations.push(invoke_at); - } + self.push_invocations(indices, invoke_at, true); oxc_ast_visit::walk::walk_new_expression(self, expression); } } @@ -2447,14 +2661,17 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan conditional_ranges: &conditional_ranges, owner_events: BTreeMap::new(), callable_events: BTreeMap::new(), + member_callable_events: BTreeMap::new(), super_aliases: Vec::new(), }; receiver_owner_collector.visit_program(&parsed.program); - let (receiver_owner_events, callable_events, super_aliases) = receiver_owner_collector.finish(); + let (receiver_owner_events, callable_events, member_callable_events, super_aliases) = + receiver_owner_collector.finish(); let mut context_ranges = definitions.ranges.clone(); for _ in 0..16 { for range in &mut definitions.ranges { range.invocations.clear(); + range.synchronous_invocations.clear(); } let mut invocations = JavascriptFunctionInvocationCollector { scoping: semantic.semantic.scoping(), @@ -2468,6 +2685,7 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan expression_ranges: expression_ranges.clone(), receiver_owner_events: receiver_owner_events.clone(), callable_events: callable_events.clone(), + member_callable_events: member_callable_events.clone(), super_aliases: super_aliases.clone(), marker: std::marker::PhantomData, }; @@ -2475,12 +2693,17 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan for range in &mut definitions.ranges { range.invocations.sort_unstable(); range.invocations.dedup(); + range.synchronous_invocations.sort_unstable(); + range.synchronous_invocations.dedup(); } let stable = definitions .ranges .iter() .zip(&context_ranges) - .all(|(current, previous)| current.invocations == previous.invocations); + .all(|(current, previous)| { + current.invocations == previous.invocations + && current.synchronous_invocations == previous.synchronous_invocations + }); context_ranges = definitions.ranges.clone(); if stable { break; @@ -5187,16 +5410,21 @@ fn javascript_alias_scope_effect( if visiting.contains(&callee_scope) { continue; } - for invocation in range.invocations.iter().copied().filter(|invocation| { - *invocation <= until - && javascript_alias_scope_at(ranges, *invocation) == scope - && !(range.start..range.end).contains(invocation) - && !javascript_position_is_in_literal_false_block( - content, - literal_false_ranges, - *invocation, - ) - }) { + for invocation in range + .synchronous_invocations + .iter() + .copied() + .filter(|invocation| { + *invocation <= until + && javascript_alias_scope_at(ranges, *invocation) == scope + && !(range.start..range.end).contains(invocation) + && !javascript_position_is_in_literal_false_block( + content, + literal_false_ranges, + *invocation, + ) + }) + { visiting.insert(callee_scope); let mut effect = javascript_alias_scope_effect( events, @@ -10863,6 +11091,123 @@ mod javascript_projection_reachability_tests { ); } + #[test] + fn asynchronous_callbacks_do_not_commit_outer_alias_side_effects_at_registration() { + for registration in [ + "setTimeout(callback, 0);", + "requestAnimationFrame(callback);", + "queueMicrotask(callback);", + "document.addEventListener('click', callback);", + "import('./seed.mjs').then(callback);", + ] { + let content = format!( + "function real() {{ return import('./async-real.mjs'); }} function decoy() {{ return import('./async-decoy.mjs'); }} let complete = real; function callback() {{ complete = decoy; }} {registration} complete();" + ); + let ranges = named_javascript_function_ranges(&content); + let real = content.find("./async-real.mjs").expect("find real import"); + let decoy = content + .find("./async-decoy.mjs") + .expect("find async decoy import"); + + assert!( + javascript_position_is_reachable(&content, &ranges, real), + "registration must not synchronously replace the outer callable: {registration}", + ); + assert!( + !javascript_position_is_reachable(&content, &ranges, decoy), + "an asynchronous callback side effect must not commit at registration: {registration}", + ); + } + + let synchronous = "function real() { return import('./sync-real.mjs'); } function replacement() { return import('./sync-replacement.mjs'); } let complete = real; [1].forEach(() => { complete = replacement; }); complete();"; + let ranges = named_javascript_function_ranges(synchronous); + let real = synchronous + .find("./sync-real.mjs") + .expect("find pre-iteration import"); + let replacement = synchronous + .find("./sync-replacement.mjs") + .expect("find synchronous replacement import"); + assert!(!javascript_position_is_reachable( + synchronous, + &ranges, + real + )); + assert!(javascript_position_is_reachable( + synchronous, + &ranges, + replacement, + )); + } + + #[test] + fn function_object_overrides_disable_call_apply_and_bind_intrinsics() { + for member in ["call", "apply", "bind"] { + let content = format!( + "function completeDecoy() {{ return import('./intrinsic-decoy-{member}.mjs'); }} function noop() {{ return import('./override-{member}.mjs'); }} completeDecoy.{member} = noop; completeDecoy.{member}(null);" + ); + let ranges = named_javascript_function_ranges(&content); + let decoy = content + .find(&format!("./intrinsic-decoy-{member}.mjs")) + .expect("find intrinsic decoy import"); + let override_import = content + .find(&format!("./override-{member}.mjs")) + .expect("find override import"); + + assert!( + !javascript_position_is_reachable(&content, &ranges, decoy), + "an own {member} override must not invoke the receiver as Function.prototype.{member}", + ); + assert!( + javascript_position_is_reachable(&content, &ranges, override_import), + "the own {member} override itself must remain callable", + ); + } + + let bound = "function completeDecoy() { return import('./bound-intrinsic-decoy.mjs'); } function noop() { return () => import('./bound-override-result.mjs'); } completeDecoy.bind = noop; const overridden = completeDecoy.bind(null); overridden();"; + let ranges = named_javascript_function_ranges(bound); + let decoy = bound + .find("./bound-intrinsic-decoy.mjs") + .expect("find bind intrinsic decoy import"); + assert!( + !javascript_position_is_reachable(bound, &ranges, decoy), + "an own bind override must not manufacture an alias of the receiver", + ); + + let installed = "function completeDecoy() { return import('./installed-bind-decoy.mjs'); } function noop() { return () => import('./installed-bind-result.mjs'); } function install() { completeDecoy.bind = noop; } install(); const overridden = completeDecoy.bind(null); overridden();"; + let ranges = named_javascript_function_ranges(installed); + let decoy = installed + .find("./installed-bind-decoy.mjs") + .expect("find installed bind decoy import"); + assert!( + !javascript_position_is_reachable(installed, &ranges, decoy), + "a synchronously installed bind override must disable the intrinsic alias", + ); + + let conditional = "function complete() { return import('./conditional-intrinsic.mjs'); } function noop() { return import('./conditional-override.mjs'); } if (flag) { complete.call = noop; } complete.call(null);"; + let ranges = named_javascript_function_ranges(conditional); + for source in ["./conditional-intrinsic.mjs", "./conditional-override.mjs"] { + let position = conditional.find(source).expect("find conditional import"); + assert!( + javascript_position_is_reachable(conditional, &ranges, position), + "a conditional own override must preserve both possible call targets: {source}", + ); + } + } + + #[test] + fn class_expression_instance_owner_survives_new_assignment() { + let content = "const game = new (class { run() { return import('./class-expression-instance.mjs'); } })(); game.run();"; + let ranges = named_javascript_function_ranges(content); + let position = content + .find("./class-expression-instance.mjs") + .expect("find class expression method import"); + + assert!( + javascript_position_is_reachable(content, &ranges, position), + "assigning a new class expression instance must retain its instance owner", + ); + } + #[test] fn controlled_callback_apis_unwrap_parenthesized_callees_and_receivers() { assert_callback_reachability( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 908a64c8f..ed12ddeae 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5993,3 +5993,4 @@ - 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。 +- JavaScript callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只作为可达根和闭包状态捕获点,不再作为函数体副作用的同步提交点;数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 形成成员 callable 状态,存在覆盖时禁止回退到 Function.prototype 语义;`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index ba6fb2fdd..a215b9c62 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -836,7 +836,7 @@ game-project/ - dynamic import demand 必须以 source 与 import occurrence position 联合作为身份,同 source 的可达裸 import 不得继承不可达 occurrence 的 export。动态 namespace 在首段 export 之后继续保留完整 member path,声明 initializer 与后续赋值式 `await import` 都必须绑定 semantic symbol。named / namespace 成员作为回调参数、对象解构 alias、实例 alias 与下游 wrapper 时继续按真实调用 span 传播 demand。constructor、`new Game().method()` 和实例 alias 分别形成精确成员根,`this.method()` 只匹配相同 class / object owner;顶层声明位置只使用 Oxc span,禁止以源码文本 `find` 反查。受控 callback API 名严格大小写敏感,调用图只读取真实 callback 参数位:timer、microtask、RAF、Promise 与数组迭代 API 取第一个参数,`addEventListener` 取第二个参数,delay、initial value、event type、options 和其它额外参数都不得建立执行边。恒假分支区间由单次函数可达性 analysis 预计算、排序合并并借用二分索引,不得使用 thread-local 完整源码 key 或在每个位置查询时复制区间、重扫整份源码。 - imported member 只有作为受控 callback API 的参数时才建立执行 demand;日志、注册元数据等普通传参不能被当作调用。普通 member alias 必须同时支持声明 initializer 与后续赋值,`new ns.Game()` 的完整 constructor path 继续传播到实例 alias。局部对象、class、实例、`this` 和 `super` 的方法调用统一按 semantic receiver owner 匹配,禁止按末级方法名跨 owner 选中同名 decoy。 - member alias、局部 receiver 与动态 namespace 的 assignment 必须保留赋值位置和 enclosing function scope;函数体内的使用必须按真实 invocation position 选择当时已生效的事件,多次调用跨过赋值边界时合并全部可能 owner,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。`receiver = source` 必须冻结赋值完成时的 source owner,source 后续重赋值不能反向改写 receiver;identifier callee 与 `new C(args)` 的 constructor / instance owner 在参数求值前冻结,真实 invocation effect 仍在参数求值后生效,callable assignment 只在 RHS 完成后替换旧值,因此 `start = start()` 必须调用旧 callable。`return / throw` 的表达式先完整执行,随后才截断后续事件;函数体和截断位置统一使用 Oxc AST body / statement span,不能被默认参数或解构参数中的 `{}` 误导。未知 guard clause 后的语句与 `catch` 体赋值都保留执行和跳过状态;调用事件继续按 JavaScript 的 callee、参数 / RHS、内层调用、外层调用 / 赋值顺序生效,`outer(inner())` 与 `receiver = mutate()` 不得按 AST 先序颠倒副作用。未知条件、循环以及 `switch case/default` 内赋值保留执行与跳过状态,conditional expression 保留全部可解析 owner,无法解析的确定重赋值显式 invalidation;已调用函数对外层 alias 的最终副作用按调用位置回写,但普通函数体内无条件 `return / throw` 之后的事件必须截断。被导出的 function、class/object member 还必须把模块初始化完成视为潜在外部调用时点,使声明之后生效的顶层赋值进入 demand,同时保留此前本地调用的旧状态。动态依赖传播继续以原始 owner 模块 AST span 为权威,不得因投影重排声明或省略独立赋值语句而重算出相反 alias。class owner 必须继续区分 static / instance,class expression 与实例化 alias 进入相同 receiver 图,`super` 的 parent owner 在 class 定义位置冻结。constructor、`.call()` 与 `.apply()` 都是实际 invocation;具名 function expression 只保留外层 binding 对应的单一 range,未被受控 API 执行的 inline function / arrow 保持不可达。恒真 / 恒假关键字只按 JavaScript 大小写识别,局部或参数遮蔽的 `undefined` 不得当作恒假;恒假分支识别必须先用 parser trivia 屏蔽注释和非执行 literal。callback API 只接受 semantic 未解析的已知全局调度函数或 AST 可证明的 literal array / dynamic import 调用;用户定义、导入或遮蔽的同名 `setTimeout / map / then` 不能触发参数执行推断。 -- `new` 必须覆盖显式 `super()`、隐式 derived constructor 和直接 class expression,未 override 的 instance / static 方法沿冻结的 `extends` 链查找;普通嵌套 function 拥有独立 `this`,只有 arrow 词法继承外层 owner。声明后通过 assignment 安装的 function / arrow、本地 function alias 和 `.bind()` 结果都必须保留 callable identity;只有真正 Function.prototype 的 `.call()` / `.apply()` / `.bind()` 采用 callable 语义,业务对象同名方法仍按 receiver method 解析。callable conditional expression 必须合并 consequent / alternate 的全部可调用身份;声明式和后续赋值式 `api.run.bind(api)` 都在绑定时冻结成员 owner 与方法名。受控 callback 识别穿透 callee 与 receiver 外层括号;Promise `.then` 的 fulfilled / rejected 两个参数都是 callback,`.catch` / `.finally` 仍仅取第一个。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop` 与 `mainloop` 合并。Canvas 视觉门必须复用同一组受 256 文件、累计源码 `2 MiB` 与投影处理 `32 MiB` 限制的 inline / external module 加载、链接和投影结果,并逐个原始大小写 unit 独立关联资产路径、图片变量与可达 `drawImage`;`