diff --git a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts index b15c1a6814..0ffd9cf11f 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.structure.test.ts @@ -533,6 +533,44 @@ describe("JavaScript semantic analysis: write references", () => { "read", // const observed = total ]); }); + + it("treats TypeScript parameter-property declarations as bindings rather than reads", () => { + const ir = analyzeJavaScriptSemantics(` + const seed = 1; + class Service { + constructor( + public x: number, + public y = 1, + public z = seed, + ) { + console.log(this.x); + console.log(y); + } + } + `); + const referencesFor = (name: string) => + ir.references.filter((reference) => reference.name === name); + + // Parameter properties are definitions, not source identifier reads. + expect(referencesFor("x")).toEqual([]); + expect(referencesFor("z")).toEqual([]); + + // Actual reads in constructor body survive. + expect(referencesFor("y")).toEqual([ + expect.objectContaining({ + name: "y", + role: "read", + }), + ]); + + // Actual reads in parameter default values remain references. + expect(referencesFor("seed")).toEqual([ + expect.objectContaining({ + name: "seed", + role: "read", + }), + ]); + }); }); describe("JavaScript semantic analysis: dynamic and case scopes", () => { diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index 27f2fd36c7..b26aa5124e 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -670,6 +670,7 @@ const isDeleteSite = (site: t.Node): boolean => t.isUnaryExpression(site, { operator: "delete" }); const isPatternOwnerDeclaration = (node: t.Node): boolean => + t.isTSParameterProperty(node) || (t.isVariableDeclarator(node) && t.isPattern(node.id)) || (t.isFunction(node) && node.params.some((value): value is t.Pattern => t.isPattern(value))) || @@ -699,6 +700,8 @@ const isDeclarationIdentifier = ( ): boolean => { if (t.isTSImportEqualsDeclaration(parent)) return parent.id === node; return ( + (t.isTSParameterProperty(parent) && + patternContains(parent.parameter, node)) || (t.isVariableDeclarator(parent) && patternContains(parent.id, node)) || ((t.isFunctionDeclaration(parent) || t.isFunctionExpression(parent)) && parent.id === node) || @@ -747,6 +750,8 @@ const patternNames = (pattern: t.Node): string[] => { const patternContains = (pattern: t.Node, target: t.Identifier): boolean => pattern === target || + (t.isTSParameterProperty(pattern) && + patternContains(pattern.parameter, target)) || (t.isAssignmentPattern(pattern) && patternContains(pattern.left, target)) || (t.isRestElement(pattern) && patternContains(pattern.argument, target)) || (t.isObjectPattern(pattern) &&