mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
f1ab5959bd
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>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
|
|
const responses = (source: string) =>
|
|
analyzeJavaScriptSemantics(source)
|
|
.requestOperations.filter(({ kind }) => kind === "response-consumer")
|
|
.map(({ method, endpoint, resolution }) => ({
|
|
method,
|
|
endpoint,
|
|
resolution,
|
|
}));
|
|
|
|
it("keeps the endpoint of a uniquely consumed request response", () => {
|
|
expect(
|
|
responses(`
|
|
async function load() {
|
|
const a = await fetch("https://example.test/a");
|
|
const b = await fetch("https://example.test/b");
|
|
return [await a.json(), await b.text()];
|
|
}
|
|
`),
|
|
).toEqual([
|
|
{
|
|
method: "json",
|
|
endpoint: "https://example.test/a",
|
|
resolution: "complete",
|
|
},
|
|
{
|
|
method: "text",
|
|
endpoint: "https://example.test/b",
|
|
resolution: "complete",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("leaves an ambiguous response consumer without an endpoint", () => {
|
|
expect(
|
|
responses(`
|
|
async function load(flag) {
|
|
let response = await fetch("https://example.test/a");
|
|
if (flag) response = await fetch("https://example.test/b");
|
|
return response.json();
|
|
}
|
|
`),
|
|
).toEqual([{ method: "json", endpoint: null, resolution: "partial" }]);
|
|
});
|
|
|
|
it("leaves a response replaced by a non-request value without an endpoint", () => {
|
|
expect(
|
|
responses(`
|
|
async function load(cachedResponse) {
|
|
let response = await fetch("https://example.test/a");
|
|
response = cachedResponse;
|
|
return response.json();
|
|
}
|
|
`),
|
|
).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 }),
|
|
]);
|
|
},
|
|
);
|