@@ -40,6 +40,14 @@ function getEnclosingFunctionLike(
4040 return undefined ;
4141}
4242
43+ function propertyNameText ( name : ts . PropertyName ) : string | undefined {
44+ if ( ts . isIdentifier ( name ) || ts . isPrivateIdentifier ( name ) ) return name . text ;
45+ if ( ts . isStringLiteralLike ( name ) || ts . isNumericLiteral ( name ) ) {
46+ return name . text ;
47+ }
48+ return undefined ;
49+ }
50+
4351export class GraphBuilder {
4452 private readonly checker : ts . TypeChecker ;
4553 private readonly nodes = new Map < string , GraphNodeMutable > ( ) ;
@@ -111,6 +119,83 @@ export class GraphBuilder {
111119 return id ;
112120 }
113121
122+ private ensurePropertyNamedNode (
123+ name : ts . PropertyName ,
124+ discriminator = "" ,
125+ ) : string | undefined {
126+ const text = propertyNameText ( name ) ;
127+ if ( ! text ) return undefined ;
128+ const sf = name . getSourceFile ( ) ;
129+ if ( ! this . isUserSourceFile ( sf ) ) return undefined ;
130+ const filePath = this . rel ( sf ) ;
131+ const start = name . getStart ( sf , false ) ;
132+ const { line, character } = sf . getLineAndCharacterOfPosition ( start ) ;
133+ const line1 = line + 1 ;
134+ const col1 = character + 1 ;
135+ const id = makeNodeId ( filePath , line1 , col1 , text , discriminator ) ;
136+ if ( ! this . nodes . has ( id ) ) {
137+ this . nodes . set ( id , {
138+ id,
139+ filePath,
140+ line : line1 ,
141+ column : col1 ,
142+ name : text ,
143+ kind : "property" ,
144+ typeString : this . typeStringAt ( name ) ,
145+ isSource : false ,
146+ infectedBy : new Set ( ) ,
147+ } ) ;
148+ }
149+ return id ;
150+ }
151+
152+ private declarationForSymbol (
153+ sym : ts . Symbol | undefined ,
154+ ) : ts . Declaration | undefined {
155+ return (
156+ sym ?. declarations ?. find (
157+ ( decl ) =>
158+ ts . isImportClause ( decl ) ||
159+ ts . isImportSpecifier ( decl ) ||
160+ ts . isNamespaceImport ( decl ) ||
161+ ts . isImportEqualsDeclaration ( decl ) ,
162+ ) ?? sym ?. valueDeclaration
163+ ) ;
164+ }
165+
166+ private propertySymbolForElementAccess (
167+ expr : ts . ElementAccessExpression ,
168+ ) : ts . Symbol | undefined {
169+ const arg = expr . argumentExpression ;
170+ if ( ! arg || ( ! ts . isStringLiteralLike ( arg ) && ! ts . isNumericLiteral ( arg ) ) ) {
171+ return undefined ;
172+ }
173+ const key = arg . text ;
174+ const baseType = this . checker . getTypeAtLocation ( expr . expression ) ;
175+ const apparent = this . checker . getApparentType ( baseType ) ;
176+ return (
177+ this . checker . getPropertyOfType ( apparent , key ) ??
178+ this . checker . getPropertyOfType ( baseType , key )
179+ ) ;
180+ }
181+
182+ private reasonForValueRead ( expr : ts . Expression ) : EdgeReason {
183+ if ( ts . isParenthesizedExpression ( expr ) ) {
184+ return this . reasonForValueRead ( expr . expression ) ;
185+ }
186+ if (
187+ ts . isAsExpression ( expr ) ||
188+ ts . isTypeAssertionExpression ( expr ) ||
189+ ts . isSatisfiesExpression ( expr ) ||
190+ ts . isNonNullExpression ( expr )
191+ ) {
192+ return this . reasonForValueRead ( expr . expression ) ;
193+ }
194+ if ( ts . isPropertyAccessExpression ( expr ) ) return "property-access" ;
195+ if ( ts . isElementAccessExpression ( expr ) ) return "index-access" ;
196+ return "assignment" ;
197+ }
198+
114199 private ensureFromValueDeclaration ( vd : ts . Declaration ) : string | undefined {
115200 if ( ts . isImportClause ( vd ) && vd . name ) {
116201 return this . ensureImportBinding ( vd . name ) ;
@@ -136,6 +221,15 @@ export class GraphBuilder {
136221 if ( ts . isPropertyDeclaration ( vd ) && ts . isIdentifier ( vd . name ) ) {
137222 return this . ensureNamedDecl ( vd , "property" ) ;
138223 }
224+ if ( ts . isGetAccessorDeclaration ( vd ) ) {
225+ return this . ensurePropertyNamedNode ( vd . name , "getter" ) ;
226+ }
227+ if ( ts . isPropertyAssignment ( vd ) ) {
228+ return this . ensurePropertyNamedNode ( vd . name , "object" ) ;
229+ }
230+ if ( ts . isShorthandPropertyAssignment ( vd ) ) {
231+ return this . ensurePropertyNamedNode ( vd . name , "object" ) ;
232+ }
139233 return undefined ;
140234 }
141235
@@ -278,16 +372,32 @@ export class GraphBuilder {
278372
279373 /** Value-flow sources: identifiers / simple references with a registered declaration. */
280374 exprToNodeId ( expr : ts . Expression ) : string | undefined {
281- if ( ! ts . isIdentifier ( expr ) ) return undefined ;
282- const sym = this . checker . getSymbolAtLocation ( expr ) ;
283- const vd =
284- sym ?. declarations ?. find (
285- ( decl ) =>
286- ts . isImportClause ( decl ) ||
287- ts . isImportSpecifier ( decl ) ||
288- ts . isNamespaceImport ( decl ) ||
289- ts . isImportEqualsDeclaration ( decl ) ,
290- ) ?? sym ?. valueDeclaration ;
375+ if ( ts . isParenthesizedExpression ( expr ) ) {
376+ return this . exprToNodeId ( expr . expression ) ;
377+ }
378+ if (
379+ ts . isAsExpression ( expr ) ||
380+ ts . isTypeAssertionExpression ( expr ) ||
381+ ts . isSatisfiesExpression ( expr ) ||
382+ ts . isNonNullExpression ( expr )
383+ ) {
384+ return this . exprToNodeId ( expr . expression ) ;
385+ }
386+
387+ let sym : ts . Symbol | undefined ;
388+ if ( ts . isIdentifier ( expr ) ) {
389+ sym = this . checker . getSymbolAtLocation ( expr ) ;
390+ } else if ( ts . isPropertyAccessExpression ( expr ) ) {
391+ sym =
392+ this . checker . getSymbolAtLocation ( expr . name ) ??
393+ this . checker . getSymbolAtLocation ( expr ) ;
394+ } else if ( ts . isElementAccessExpression ( expr ) ) {
395+ sym = this . propertySymbolForElementAccess ( expr ) ;
396+ } else {
397+ return undefined ;
398+ }
399+
400+ const vd = this . declarationForSymbol ( sym ) ;
291401 if ( ! vd ) return undefined ;
292402 return this . ensureFromValueDeclaration ( vd ) ;
293403 }
@@ -370,6 +480,20 @@ export class GraphBuilder {
370480 if ( ts . isSpreadAssignment ( p ) ) {
371481 const sid = this . exprToNodeId ( p . expression ) ;
372482 if ( sid ) this . addEdge ( sid , lhs , "spread" ) ;
483+ continue ;
484+ }
485+ if ( ts . isPropertyAssignment ( p ) ) {
486+ const pid = this . ensureFromValueDeclaration ( p ) ;
487+ const rhs = this . exprToNodeId ( p . initializer ) ;
488+ if ( pid && rhs ) {
489+ this . addEdge ( rhs , pid , this . reasonForValueRead ( p . initializer ) ) ;
490+ }
491+ continue ;
492+ }
493+ if ( ts . isShorthandPropertyAssignment ( p ) ) {
494+ const pid = this . ensureFromValueDeclaration ( p ) ;
495+ const rhs = this . exprToNodeId ( p . name ) ;
496+ if ( pid && rhs ) this . addEdge ( rhs , pid , "assignment" ) ;
373497 }
374498 }
375499 }
@@ -382,13 +506,16 @@ export class GraphBuilder {
382506 }
383507 if (
384508 ts . isBinaryExpression ( e ) &&
385- e . operatorToken . kind === ts . SyntaxKind . EqualsToken &&
386- ts . isCallExpression ( e . right )
509+ e . operatorToken . kind === ts . SyntaxKind . EqualsToken
387510 ) {
388- const lhsId = ts . isIdentifier ( e . left )
389- ? this . exprToNodeId ( e . left )
390- : undefined ;
391- this . edgesFromCall ( e . right , lhsId ) ;
511+ const lhsId = this . exprToNodeId ( e . left ) ;
512+ if ( ! lhsId ) return ;
513+ if ( ts . isCallExpression ( e . right ) ) {
514+ this . edgesFromCall ( e . right , lhsId ) ;
515+ return ;
516+ }
517+ const rhsId = this . exprToNodeId ( e . right ) ;
518+ if ( rhsId ) this . addEdge ( rhsId , lhsId , this . reasonForValueRead ( e . right ) ) ;
392519 }
393520 }
394521
@@ -407,6 +534,11 @@ export class GraphBuilder {
407534 this . edgesFromCall ( init , lhs ) ;
408535 return ;
409536 }
537+ const rhs = this . exprToNodeId ( init ) ;
538+ if ( rhs ) {
539+ this . addEdge ( rhs , lhs , this . reasonForValueRead ( init ) ) ;
540+ return ;
541+ }
410542 if ( ts . isObjectLiteralExpression ( init ) ) {
411543 this . edgesFromObjectLiteral ( init , node ) ;
412544 }
@@ -427,7 +559,9 @@ export class GraphBuilder {
427559 if ( ! fn ) return ;
428560 const retId = this . ensureReturnNode ( fn ) ;
429561 const exId = this . exprToNodeId ( node . expression ) ;
430- if ( retId && exId ) this . addEdge ( exId , retId , "assignment" ) ;
562+ if ( retId && exId ) {
563+ this . addEdge ( exId , retId , this . reasonForValueRead ( node . expression ) ) ;
564+ }
431565 }
432566
433567 private visitPropertyDeclaration ( node : ts . PropertyDeclaration ) : void {
0 commit comments