diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 07efba81d..db05519a6 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -141,6 +141,13 @@ function rootsAtError(lowerer: Lowerer, t: IrType | null): boolean { return false; } +/** Whether the scan recorded a dyn-flavored 'newListener' or 'removeListener' + * listener. Then every listener registers through the adapter, typed meta + * listeners included, so each entry has a function value to pass. */ +function hasDynMetaListener(table: Map): boolean { + return [...META_EVENTS].some((meta) => table.get(meta)?.dynListener === true); +} + /** Whether a typed listener of `name` registers through the dyn adapter, so * its entry holds a checked-dynamic function value: a dyn-flavored * 'newListener' or 'removeListener' listener receives every added or @@ -150,7 +157,7 @@ function rootsAtError(lowerer: Lowerer, t: IrType | null): boolean { * conversion admits it or throws a TypeError. */ function registersAsDyn(lowerer: Lowerer, name: string, streamForced: boolean): boolean { const table = emitterEvents(lowerer); - if ([...META_EVENTS].some((meta) => table.get(meta)?.dynListener === true)) return true; + if (hasDynMetaListener(table)) return true; if ((name === "error" || name === ERROR_MONITOR_EVENT) && table.get("error")?.valueEmit === true) return true; const sig = streamForced ? undefined : table.get(name); @@ -856,7 +863,9 @@ function boxedPayload(lowerer: Lowerer, value: IrExpr, node: ts.Expression): IrE * function that was registered. JavaScript sources read the untyped array, * whose functions take every argument a call passes. TypeScript sources * read the element type the typed path declares, the event's tuple: a - * listener of exactly that signature unwraps to the original closure. */ + * listener of exactly that signature unwraps to the original closure. An + * event no literal emit or listener gives a tuple (one registered only under + * computed names) reads the untyped array in both. */ function dynListenerArray( lowerer: Lowerer, table: Map, @@ -872,7 +881,7 @@ function dynListenerArray( type: DYN, loc, }; - if (isJsSourceFile(ts.getSourceFileOfNode(call))) return array; + if (isJsSourceFile(ts.getSourceFileOfNode(call)) || !table.has(name)) return array; const tuple = tupleOf(lowerer, table, name, call); return { kind: "dynCheck", @@ -1007,8 +1016,9 @@ function lowerDynListenerCall( // receives the event name and the added or removed listener's // checked-dynamic function value, as in Node. Every other listener // then registers through the adapter (registersAsDyn), which the event - // table's scan arranges. - if (registering && emitterEvents(lowerer).get(name)?.dynListener !== true) { + // table's scan arranges when it records a dyn-flavored listener of + // either meta event; a typed listener of the other one arrives here too. + if (registering && !hasDynMetaListener(emitterEvents(lowerer))) { lowerer.noLowering( `'${member}' of '${name}' with a checked-dynamic listener the event table did not record`, cbNode, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-mixed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-mixed.ts.json new file mode 100644 index 000000000..79a90729d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emitter-meta-listeners-mixed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emitter-meta-listeners-mixed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-computed-listeners.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-computed-listeners.ts.json new file mode 100644 index 000000000..e355d8e29 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-computed-listeners.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/events-computed-listeners.ts" + ], + "diags": [] +} diff --git a/tests/corpus/emitter-meta-listeners-mixed.ts b/tests/corpus/emitter-meta-listeners-mixed.ts new file mode 100644 index 000000000..9a1c8d884 --- /dev/null +++ b/tests/corpus/emitter-meta-listeners-mixed.ts @@ -0,0 +1,25 @@ +// A checked-dynamic listener of one meta event beside a typed listener of +// the other: both receive the event name, and the checked-dynamic one also +// the added or removed listener, as in Node. +import { EventEmitter } from "node:events"; + +const dynamicRemoved: any = (name: unknown, fn: unknown) => + console.log("removed", name, typeof fn); +const first = new EventEmitter(); +first.on("newListener", (name: string) => console.log("new", name)); +first.on("removeListener", dynamicRemoved); +const f = (n: number) => console.log("x", n); +first.on("x", f); +first.emit("x", 1); +first.removeListener("x", f); + +const dynamicAdded: any = (name: unknown, fn: unknown) => console.log("added", name, typeof fn); +const second = new EventEmitter(); +second.on("newListener", dynamicAdded); +second.on("removeListener", (name: string) => console.log("gone", name)); +const g = (s: string) => console.log("y", s); +second.on("y", g); +second.emit("y", "two"); +second.off("y", g); +second.removeAllListeners("y"); +console.log(first.listenerCount("x"), second.listenerCount("y")); diff --git a/tests/fixtures/node-types/events-computed-listeners.ts b/tests/fixtures/node-types/events-computed-listeners.ts new file mode 100644 index 000000000..17d3b3ba6 --- /dev/null +++ b/tests/fixtures/node-types/events-computed-listeners.ts @@ -0,0 +1,17 @@ +// listeners() and rawListeners() of an event registered only under computed +// names: no literal emit or listener fixes its arguments, so each returned +// listener takes every argument a call passes, as in Node. +import { EventEmitter } from "node:events"; +const ee = new EventEmitter(); +const keys = ["a", "b"]; +for (const k of keys) ee.on(`evt:${k}`, (x: number, y: number) => console.log("listener", k, x, y)); +function run(): void { + const fns = ee.listeners("evt:a"); + console.log("count", fns.length); + fns[0]!(1, 2); + const typed = ee.listeners("evt:b") as Array<(x: number, y: number) => void>; + typed[0]!(3, 4); + for (const fn of ee.rawListeners("evt:a")) fn(5, 6); + console.log("same", fns[0] === ee.listeners("evt:a")[0]); +} +run(); diff --git a/tests/harness/corpus-costs/emitter-meta-listeners-mixed.ts.json b/tests/harness/corpus-costs/emitter-meta-listeners-mixed.ts.json new file mode 100644 index 000000000..a82c79997 --- /dev/null +++ b/tests/harness/corpus-costs/emitter-meta-listeners-mixed.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 7100 } diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 03f05bdc6..d68ae59f1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -254,6 +254,24 @@ test("node-types: events.once over typed EventEmitter listeners under @types/nod expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: listener arrays of computed-name events take every argument under @types/node", async () => { + const outDir = outDirFor("events-computed-listeners"); + const entry = join(nodeTypesDir, "events-computed-listeners.ts"); + const result = await compile(entry, { + outPath: join(outDir, "events-computed-listeners"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: events.once with a signal over streams, raced, under @types/node", async () => { const outDir = outDirFor("events-once-signal-race"); const entry = join(nodeTypesDir, "events-once-signal-race.ts");