From c0b3cd14b1fd2367c8f8c115a070c4759e67b7ad Mon Sep 17 00:00:00 2001 From: Kshitiz Jain <118300211+kshitizj03@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:41:19 +0530 Subject: [PATCH] fix(javascript): exclude class member keys from lexical reads Classify noncomputed class member keys and private names at the shared reference owner, avoiding false reads and closure captures. Preserve computed keys and initializer/body reads. Validation: regression controls, fresh public CLI/MCP workflows and complete exact-head CI passed. Fixes #3310. --- ...vascriptSemanticAnalysis.structure.test.ts | 68 +++++++++++++++++++ .../javascriptSemanticProjection.ts | 13 ++++ 2 files changed, 81 insertions(+) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts index 0ffd9cf11f..5024be3a01 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts @@ -291,6 +291,74 @@ describe("JavaScript semantic analysis: class expression lexical names", () => { }); }); +describe("JavaScript semantic analysis: class member keys", () => { + it("does not read an outer binding through noncomputed class member names", () => { + const ir = analyzeJavaScriptSemantics(` + const marker = "seed"; + class Holder { + marker() {} + static marker() {} + get marker() { return 1; } + set marker(value) {} + marker = 1; + static marker = 2; + #marker = 3; + #read() { return this.#marker; } + } + `); + expect(ir.references.filter(({ name }) => name === "marker")).toEqual([]); + }); + + it("does not read an outer binding through TypeScript class member names", () => { + const ir = analyzeJavaScriptSemantics(` + const marker = "seed"; + abstract class Holder { + declare marker: string; + abstract marker(): void; + abstract [marker](): void; + } + class Overloads { + marker(): void; + marker(value?: number) {} + } + `); + // Only the computed abstract key evaluates the outer binding. + expect( + ir.references + .filter(({ name }) => name === "marker") + .map(({ role, location }) => `${role}@${location.start.line}`), + ).toEqual(["read@6"]); + }); + + it("does not capture an outer binding through a noncomputed method name", () => { + const ir = analyzeJavaScriptSemantics(` + function outer() { + const marker = 1; + class Holder { marker() { return 2; } } + return Holder; + } + `); + expect(ir.closureCaptures).toEqual([]); + }); + + it("keeps reads from computed class keys and field initializers", () => { + const ir = analyzeJavaScriptSemantics(` + const marker = "seed"; + class Holder { + [marker]() {} + field = marker; + } + `); + const reads = ir.references.filter( + ({ name, role }) => name === "marker" && role === "read", + ); + expect(reads.map(({ resolution }) => resolution)).toEqual([ + "resolved", + "resolved", + ]); + }); +}); + describe("JavaScript semantic analysis: structure 2", () => { it("retains ESM, re-export, require, and CommonJS export relationships", () => { const ir = analyzeJavaScriptSemantics(` diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index b26aa5124e..3099aadb06 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -725,11 +725,24 @@ const isNonReferenceKey = (node: t.Identifier, parent: t.Node): boolean => parent.key === node && !parent.computed && !(t.isObjectProperty(parent) && parent.shorthand)) || + isNoncomputedClassMemberKey(node, parent) || + t.isPrivateName(parent) || (t.isExportSpecifier(parent) && parent.exported === node) || t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent); +/** A noncomputed class member name is a property key, never a lexical read. */ +const isNoncomputedClassMemberKey = ( + node: t.Identifier, + parent: t.Node, +): boolean => + (t.isClassMethod(parent) || + t.isClassProperty(parent) || + t.isTSDeclareMethod(parent)) && + parent.key === node && + !parent.computed; + const patternNames = (pattern: t.Node): string[] => { if (t.isTSParameterProperty(pattern)) return patternNames(pattern.parameter); if (t.isIdentifier(pattern)) return [pattern.name];