mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-14 12:55:16 +08:00
Schedule a node:timers import with an untyped delay as a real timer
An untyped delay converts before the timer call and waits in a hidden local, so the lowered call sits inside a seqExpr. The realTimer mark was applied to the value lowerTimerCall returned, which that wrapper hid, so an imported setTimeout or setInterval with an untyped delay emitted the mock-aware entry point and its callback ran on the mocked queue. timers-delay-warnings-mocked.ts schedules an imported setTimeout with an untyped delay while mock.timers is enabled and calls the callback once the script ends, after the tick; timer-delay-lowering.test.ts pins the realTimer mark on the call inside the seqExpr.
This commit is contained in:
@@ -341,14 +341,18 @@ function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean {
|
||||
}
|
||||
|
||||
/** Marks a lowered timer call `realTimer` when its callee is a binding
|
||||
* mock.timers does not replace. */
|
||||
* mock.timers does not replace. A call whose delay is stored first returns a
|
||||
* `seqExpr` around the call, so the mark goes on the call inside it. */
|
||||
function markRealTimerCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
lowered: IrExpr | null,
|
||||
): IrExpr | null {
|
||||
if (lowered?.kind !== "libCall" || !MOCKABLE_TIMER_FNS.has(lowered.fn)) return lowered;
|
||||
return isRealTimerCallee(lowerer, expr.expression) ? { ...lowered, realTimer: true } : lowered;
|
||||
const call = lowered?.kind === "seqExpr" ? lowered.result : lowered;
|
||||
if (call?.kind !== "libCall" || !MOCKABLE_TIMER_FNS.has(call.fn)) return lowered;
|
||||
if (!isRealTimerCallee(lowerer, expr.expression)) return lowered;
|
||||
const marked: IrExpr = { ...call, realTimer: true };
|
||||
return lowered?.kind === "seqExpr" ? { ...lowered, result: marked } : marked;
|
||||
}
|
||||
|
||||
/** One timer call by MEMBER NAME — the shared lowering behind the ambient
|
||||
|
||||
@@ -5,8 +5,10 @@ import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrExpr } from "../src/ir/ir.js";
|
||||
|
||||
/** The delay argument of every setTimeout/setInterval libCall in the module. */
|
||||
async function timerDelays(source: string): Promise<IrExpr[]> {
|
||||
type TimerCall = Extract<IrExpr, { kind: "libCall" }>;
|
||||
|
||||
/** Every setTimeout/setInterval libCall in the module. */
|
||||
async function timerCalls(source: string): Promise<TimerCall[]> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-timer-delays-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
@@ -16,7 +18,7 @@ async function timerDelays(source: string): Promise<IrExpr[]> {
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
const delays: IrExpr[] = [];
|
||||
const calls: TimerCall[] = [];
|
||||
const visit = (value: unknown): void => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) visit(item);
|
||||
@@ -29,16 +31,21 @@ async function timerDelays(source: string): Promise<IrExpr[]> {
|
||||
typeof node.fn === "string" &&
|
||||
/^timers\.set(Timeout|TimeoutHandle|Interval)$/.test(node.fn)
|
||||
)
|
||||
delays.push(node.args![1]!);
|
||||
calls.push(node as TimerCall);
|
||||
for (const child of Object.values(value)) visit(child);
|
||||
};
|
||||
visit(mod.functions);
|
||||
return delays;
|
||||
return calls;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The delay argument of every setTimeout/setInterval libCall in the module. */
|
||||
async function timerDelays(source: string): Promise<IrExpr[]> {
|
||||
return (await timerCalls(source)).map((call) => call.args[1]!);
|
||||
}
|
||||
|
||||
test("number delays pass to the timer unchanged", async () => {
|
||||
const delays = await timerDelays(
|
||||
`function arm(ms: number): void {
|
||||
@@ -94,3 +101,23 @@ test("a number | undefined delay converts with ToNumber", async () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("an imported timer call stays real through an untyped delay", async () => {
|
||||
const calls = await timerCalls(
|
||||
`import { setTimeout as realSetTimeout, setInterval as realSetInterval } from "node:timers";
|
||||
function arm(delay: any): void {
|
||||
realSetTimeout(() => console.log("a"), delay);
|
||||
realSetInterval(() => console.log("b"), delay);
|
||||
setTimeout(() => console.log("c"), delay);
|
||||
}
|
||||
arm(1);
|
||||
export {};
|
||||
`,
|
||||
);
|
||||
// An untyped delay waits in a hidden local, so the call sits inside a
|
||||
// seqExpr; the callee still decides whether mock.timers routes it.
|
||||
expect(calls.map((call) => call.realTimer)).toEqual([true, true, undefined]);
|
||||
expect(calls[0]).toMatchObject({ fn: "timers.setTimeout" });
|
||||
expect(calls[1]).toMatchObject({ fn: "timers.setInterval" });
|
||||
expect(calls[2]).toMatchObject({ fn: "timers.setTimeout" });
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// While mock.timers mocks setTimeout and setInterval, the mocked functions
|
||||
// build no Timeout, so a negative or overflowing delay warns nothing;
|
||||
// after reset the real functions warn again, and a node:timers import is
|
||||
// never mocked.
|
||||
// never mocked: it schedules a real timer, including through an untyped
|
||||
// delay, whose conversion runs before the call.
|
||||
import { mock } from "node:test";
|
||||
import { setTimeout as realSetTimeout } from "node:timers";
|
||||
|
||||
@@ -15,7 +16,13 @@ const interval = setInterval(() => {
|
||||
clearInterval(interval);
|
||||
}, 2 ** 31);
|
||||
realSetTimeout(() => console.log("imported overflow"), 2 ** 32);
|
||||
function scheduleImported(delay: any): void {
|
||||
realSetTimeout(() => console.log("imported untyped delay"), delay);
|
||||
}
|
||||
scheduleImported(1);
|
||||
setTimeout(() => console.log("mocked delay"), 1);
|
||||
mock.timers.tick(1);
|
||||
console.log("after tick");
|
||||
mock.timers.reset();
|
||||
|
||||
setTimeout(() => console.log("real negative"), -5);
|
||||
|
||||
Reference in New Issue
Block a user