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:
1 parent
31c87b40ee
commit
f1ab5959bd
2 files changed
+54
-3
No files matched your search
@@ -57,3 +57,38 @@ it("leaves a response replaced by a non-request value without an endpoint", () =
|
||||
`),
|
||||
).toEqual([expect.objectContaining({ method: "json", endpoint: null })]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"ordered",
|
||||
`async function load() { const res = await fetch("https://example.invalid/api"); return res.json(); }`,
|
||||
"https://example.invalid/api",
|
||||
],
|
||||
[
|
||||
"parameter-replaced-before",
|
||||
`async function load(res) { res = await fetch("https://example.invalid/api"); return res.json(); }`,
|
||||
"https://example.invalid/api",
|
||||
],
|
||||
[
|
||||
"parameter-conditional",
|
||||
`async function load(res, maybe) { if (maybe) res = await fetch("https://example.invalid/api"); return res.json(); }`,
|
||||
null,
|
||||
],
|
||||
[
|
||||
"parameter-later",
|
||||
`async function load(res) { const value = await res.json(); res = await fetch("https://example.invalid/api"); return value; }`,
|
||||
null,
|
||||
],
|
||||
[
|
||||
"multiple-requests",
|
||||
`async function load(maybe) { let res = await fetch("https://example.invalid/a"); if (maybe) res = await fetch("https://example.invalid/b"); return res.json(); }`,
|
||||
null,
|
||||
],
|
||||
] as const)(
|
||||
"keeps a response endpoint only when its request reaches the read: %s",
|
||||
(_name, source, endpoint) => {
|
||||
expect(responses(source)).toEqual([
|
||||
expect.objectContaining({ method: "json", endpoint }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user