From 090b454350591f58d3349fa992109d55414f5294 Mon Sep 17 00:00:00 2001 From: saurabh sharma <146759278+saurabh-sol@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:18:54 +0530 Subject: [PATCH] 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 * 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 --------- Co-authored-by: Claude Fable 5.1 --- ...emanticAnalysis.runtime_boundaries.test.ts | 71 +++++++++++++++++++ .../javascriptSemanticAsyncEffects.ts | 11 ++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts index 03e25032a6..792b8a0ea3 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.runtime_boundaries.test.ts @@ -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"], + ]); + }); +}); diff --git a/src/domain/javascript/javascriptSemanticAsyncEffects.ts b/src/domain/javascript/javascriptSemanticAsyncEffects.ts index f7914c20f7..5b3068101c 100644 --- a/src/domain/javascript/javascriptSemanticAsyncEffects.ts +++ b/src/domain/javascript/javascriptSemanticAsyncEffects.ts @@ -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({