Bridge events whose typed listeners take no parameters

An event that typed code only listened to with a listener taking no
parameters had no bridge entry: the scan marked a listener as typed only
when one of its parameters was not a checked-dynamic value. Emitting the
event through an `any` reference then threw instead of calling the
listener. A TypeScript listener with no parameters now marks the event as
typed. A JavaScript function without parameters still registers as before,
since it can read `arguments`.
This commit is contained in:
Chris Tate
2026-10-21 11:03:19 -05:00
parent f798f6736c
commit cd38fd2248
4 changed files with 37 additions and 1 deletions
@@ -338,9 +338,15 @@ function emitterEvents(lowerer: Lowerer): Map<string, EventSig> {
const cbCt = lowerer.typeOf(node.arguments.nodes[1]);
const cbT =
lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments.nodes[1], cbCt);
// A TypeScript listener with no parameters is typed too: it
// registers without the adapter, so an untyped emit reaches it
// only through the event's fixed ABI. A JavaScript function
// without parameters can still read `arguments`.
if (
cbT?.kind !== "dyn" &&
(cbT?.kind !== "func" || cbT.params.some((p) => p.kind !== "dyn"))
(cbT?.kind !== "func" ||
(cbT.params.length === 0 && !isJsSourceFile(sf)) ||
cbT.params.some((p) => p.kind !== "dyn"))
) {
sigOf(name).typedListener = true;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6941-emitter-untyped-zero-arg-listener.ts"
],
"diags": []
}
@@ -0,0 +1,23 @@
// Emitting through an `any` reference reaches listeners that typed code
// registered with no parameters, for an event no typed code emits: they
// run once per emit, whatever arguments the untyped emit passes.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
let ready = 0;
emitter.on("ready", () => {
ready++;
console.log("ready", ready);
});
emitter.once("closed", function () {
console.log("closed once");
});
const loose: any = emitter;
console.log(loose.emit("ready"));
console.log(loose.emit("ready", 1, "two"));
console.log(loose.emit("closed", { reason: "done" }));
console.log(loose.emit("closed"));
console.log(loose.emit("missing"));
console.log(emitter.listenerCount("ready"), emitter.listenerCount("closed"), ready);
@@ -0,0 +1 @@
{ "plain": 2800, "san": 3400 }