fix(javascript): isolate lexical loop bindings (#511)

* fix(javascript): isolate lexical loop bindings

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

* test(javascript): isolate loop lexical names regression cases

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-05 03:11:24 -04:00 committed by GitHub
parent 6b7923cdd2
commit c30878f616
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 0 deletions

View File

@ -166,6 +166,66 @@ describe("JavaScript semantic analysis: structure 1", () => {
});
});
describe("JavaScript semantic analysis: loop lexical names", () => {
it.each([
`for (let value = "inside"; ready; step()) { consume(value); }`,
`for (const value of entries) { consume(value); }`,
`for (const value in entries) { consume(value); }`,
])("does not leak lexical loop bindings into the enclosing scope", (loop) => {
const ir = analyzeJavaScriptSemantics(`
const value = "outside";
${loop}
consume(value);
`);
expect(topLevelBinding(ir, "value").value).toEqual({
status: "literal",
value: "outside",
});
expect(bindingsNamed(ir, "value")).toHaveLength(2);
const reads = ir.references.filter(
({ name, role }) => name === "value" && role === "read",
);
expect(reads).toHaveLength(2);
expect(reads[0]?.bindingId).not.toBe(reads[1]?.bindingId);
expect(reads[1]?.bindingId).toBe(topLevelBinding(ir, "value").bindingId);
});
it("resolves closures inside a loop against the loop binding", () => {
const ir = analyzeJavaScriptSemantics(`
const value = "outside";
for (const value of entries) { callbacks.push(() => value); }
consume(value);
`);
const reads = ir.references.filter(
({ name, role }) => name === "value" && role === "read",
);
expect(reads).toHaveLength(2);
expect(reads[0]?.bindingId).not.toBe(
topLevelBinding(ir, "value").bindingId,
);
expect(reads[1]?.bindingId).toBe(topLevelBinding(ir, "value").bindingId);
expect(ir.closureCaptures).toContainEqual(
expect.objectContaining({
bindingId: reads[0]?.bindingId,
}),
);
});
it("keeps var loop declarations in the enclosing function", () => {
const ir = analyzeJavaScriptSemantics(`
function run() { for (var value of entries) { consume(value); } return value; }
`);
const binding = onlyBinding(ir, "value");
const callable = ir.callables.find(({ name }) => name === "run");
expect(binding.scopeId).toBe(callable?.bodyScopeId);
expect(
ir.references
.filter(({ name, role }) => name === "value" && role === "read")
.every(({ bindingId }) => bindingId === binding.bindingId),
).toBe(true);
});
});
describe("JavaScript semantic analysis: class expression lexical names", () => {
it("resolves named class expressions inside the class without changing the outer binding", () => {
const ir = analyzeJavaScriptSemantics(`

View File

@ -265,6 +265,12 @@ const scopeKind = (
if (t.isFunction(node)) return "function";
if (t.isClass(node)) return "class";
if (t.isCatchClause(node)) return "catch";
if (
t.isForStatement(node) ||
t.isForOfStatement(node) ||
t.isForInStatement(node)
)
return "block";
if (
t.isBlockStatement(node) &&
!(parent !== null && t.isFunction(parent) && parent.body === node)