fix(javascript): bind response endpoints only from writes that reach the read (#4759)

collectResponseConsumers copied the linked request's endpoint onto a
response consumer whenever the receiver binding had exactly one
initializer (added in 0323ce0a2, #3822). A single initializer is a source
fact, not proof that the write reaches the read: an incoming parameter
that is overwritten only conditionally, or only after the read, still
lets the consumer read a caller-supplied response, yet it received the
request's exact endpoint.

The sole write now also has to cover the read, using the same rule the
call resolver applies to incoming values: either it is the binding's
stable unconditional declaration initializer, or the shared read-position
owner (semanticBindingAtRead) reports a definite preceding initializer.
Otherwise the endpoint stays null.

Fixes #4606

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-17 04:26:55 -07:00
committed by GitHub
parent 31c87b40ee
commit f1ab5959bd
2 changed files with 54 additions and 3 deletions
@@ -17,8 +17,11 @@ import {
recordSemanticValueLimit,
resolveSemanticBindingState,
semanticResolutionBlocked,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
builtinDataEffectMethod as builtinMethod,
dataEffectArgumentBindingId as argumentBindingId,
@@ -242,6 +245,21 @@ const collectRequests = (
return output;
};
/**
* Any other write (assignment, update, loop or destructuring target) can
* replace the response before this read. A sole write that can be skipped or
* runs after the read leaves an incoming value possible.
*/
const soleWriteReachesRead = (
binding: JavaScriptSemanticBindingState | undefined,
read: t.Node,
context: TraversalContext,
): boolean =>
binding?.initializers.length === 1 &&
(semanticStableInitializer(binding, context.state) !== undefined ||
semanticBindingAtRead(binding, read, context.state.parentsByNode)
.hasDefiniteInitializer);
const collectResponseConsumers = (
program: t.Program,
context: TraversalContext,
@@ -290,9 +308,7 @@ const collectResponseConsumers = (
)
? linked[0]
: undefined;
// Any other write (assignment, update, loop or destructuring target) can
// replace the response before this read, so its endpoint stays unknown.
const soleWrite = binding?.initializers.length === 1;
const soleWrite = soleWriteReachesRead(binding, member.object, context);
output.push({
requestId: `response:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
kind: "response-consumer",