Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const eventsFor = (call: string) =>
eventName,
}));

const eventsForSource = (source: string) =>
analyzeJavaScriptSemantics(source).eventOperations.map(
({ kind, method, eventName }) => ({ kind, method, eventName }),
);

it.each([
["a numeric listener", `e.on("x", 1)`],
["a string listener", `e.on("x", "fn")`],
Expand Down Expand Up @@ -67,6 +72,299 @@ it.each([
]);
});

/**
* A listener method called without a listener argument passes `undefined` to
* Node's `checkListener`, which throws before anything is registered or
* removed. The argument position is empty, so no function can arrive there.
*
* Measured on Node 22.23.3:
*
* emitter.on() -> THREW ERR_INVALID_ARG_TYPE
* emitter.on("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.once("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.addListener("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.prependListener("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.prependOnceListener("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.removeListener("x") -> THREW ERR_INVALID_ARG_TYPE
* emitter.off("x") / emitter.off() -> THREW ERR_INVALID_ARG_TYPE
*
* A spread still reaches the position through the call, so it is untouched.
*/
it.each([
["a missing listener", `e.on("x")`],
["a missing once listener", `e.once("x")`],
["a missing addListener listener", `e.addListener("x")`],
["a missing prependListener listener", `e.prependListener("x")`],
["a missing prependOnceListener listener", `e.prependOnceListener("x")`],
["a missing removeListener listener", `e.removeListener("x")`],
["a missing off listener", `e.off("x")`],
["no arguments at all", `e.on()`],
["no off arguments at all", `e.off()`],
] as const)("emits no event operation for %s", (_name, call) => {
expect(eventsFor(call)).toEqual([]);
});

it("keeps the event operation for a spread call with no fixed listener", () => {
expect(eventsFor(`e.on(...pairs)`)).toEqual([
{
kind: expect.any(String) as unknown as string,
method: "on",
eventName: null,
},
]);
});

it.each([
[
"a named import",
`import { EventEmitter } from "node:events";`,
"EventEmitter",
],
["a default import", `import EE from "node:events";`, "EE"],
[
"a destructured require",
`const { EventEmitter } = require("events");`,
"EventEmitter",
],
[
"a member require",
`const events = require("node:events");`,
"events.EventEmitter",
],
] as const)(
"drops a missing listener on a receiver built from %s",
(_name, header, constructor) => {
expect(
eventsForSource(`${header}
const e = new ${constructor}();
e.on("x");`),
).toEqual([]);
},
);

it("keeps a missing listener when the receiver was reassigned", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
e = other;
e.on("x");`),
).toContainEqual({ kind: "register", method: "on", eventName: "x" });
});

/**
* `checkListener` is Node's own contract. An EventEmitter-style receiver that
* this module does not construct from `events.EventEmitter` may define a
* one-argument form: Socket.IO's `socket.off("x")` removes every listener for
* the event, so the operation must survive as an unresolved candidate.
*/
it("keeps a one-argument removal on a receiver with no proven Node emitter", () => {
expect(
eventsForSource(`function g(socket) { socket.off("x"); }`),
).toContainEqual({ kind: "remove", method: "off", eventName: "x" });
});

/**
* A subclass may default the listener position, which makes `bus.on("ready")`
* a real registration even though the module never proves Node's rejection.
* Measured on Node 22.23.3: this class stores `"ready"` and does not throw.
*/
it("keeps the registration of a subclass that defaults its listener", () => {
const events = eventsForSource(
`import { EventEmitter } from "node:events";
class Bus extends EventEmitter {
on(name, fn = () => {}) { return super.on(name, fn); }
}
const bus = new Bus();
bus.on("ready");`,
);
expect(events).toContainEqual({
kind: "register",
method: "on",
eventName: "ready",
});
});

/**
* A member write replaces the method the native contract belongs to. Measured
* on Node 22.23.3: `e.on = (name, fn = () => {}) => ...` stores `"ready"`, so
* `checkListener` never runs and the one-argument call is a real registration.
*/
it("keeps the registration of a receiver whose method was replaced", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
e.on = (name, fn = () => {}) => EventEmitter.prototype.on.call(e, name, fn);
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* Every instance shares the constructor's prototype, so replacing the method
* there replaces it for this receiver too.
*/
it("keeps the registration of a receiver whose prototype was replaced", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
EventEmitter.prototype.on = function (name, fn = () => {}) { return this; };
const e = new EventEmitter();
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* The proof is keyed on the selected method, so a write to an unrelated member
* must not weaken it: `tag` never answers `on`.
*/
it("still rejects a missing listener beside an unrelated member write", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
e.tag = 1;
e.on("ready");`),
).toEqual([]);
});

/**
* A namespace import constructs the class through a member expression, so the
* constructor has no binding of its own and the prototype write is recorded
* against the module instead. Measured on Node 22.22.2: rewriting `on` to
* delegate to the `addListener` captured before the rewrite stores `"ready"`
* for a one-argument call. Delegating to `this.addListener` instead overflows
* the stack, because Node defines `addListener` as the same function as `on`.
*/
it("keeps the registration of a receiver built from a namespace member", () => {
expect(
eventsForSource(`import * as events from "node:events";
const original = events.EventEmitter.prototype.addListener;
const e = new events.EventEmitter();
events.EventEmitter.prototype.on = function (name, fn = () => {}) { return original.call(this, name, fn); };
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/** A member require reaches the same class through the same member expression. */
it("keeps the registration of a receiver built from a member require", () => {
expect(
eventsForSource(`const events = require("node:events");
const original = events.EventEmitter.prototype.addListener;
const e = new events.EventEmitter();
events.EventEmitter.prototype.on = function (name, fn = () => {}) { return original.call(this, name, fn); };
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* Handing the instance to another call lets that call replace the method.
* Measured on Node 22.22.2: `configure(e)` replaces `on` with a form that
* defaults the listener, so `e.on("ready")` registers.
*/
it("keeps the registration of a receiver that was passed to a call", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
function configure(target) {
target.on = (name, fn = () => {}) => target.addListener(name, fn);
}
configure(e);
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* The added checks must not withdraw the proof from an untouched namespace
* receiver, or the guard would never apply to the member-expression forms.
*/
it("still rejects a missing listener on an untouched namespace receiver", () => {
expect(
eventsForSource(`import * as events from "node:events";
const e = new events.EventEmitter();
e.on("ready");`),
).toEqual([]);
});

/**
* A call that runs after the registration cannot change whether that
* registration threw. Measured on Node 22.22.2: `e.on("x")` throws
* `ERR_INVALID_ARG_TYPE` before `configure(e)` or `e.emit("done")` run, so a
* later escape is not evidence about this call.
*/
it("still rejects a missing listener registered before the receiver escapes", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
e.on("ready");
function configure(target) {
target.on = (name, fn = () => {}) => target.addListener(name, fn);
}
configure(e);
e.emit("done");`),
).not.toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* `prototype[key]` records an unknown property name, which still covers the
* method this call selects. Measured on Node 22.22.2: after that write
* `e.on("ready")` stores one listener.
*/
it("keeps the registration of a receiver whose method is replaced by key", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const original = EventEmitter.prototype.addListener;
const key = "on";
EventEmitter.prototype[key] = function (name, fn = () => {}) { return original.call(this, name, fn); };
const e = new EventEmitter();
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* A write through an inline require belongs to the module rather than to a
* binding. Measured on Node 22.22.2: it replaces `on` for every instance, so
* `e.on("ready")` stores one listener.
*/
it("keeps the registration of a receiver after an inline require write", () => {
expect(
eventsForSource(`const original = require("node:events").EventEmitter.prototype.addListener;
require("node:events").EventEmitter.prototype.on = function (name, fn = () => {}) { return original.call(this, name, fn); };
const { EventEmitter } = require("node:events");
const e = new EventEmitter();
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* Handing the prototype object itself to a call lets that call install a
* method. Measured on Node 22.22.2: after `configure(EventEmitter.prototype)`
* installs a defaulting wrapper, `e.on("ready")` stores one listener.
*/
it("keeps the registration after the prototype object escapes", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
function configure(proto) {
const saved = proto.on;
proto.on = function (name, fn = () => {}) { return saved.call(this, name, fn); };
}
configure(EventEmitter.prototype);
const e = new EventEmitter();
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* Replacing the receiver's own prototype changes which method answers the call.
* Measured on Node 22.22.2: with `e.__proto__` set to an object that defaults
* the listener, `e.on("ready")` does not throw.
*/
it("keeps the registration after the receiver prototype is replaced", () => {
expect(
eventsForSource(`import { EventEmitter } from "node:events";
const e = new EventEmitter();
const saved = EventEmitter.prototype.on;
e.__proto__ = { on(name, fn = () => {}) { return saved.call(this, name, fn); } };
e.on("ready");`),
).toContainEqual({ kind: "register", method: "on", eventName: "ready" });
});

/**
* `emit` passes its remaining arguments to listeners that already exist, so it
* does not validate them the way a registration does.
Expand Down
Loading
Loading