mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
fix(javascript): emit no event operation for a literal non-function listener (#8621)
* fix(javascript): emit no event operation for a literal non-function listener
`EventEmitter` validates a listener before registering or removing anything:
if (typeof listener !== 'function')
throw new ERR_INVALID_ARG_TYPE('listener', 'function', listener);
The throw is synchronous, before any listener exists, but the analysis reported a
`register` or `remove` operation for those calls.
Measured on Node 24.22.1:
emitter.on(1) -> THREW ERR_INVALID_ARG_TYPE
emitter.on("x", 1) -> THREW ERR_INVALID_ARG_TYPE
emitter.on("x", null) -> THREW ERR_INVALID_ARG_TYPE
emitter.on("x", true) -> THREW ERR_INVALID_ARG_TYPE
emitter.on("x", {}) / ([]) -> THREW ERR_INVALID_ARG_TYPE
emitter.once / addListener / prependListener / prependOnceListener /
off / removeListener with a literal -> THREW ERR_INVALID_ARG_TYPE
--- accepted ---
emitter.on("x", () => {}) -> ok
emitter.on("x", function () {}) -> ok
`rejectsEventListener` drops the operation only for an exactly readable literal
that cannot be a function. An identifier, a call result or a spread may still
supply a function and keeps its operation, and a class expression passes Node's
own `typeof` check.
`emit` and `removeAllListeners` are untouched: `emit` passes its remaining
arguments as data to listeners that already exist, and `removeAllListeners` takes
only an optional event name.
* fix(javascript): price a non-finite listener and keep a shifted spread
Two review findings on the event-listener guard.
**A non-finite numeric literal was missed.** Babel stores `1e999` as a
`NumericLiteral` holding `Infinity`, and the exact-value reader refuses to price a
non-finite number, so the guard never saw it:
e.on("x", 1e999) -> THREW ERR_INVALID_ARG_TYPE
A numeric literal is always a number and therefore never callable, so the guard now
decides on the literal node directly.
**A leading spread shifts the listener position.** `e.on(...args, 1)` does not pass
`1` as the listener: Node spreads the array and ignores the trailing argument once
the listener is satisfied. The guard read `arguments[1]` regardless, so it dropped a
real registration. It now leaves the position undecided when argument 0 is a spread.
Five regressions cover both forms; all five fail without the fixes.
This commit is contained in:
1 parent
2ac6d3efe2
commit
59f5502a40
2 files changed
+159
-1
No files matched your search
@@ -7,7 +7,11 @@ import type {
|
||||
JavaScriptSemanticTimerOperation,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import { createSemanticCallableIdStack } from "./javascriptSemanticCallableStack.js";
|
||||
import { semanticStaticPropertyName } from "./javascriptAstValues.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
@@ -127,6 +131,36 @@ export const collectJavaScriptSemanticAsyncEffects = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A listener must be a function, and Node throws `ERR_INVALID_ARG_TYPE` before
|
||||
* anything is registered or removed when it is not. An exactly readable literal
|
||||
* that cannot be a function is therefore not an event operation, while an
|
||||
* identifier, a call result or a spread may still supply one and keeps its
|
||||
* operation.
|
||||
*
|
||||
* A class expression passes Node's own `typeof` check, so it is kept.
|
||||
*/
|
||||
const rejectsEventListener = (listener: t.Node): boolean => {
|
||||
if (t.isSpreadElement(listener)) return false;
|
||||
const value = unwrapJavaScriptExpression(listener).node;
|
||||
if (t.isFunctionExpression(value) || t.isArrowFunctionExpression(value))
|
||||
return false;
|
||||
if (t.isClassExpression(value)) return false;
|
||||
// A numeric literal is always a number, including the non-finite value Babel
|
||||
// stores for `1e999`, which the exact-value reader refuses to price.
|
||||
if (t.isNumericLiteral(value)) return true;
|
||||
// Every unary operator yields a primitive, and a primitive is never callable.
|
||||
return (
|
||||
t.isUnaryExpression(value) ||
|
||||
readExactJavaScriptLiteral(value).found ||
|
||||
t.isTemplateLiteral(value) ||
|
||||
t.isBigIntLiteral(value) ||
|
||||
t.isRegExpLiteral(value) ||
|
||||
t.isArrayExpression(value) ||
|
||||
t.isObjectExpression(value)
|
||||
);
|
||||
};
|
||||
|
||||
const collectEvent = (
|
||||
node: t.CallExpression | t.OptionalCallExpression,
|
||||
ownerCallableId: string | null,
|
||||
@@ -146,6 +180,15 @@ const collectEvent = (
|
||||
const listenerArgument = EVENT_LISTENER_METHODS.includes(method)
|
||||
? node.arguments[1]
|
||||
: undefined;
|
||||
// A leading spread shifts the runtime argument positions, so argument 1 is not
|
||||
// provably the listener: `e.on(...["x", fn], 1)` registers `fn` and ignores the
|
||||
// trailing `1`.
|
||||
if (
|
||||
!t.isSpreadElement(eventArgument) &&
|
||||
listenerArgument !== undefined &&
|
||||
rejectsEventListener(listenerArgument)
|
||||
)
|
||||
return;
|
||||
const emitter = emitterIdentity(member.object, state);
|
||||
const eventName = dataEffectLiteralString(eventArgument);
|
||||
output.push({
|
||||
|
||||
Reference in new issue
Block a user