From 186b2c352c55227bec71fad10fab1715e48b8ab7 Mon Sep 17 00:00:00 2001 From: RRFZ <247457381+Plastic-time@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:57:26 -0500 Subject: [PATCH] fix(javascript): omit Node timers with rejected delay values --- ...criptSemanticAnalysis.timer_delays.test.ts | 56 +++++++++++++++++++ .../javascriptSemanticAsyncEffects.ts | 34 ++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts diff --git a/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts new file mode 100644 index 0000000000..7558d5aec1 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts @@ -0,0 +1,56 @@ +import { expect, it } from "vitest"; +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; + +const operations = (source: string) => + analyzeJavaScriptSemantics(source).timerOperations; + +it.each(["setTimeout", "setInterval"])( + "does not create a %s handle when delay conversion must throw", + (method) => { + const result = operations(` + import { ${method}, clearTimeout } from "node:timers"; + const positive = ${method}(() => {}, 1n); + const negative = ${method}(() => {}, -1n); + const zero = ${method}(() => {}, 0n); + const symbol = ${method}(() => {}, Symbol("delay")); + const converted = ${method}(() => {}, BigInt("1")); + clearTimeout(positive); + `); + expect(result.filter(({ kind }) => kind === "schedule")).toEqual([]); + expect(result).toMatchObject([ + { kind: "cancel", resolution: "unresolved", linkedTimerId: null }, + ]); + }, +); + +it("keeps supported delays and the immediate callback's BigInt argument", () => { + const result = operations(` + import { setTimeout, setInterval, setImmediate } from "node:timers"; + setTimeout(() => {}, "2"); + setInterval(() => {}, null); + setTimeout(() => {}, !1n); + setTimeout(() => {}, void 1n); + setImmediate(() => {}, 1n); + `); + expect(result.map(({ method }) => method)).toEqual([ + "setTimeout", + "setInterval", + "setTimeout", + "setTimeout", + "setImmediate", + ]); +}); + +it("retains uncertain delays, shifted arguments and shadowed constructors", () => { + const result = operations(` + import { setTimeout } from "node:timers"; + function schedule(delay, args, Symbol, BigInt) { + setTimeout(() => {}, delay); + setTimeout(...args, 1n); + setTimeout(() => {}, ...args); + setTimeout(() => {}, Symbol("delay")); + setTimeout(() => {}, BigInt("1")); + } + `); + expect(result).toHaveLength(5); +}); diff --git a/src/domain/javascript/javascriptSemanticAsyncEffects.ts b/src/domain/javascript/javascriptSemanticAsyncEffects.ts index 4e5802e8c4..cbc3f7279f 100644 --- a/src/domain/javascript/javascriptSemanticAsyncEffects.ts +++ b/src/domain/javascript/javascriptSemanticAsyncEffects.ts @@ -376,6 +376,36 @@ const schedulesWithNonFunctionCallback = ( return t.isUnaryExpression(value); }; +// Node converts timeout and interval delays to numbers before creating a handle. +// BigInt and Symbol values throw there. A spread can shift the delay slot, and +// shadowed or replaced constructors cannot establish the argument's type. +const schedulesWithInvalidDelay = ( + node: t.CallExpression | t.OptionalCallExpression, + state: JavaScriptSemanticAnalysisState, +): boolean => { + if ( + node.arguments.slice(0, 2).some((argument) => t.isSpreadElement(argument)) + ) + return false; + const argument = node.arguments[1]; + if (argument === undefined) return false; + const value = unwrapJavaScriptExpression(argument).node; + if (t.isBigIntLiteral(value)) return true; + if ( + t.isUnaryExpression(value) && + ["+", "-", "~"].includes(value.operator) && + t.isBigIntLiteral(unwrapJavaScriptExpression(value.argument).node) + ) + return true; + return ( + t.isCallExpression(value) && + t.isIdentifier(value.callee) && + ["BigInt", "Symbol"].includes(value.callee.name) && + isUnmodifiedGlobalReference(value.callee, state, value.callee.name) && + !semanticOpaqueNameMayHaveChanged(value.callee, state) + ); +}; + const collectTimer = (input: TimerCollectionInput): void => { const { node, ownerCallableId, state, output, ancestors } = input; const identity = timerMethod(node.callee, state); @@ -384,7 +414,9 @@ const collectTimer = (input: TimerCollectionInput): void => { if ( TIMER_SCHEDULE_METHODS.some((candidate) => candidate === method) && nodeBuiltinTimerMethod(node.callee, state) !== null && - schedulesWithNonFunctionCallback(node, input.state) + (schedulesWithNonFunctionCallback(node, state) || + ((method === "setTimeout" || method === "setInterval") && + schedulesWithInvalidDelay(node, state))) ) return; const kind = TIMER_SCHEDULE_METHODS.some((candidate) => candidate === method)