mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Read untyped listener arrays for tuple-less events, and route typed meta listeners beside checked-dynamic ones
- listeners() and rawListeners() in TypeScript of an event that no literal emit or listener gives a tuple (one registered only under computed names) read the untyped array, so each returned listener takes every argument a call passes. The array was checked into zero-argument functions, which called the listener without its arguments. - A typed 'newListener' or 'removeListener' listener in a program with a checked-dynamic listener of the other meta event registers through the adapter. The meta lowering's check looked only at the listener's own event and refused it with SC2020.
This commit is contained in:
@@ -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<string, EventSig>): 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<string, EventSig>,
|
||||
@@ -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,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/emitter-meta-listeners-mixed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/events-computed-listeners.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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"));
|
||||
@@ -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();
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4500, "san": 7100 }
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user