From 4e1e9b62a7d2763a0ca528f9f70173e86ccf841d Mon Sep 17 00:00:00 2001 From: RRFZ <247457381+Plastic-time@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:09:23 -0500 Subject: [PATCH] fix(javascript): preserve global escapes across callback boundaries --- .../javascriptSemanticAmbientWrites.ts | 24 ++++++++++--------- ...criptSemanticAnalysis.timer_delays.test.ts | 19 +++++++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 162ab650c8..152ffd495a 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -965,17 +965,19 @@ const createGlobalPathRead = ( // That escape cannot have replaced the callee before the call selects it. const escapePrecedesRead = effects.escapes.some((effect) => { const escape = effect.node; - if ( - escape !== undefined && - escape !== site && - typeof escape.start === "number" && - typeof escape.end === "number" && - typeof site.start === "number" && - typeof site.end === "number" && - site.start >= escape.start && - site.end <= escape.end - ) - return false; + // Containment alone does not order deferred callback/class bodies. + // Only a read reached without crossing such a boundary precedes escape. + if (escape !== undefined && escape !== site) { + let parent = state.parentsByNode.get(site); + while ( + parent !== undefined && + !t.isFunction(parent) && + !t.isClass(parent) + ) { + if (parent === escape) return false; + parent = state.parentsByNode.get(parent); + } + } return semanticEffectTiming(site, escape, state) !== "after"; }); if ( diff --git a/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts index 465740fe40..82779ae387 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.timer_delays.test.ts @@ -156,3 +156,22 @@ it("selects a constructor before an argument can escape the global object", () = `), ).toEqual([]); }); + +it.each(["Symbol", "BigInt"])( + "retains %s identity uncertainty inside an escaped callback", + (constructor) => { + const result = operations(` + import configure from "./configure.js"; + import { setTimeout, clearTimeout } from "node:timers"; + configure(globalThis, () => { + const handle = setTimeout(() => {}, ${constructor}("1")); + clearTimeout(handle); + }); + `); + expect(result).toMatchObject([ + { kind: "schedule", resolution: "complete" }, + { kind: "cancel", resolution: "complete" }, + ]); + expect(result[1]?.linkedTimerId).toBe(result[0]?.timerId); + }, +);