mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript-semantics): keep emitted data out of dispatch listener fields (#2952)
* fix(javascript-semantics): keep emitted data out of dispatch listener fields
The EventEmitter operation collector read argument position 1 as the
listener for every event method, so a dispatch such as
bus.emit("ready", data) reported the data binding and its range as the
listener. Only registration and removal name a listener there; emit
passes its remaining arguments to listeners that already exist. Read the
listener argument only for register and remove operations.
Fixes #2574
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(javascript-semantics): read listener arguments only for listener-taking methods
removeAllListeners accepts only an optional event name, so gating the
listener argument on operation kind still reported an ignored second
argument as a listener. Gate on the methods whose argument position 1
names a listener instead: the registration methods, off, and
removeListener.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -130,3 +130,74 @@ describe("JavaScript semantic analysis: runtime boundaries 1", () => {
|
||||
expect(ir.objectOperations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: event dispatch data", () => {
|
||||
it("keeps emitted data out of dispatch listener fields", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import { EventEmitter } from "node:events";
|
||||
const bus = new EventEmitter();
|
||||
const data = 1;
|
||||
const handler = () => {};
|
||||
bus.on("ready", handler);
|
||||
bus.emit("ready", data);
|
||||
bus.emit("ready");
|
||||
bus.off("ready", handler);
|
||||
bus.removeAllListeners("ready", data);
|
||||
`);
|
||||
const handler = ir.bindings.find(({ name }) => name === "handler");
|
||||
expect(handler).toBeDefined();
|
||||
expect(
|
||||
ir.eventOperations.map(
|
||||
({ kind, method, listenerBindingId, listenerLocation }) => ({
|
||||
kind,
|
||||
method,
|
||||
listenerBindingId,
|
||||
hasListenerLocation: listenerLocation !== null,
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "register",
|
||||
method: "on",
|
||||
listenerBindingId: handler?.bindingId,
|
||||
hasListenerLocation: true,
|
||||
},
|
||||
{
|
||||
kind: "dispatch",
|
||||
method: "emit",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
{
|
||||
kind: "dispatch",
|
||||
method: "emit",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
{
|
||||
kind: "remove",
|
||||
method: "off",
|
||||
listenerBindingId: handler?.bindingId,
|
||||
hasListenerLocation: true,
|
||||
},
|
||||
{
|
||||
kind: "remove",
|
||||
method: "removeAllListeners",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
ir.eventOperations.map(({ eventName, resolution }) => [
|
||||
eventName,
|
||||
resolution,
|
||||
]),
|
||||
).toEqual([
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,13 @@ const EVENT_REMOVE_METHODS = [
|
||||
"removeListener",
|
||||
"removeAllListeners",
|
||||
] as const;
|
||||
/**
|
||||
* Methods whose argument position 1 names a listener. `removeAllListeners`
|
||||
* takes only an optional event name, and `emit` passes its remaining
|
||||
* arguments as data to listeners that already exist.
|
||||
*/
|
||||
const EVENT_LISTENER_METHODS: readonly JavaScriptSemanticEventOperation["method"][] =
|
||||
[...EVENT_REGISTER_METHODS, "off", "removeListener"];
|
||||
const EVENT_METHODS = [
|
||||
...EVENT_REGISTER_METHODS,
|
||||
...EVENT_REMOVE_METHODS,
|
||||
@@ -137,7 +144,9 @@ const collectEvent = (
|
||||
if (method === undefined) return;
|
||||
const kind = eventKind(method);
|
||||
const eventArgument = node.arguments[0];
|
||||
const listenerArgument = node.arguments[1];
|
||||
const listenerArgument = EVENT_LISTENER_METHODS.includes(method)
|
||||
? node.arguments[1]
|
||||
: undefined;
|
||||
const emitter = emitterIdentity(member.object, state);
|
||||
const eventName = dataEffectLiteralString(eventArgument);
|
||||
output.push({
|
||||
|
||||
Reference in New Issue
Block a user