fix(javascript): bind named class expressions in their class scope (#510)

* fix(javascript): bind named class expressions in their class scope

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

* test(javascript): isolate class expression lexical names regression cases

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

---------

Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-05 02:59:01 -04:00 committed by GitHub
parent 488fd5f60b
commit 55f0267c40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 0 deletions

View File

@ -166,6 +166,50 @@ describe("JavaScript semantic analysis: structure 1", () => {
});
});
describe("JavaScript semantic analysis: class expression lexical names", () => {
it("resolves named class expressions inside the class without changing the outer binding", () => {
const ir = analyzeJavaScriptSemantics(`
const Service = "outside";
const Wrapper = class Service {
create() { return new Service(); }
};
consume(Service);
`);
expect(bindingsNamed(ir, "Service")).toHaveLength(2);
expect(topLevelBinding(ir, "Service").value).toEqual({
status: "literal",
value: "outside",
});
const references = ir.references.filter(
({ name, role }) => name === "Service" && role === "read",
);
expect(references).toHaveLength(2);
expect(references[0]?.bindingId).not.toBe(references[1]?.bindingId);
expect(references[0]?.resolution).toBe("resolved");
const classScope = ir.scopes.find(({ kind }) => kind === "class");
expect(
bindingsNamed(ir, "Service").some(
({ scopeId }) => scopeId === classScope?.scopeId,
),
).toBe(true);
});
it("does not expose a named class expression outside its class", () => {
const ir = analyzeJavaScriptSemantics(`
const Wrapper = class Internal { create() { return new Internal(); } };
consume(Internal);
`);
const references = ir.references.filter(
({ name, role }) => name === "Internal" && role === "read",
);
expect(references.map(({ resolution }) => resolution)).toEqual([
"resolved",
"unbound",
]);
expect(topLevelBinding(ir, "Wrapper").provenance.status).toBe("local");
});
});
describe("JavaScript semantic analysis: structure 2", () => {
it("retains ESM, re-export, require, and CommonJS export relationships", () => {
const ir = analyzeJavaScriptSemantics(`

View File

@ -213,6 +213,16 @@ const bindInnerDeclaration = (
? parent.kind !== "const"
: true,
});
else if (t.isClassExpression(node) && t.isIdentifier(node.id))
addBinding({
state,
scope,
name: node.id.name,
kind: "class",
mutable: false,
definitionNode: node.id,
initializer: node,
});
else if (t.isFunction(node)) bindFunctionLocals(node, scope, state);
else if (t.isCatchClause(node) && node.param != null)
bindPattern({