Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
041a506
fix(javascript): emit no event operation for a missing listener argument
gongfpp Nov 2, 2026
f1d30fe
fix(javascript): hold only a proven Node emitter to the listener cont…
gongfpp Nov 3, 2026
4d36250
fix(javascript): require the native listener method itself, not just …
gongfpp Nov 3, 2026
b6917aa
chore: merge main to pick up the Wrangler config format fix
gongfpp Nov 3, 2026
9617f1a
fix(javascript): keep the listener operation when the method or the r…
gongfpp Nov 3, 2026
cb36de8
fix(javascript): read prototype writes by slot, not by exact key
gongfpp Nov 3, 2026
e9e604a
fix(javascript): withdraw the listener proof when the prototype itsel…
gongfpp Nov 3, 2026
0bd6c11
fix(javascript): require the events class surface to be untouched
gongfpp Nov 3, 2026
c964e98
fix(javascript): scope the class-surface proof to the events class
gongfpp Nov 3, 2026
87b7be8
Merge upstream main into missing-listener fix
gongfpp Nov 5, 2026
c9427ea
fix(javascript): keep listener candidates after shared mutations
gongfpp Nov 5, 2026
f038fdc
fix(javascript): retain constructed receiver class references
gongfpp Nov 5, 2026
47e3b9b
fix(javascript): time ambient prototype effects at XHR reads
gongfpp Nov 5, 2026
ad13261
perf(javascript): share ambient path effect matching
gongfpp Nov 5, 2026
94dc27a
merge: sync main into missing-listener fix
gongfpp Nov 9, 2026
98316a1
perf(javascript): index shared EventEmitter effects
gongfpp Nov 9, 2026
c719fb1
fix(javascript): track EventEmitter initializer escapes
gongfpp Nov 9, 2026
88d5732
fix(javascript): honor initializer binding provenance
gongfpp Nov 9, 2026
514a306
fix(javascript): preserve timed native receiver proofs
gongfpp Nov 9, 2026
9a7b483
fix(javascript): retain builtin acquisition mutation effects
gongfpp Nov 9, 2026
3c3684c
fix(javascript): retain awaited module acquisition effects
gongfpp Nov 9, 2026
65d74b0
fix(javascript): keep intrinsic Map calls from escaping
gongfpp Nov 9, 2026
b6cc762
fix(javascript): preserve pristine XHR and getter alias effects
gongfpp Nov 9, 2026
5944b69
chore: merge upstream main to refresh CI base
gongfpp Nov 9, 2026
676c01d
fix(javascript): share mutable events default effects
gongfpp Nov 9, 2026
25cc26b
fix(javascript): track inherited initializer call effects
gongfpp Nov 9, 2026
95cd68c
fix(javascript): preserve iterator and initializer effect boundaries
gongfpp Nov 9, 2026
e118fc9
fix(javascript): retain pristine Array receiver certainty
gongfpp Nov 9, 2026
ba119f3
fix(javascript): preserve builtin receiver and getter effects
gongfpp Nov 9, 2026
6b18b70
fix(javascript): preserve pristine Array read certainty
gongfpp Nov 9, 2026
9b39943
fix(javascript): preserve Array iteration read timing
gongfpp Nov 10, 2026
09001af
merge: sync main and preserve timed iteration effects
gongfpp Nov 10, 2026
be61d5c
fix(javascript): preserve native inspection and discarded-result proofs
gongfpp Nov 10, 2026
048274c
fix(javascript): preserve inherited native method proofs
gongfpp Nov 10, 2026
486aade
fix(javascript): reconcile listener proofs with upstream main
gongfpp Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 47 additions & 18 deletions src/domain/javascript/javascriptNetworkRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import {
} from "./javascriptAstValues.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
import { semanticPrimitiveCandidates } from "./javascriptSemanticPrimitives.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptBindingIndex,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import * as t from "@babel/types";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import {
isUnmodifiedGlobalReference,
semanticAmbientPathMayHaveChangedAt,
} from "./javascriptSemanticAmbientWrites.js";

export const isFetchCall = (
node: t.Node,
Expand Down Expand Up @@ -40,6 +45,7 @@ const isXhrOpenRequest = (
node: t.Node,
bindings: JavaScriptSemanticAnalysisState,
index?: Pick<JavaScriptBindingIndex, "referenceMayHaveChangedAt">,
purpose: "request" | "receiver" = "request",
): boolean | undefined => {
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return false;
Expand All @@ -49,22 +55,36 @@ const isXhrOpenRequest = (
semanticStaticPropertyName(callee.property, callee.computed) !== "open"
)
return false;
// Native open can dispatch readystatechange. Its receiver is nonescaping
// only when no handler or prior opaque operation can have configured it.
// Primitive arguments also avoid user code during WebIDL string conversion.
if (
purpose === "receiver" &&
node.arguments.some(
(argument) =>
t.isSpreadElement(argument) ||
semanticPrimitiveCandidates(
evaluateSemanticExpression(argument, bindings),
) === null,
)
)
return false;
const prototypeOwners =
purpose === "receiver"
? ["XMLHttpRequest", "XMLHttpRequestEventTarget", "EventTarget", "Object"]
: ["XMLHttpRequest", "Object"];
if (
["XMLHttpRequest", "Object"].some((owner) =>
["open", "__proto__"].some(
(member) =>
bindings.ambientWrites.covers([
"global",
owner,
"prototype",
member,
]) ||
bindings.ambientEscapes.covers([
"global",
owner,
"prototype",
member,
]),
prototypeOwners.some((owner) =>
(purpose === "receiver"
? ["open", "__proto__", "onreadystatechange"]
: ["open", "__proto__"]
).some((member) =>
semanticAmbientPathMayHaveChangedAt(
purpose === "receiver" ? node : callee,
bindings,
["global", owner, "prototype", member],
node,
),
),
)
)
Expand All @@ -74,8 +94,10 @@ const isXhrOpenRequest = (
while (t.isIdentifier(receiver)) {
if (
index !== undefined &&
(index.referenceMayHaveChangedAt(node, receiver, ["open"]) ||
index.referenceMayHaveChangedAt(node, receiver, ["__proto__"]))
(purpose === "receiver"
? index.referenceMayHaveChangedAt(node, receiver, [])
: index.referenceMayHaveChangedAt(node, receiver, ["open"]) ||
index.referenceMayHaveChangedAt(node, receiver, ["__proto__"]))
)
return false;
const binding = resolveSemanticBindingState(
Expand Down Expand Up @@ -103,6 +125,13 @@ const isXhrOpenRequest = (
return index === undefined && seen.size > 0 ? undefined : true;
};

/** A pristine native open is safe for the mutation producer, not every XHR call. */
export const isIntrinsicNonEscapingXhrOpen = (
node: t.Node,
bindings: JavaScriptSemanticAnalysisState,
index?: Pick<JavaScriptBindingIndex, "referenceMayHaveChangedAt">,
): boolean | undefined => isXhrOpenRequest(node, bindings, index, "receiver");

/** Resolve once through the shared mutation index when lexical proof needs it. */
export const createXhrOpenRequestQuery =
(index: JavaScriptBindingIndex): ((node: t.Node) => boolean) =>
Expand Down
255 changes: 254 additions & 1 deletion src/domain/javascript/javascriptSemanticAmbientWrites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js";
import {
createGlobalReferenceReader,
retainSemanticAmbientEffect,
semanticAmbientPathMayHaveChangedAt,
semanticAmbientObjectMayHaveChangedAt,
} from "./javascriptSemanticAmbientWrites.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";

it("keeps wrapper module aliases distinct from global-object module properties", () => {
Expand Down Expand Up @@ -337,3 +342,251 @@ it.each([
expect(statuses).toEqual(expected);
},
);

it.each([
[
"an intact method",
"const xhr=new XMLHttpRequest();xhr.open('GET','/xhr');",
false,
],
[
"a current function-local invocation",
"function run(){const xhr=new XMLHttpRequest();xhr.open('GET','/xhr');}",
false,
],
[
"an earlier constructor escape",
"const xhr=new XMLHttpRequest();mutate(xhr.constructor);xhr.open('GET','/xhr');",
true,
],
[
"a later constructor escape",
"const xhr=new XMLHttpRequest();xhr.open('GET','/xhr');mutate(xhr.constructor);",
false,
],
[
"an escaped method value",
"const xhr=new XMLHttpRequest();mutate(XMLHttpRequest.prototype.open);xhr.open('GET','/xhr');",
false,
],
[
"a selected method before argument effects",
"const xhr=new XMLHttpRequest();xhr.open('GET',(mutate(xhr.constructor),'/xhr'));",
false,
],
[
"a later effect in a possibly repeated activation",
"function run(){const xhr=new XMLHttpRequest();xhr.open('GET','/xhr');mutate(xhr.constructor);}",
true,
],
[
"an unlocated ambient escape",
"function request(){window.fetch('/other');}fetch=replacement;const xhr=new XMLHttpRequest();xhr.open('GET','/xhr');mutate(xhr.constructor);",
true,
],
])("retains timed ambient path effects for %s", (_label, source, changed) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected ambient-effect fixture");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
completeSemanticSteps(index.indexMemberMutationFactsSteps());
const results: boolean[] = [];
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, { name: "open" })
)
results.push(
semanticAmbientPathMayHaveChangedAt(
node.callee,
index.bindings,
["global", "XMLHttpRequest", "prototype", "open"],
node,
),
);
},
});
expect(results).toEqual([changed]);
});

it("shares matching ambient records across reads instead of scanning unrelated effects again", () => {
const work = (count: number): number => {
const source =
Array.from(
{ length: count },
(_value, index) =>
"new XMLHttpRequest().open('GET','/xhr');globalThis.unrelated" +
index +
"=replacement;",
).join("") + "XMLHttpRequest.prototype.open=replacement;";
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected ambient work fixture");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
completeSemanticSteps(index.indexMemberMutationFactsSteps());
// Producer proofs can already warm this path. Grow facts once so the
// measured reads include one cold lookup followed by shared warm lookups.
retainSemanticAmbientEffect(
index.bindings,
"write",
["global", "addedAfterIndex"],
parsed.program,
);
let reads = 0;
for (const effect of [
...index.bindings.ambientWriteEffects,
...index.bindings.ambientEscapeEffects,
]) {
const path = effect.path;
Object.defineProperty(effect, "path", {
configurable: true,
get: () => {
reads += 1;
return path;
},
});
}
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, { name: "open" })
)
semanticAmbientPathMayHaveChangedAt(
node.callee,
index.bindings,
["global", "XMLHttpRequest", "prototype", "open"],
node,
);
},
});
return reads;
};
const smaller = work(32);
expect(smaller).toBeGreaterThan(0);
expect(work(64)).toBeLessThan(3 * smaller);
});

it.each(["write", "escape"] as const)(
"refreshes cached ambient records when %s facts grow without caching read answers",
(kind) => {
const parsed = parseJavaScriptSource(
"before();inspect();after();inspect();",
);
if (parsed === null)
throw new Error("Expected growing ambient-effect fixture");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const calls: t.CallExpression[] = [];
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (t.isCallExpression(node)) calls.push(node);
},
});
const [before, first, after, second] = calls;
if (!before || !first || !after || !second)
throw new Error("Expected four call sites");
const path = ["global", "XMLHttpRequest", "prototype", "open"];
retainSemanticAmbientEffect(state, "write", path, after);
expect(semanticAmbientPathMayHaveChangedAt(first.callee, state, path)).toBe(
false,
);
expect(
semanticAmbientPathMayHaveChangedAt(second.callee, state, path),
).toBe(true);
retainSemanticAmbientEffect(
state,
kind,
kind === "write" ? path : ["global", "XMLHttpRequest"],
before,
);
expect(semanticAmbientPathMayHaveChangedAt(first.callee, state, path)).toBe(
true,
);
},
);

it("keeps invocation exclusions outside the shared ambient record cache", () => {
const parsed = parseJavaScriptSource("function run(){invoke();}");
if (parsed === null) throw new Error("Expected invocation-effect fixture");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
let invocation: t.CallExpression | undefined;
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (t.isCallExpression(node)) invocation = node;
},
});
if (invocation === undefined) throw new Error("Expected invocation");
const path = ["global", "XMLHttpRequest", "prototype", "open"];
retainSemanticAmbientEffect(
state,
"escape",
["global", "XMLHttpRequest"],
invocation,
);
expect(
semanticAmbientPathMayHaveChangedAt(
invocation.callee,
state,
path,
invocation,
),
).toBe(false);
expect(
semanticAmbientPathMayHaveChangedAt(invocation.callee, state, path),
).toBe(true);
});

it.each([
["Array.prototype[0] = replacement; inspect();", true],
["Array.prototype[key] = replacement; inspect();", true],
["Array.prototype[0].value = replacement; inspect();", true],
["configure(Array.prototype[0]); inspect();", true],
["configure(Array.prototype); inspect();", true],
["configure(Array); inspect();", true],
["inspect(); Array.prototype[0] = replacement;", false],
["function patch() { Array.prototype[0] = replacement; } inspect();", true],
["Map.prototype[0] = replacement; inspect();", false],
])("queries timed ambient subtree overlap: %s", (source, changed) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected ambient subtree fixture");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
completeSemanticSteps(index.indexAmbientWritesSteps());
completeSemanticSteps(index.indexMemberMutationFactsSteps());
const results: boolean[] = [];
traverseJavaScriptAst(parsed.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "inspect" })
) {
// A path query must not cache a subtree query's matching records or vice versa.
semanticAmbientPathMayHaveChangedAt(
node,
index.bindings,
["global", "Array", "prototype"],
node,
);
results.push(
semanticAmbientObjectMayHaveChangedAt(
node,
index.bindings,
["global", "Array", "prototype"],
node,
),
);
}
},
});
expect(results).toEqual([changed]);
});
Loading
Loading