From 2bba39bc2b442074ef55893436df136c48d6ffe5 Mon Sep 17 00:00:00 2001 From: Kevin Joshua L Date: Fri, 16 Oct 2026 21:49:40 +0530 Subject: [PATCH] fix(domain): classify TypeScript parameter properties as declarations rather than identifier reads (#4465) * fix(domain): classify TypeScript parameter properties as declarations rather than identifier reads Recognize TSParameterProperty in isPatternOwnerDeclaration, isDeclarationIdentifier, and patternContains so parameter property definitions are not emitted as identifier reads, while retaining valid initializer reads. Fixes #4371 * test(domain): assert constructor body parameter property reads survive --- ...vascriptSemanticAnalysis.structure.test.ts | 38 +++++++++++++++++++ .../javascriptSemanticProjection.ts | 5 +++ 2 files changed, 43 insertions(+) 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) &&