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];