fix(javascript): select timer handles at cancellation reads

Use the shared binding-at-read owner instead of grouping schedules across the
whole binding lifetime. Match selected initializer nodes, retain uncertain
origins as candidate links, and preserve immutable closure captures.

Adapt the diagnosis in PR #3794 by @Xinglan233, whose source commit was authored
by Codex. Verify stale writes, later schedules, replacements and CLI graph links.

Refs #3358, #3794

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
morlutoandCodex committed 2026-10-16 06:06:11 +08:00
1 parent ebd5a82c43
commit 6183544e8b
3 files changed
+198 -19

No files matched your search

@@ -0,0 +1,89 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
const timerOperations = (source: string) =>
analyzeJavaScriptSemantics(source).timerOperations;
it("does not link cancellation to a timer after its handle is cleared", () => {
const operations = timerOperations(`
let handle = setTimeout(work, 10);
handle = null;
clearTimeout(handle);
`);
expect(operations.at(-1)).toMatchObject({
kind: "cancel",
linkedTimerId: null,
resolution: "unresolved",
});
});
it("links cancellation to the latest preceding timer assignment", () => {
const operations = timerOperations(`
let handle = setTimeout(first, 10);
handle = setTimeout(second, 20);
clearTimeout(handle);
`);
expect(operations[2]).toMatchObject({
kind: "cancel",
linkedTimerId: operations[1]?.timerId,
resolution: "complete",
});
});
it("does not link cancellation to a timer scheduled later", () => {
const operations = timerOperations(`
let handle;
clearTimeout(handle);
handle = setTimeout(work, 10);
`);
expect(operations[0]).toMatchObject({
kind: "cancel",
linkedTimerId: null,
resolution: "unresolved",
});
});
it("preserves an immutable timer handle captured by a closure", () => {
const operations = timerOperations(`
const handle = setTimeout(work, 10);
const cancel = () => clearTimeout(handle);
`);
expect(operations[1]).toMatchObject({
kind: "cancel",
linkedTimerId: operations[0]?.timerId,
resolution: "complete",
});
});
it("keeps cancellation uncertain after a conditional handle replacement", () => {
const operations = timerOperations(`
let handle = setTimeout(work, 10);
if (condition) handle = null;
clearTimeout(handle);
`);
expect(operations.at(-1)).toMatchObject({
kind: "cancel",
linkedTimerId: operations[0]?.timerId,
resolution: "partial",
});
});
it("keeps cancellation uncertain across a mutable closure capture", () => {
const operations = timerOperations(`
let handle = setTimeout(work, 10);
const cancel = () => clearTimeout(handle);
handle = null;
`);
expect(operations[1]).toMatchObject({
kind: "cancel",
linkedTimerId: operations[0]?.timerId,
resolution: "partial",
});
});