@angular-modernizer/api 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/investigation/call-graph-builder.d.ts +95 -8
  6. package/dist/investigation/call-graph-builder.d.ts.map +1 -1
  7. package/dist/investigation/call-graph-builder.js +424 -81
  8. package/dist/investigation/call-graph-builder.js.map +1 -1
  9. package/dist/investigation/codebase-searcher.d.ts +68 -0
  10. package/dist/investigation/codebase-searcher.d.ts.map +1 -1
  11. package/dist/investigation/codebase-searcher.js +154 -10
  12. package/dist/investigation/codebase-searcher.js.map +1 -1
  13. package/dist/investigation/template-usage-finder.d.ts +64 -0
  14. package/dist/investigation/template-usage-finder.d.ts.map +1 -0
  15. package/dist/investigation/template-usage-finder.js +279 -0
  16. package/dist/investigation/template-usage-finder.js.map +1 -0
  17. package/dist/investigation/template-usage-scanner.d.ts +69 -0
  18. package/dist/investigation/template-usage-scanner.d.ts.map +1 -0
  19. package/dist/investigation/template-usage-scanner.js +375 -0
  20. package/dist/investigation/template-usage-scanner.js.map +1 -0
  21. package/dist/investigation/usage-finder.d.ts +51 -5
  22. package/dist/investigation/usage-finder.d.ts.map +1 -1
  23. package/dist/investigation/usage-finder.js +129 -36
  24. package/dist/investigation/usage-finder.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/index.ts +16 -0
  27. package/src/investigation/call-graph-builder.ts +528 -103
  28. package/src/investigation/codebase-searcher.ts +240 -11
  29. package/src/investigation/template-usage-finder.ts +389 -0
  30. package/src/investigation/template-usage-scanner.ts +529 -0
  31. package/src/investigation/usage-finder.ts +194 -54
@@ -1,13 +1,36 @@
1
1
  /**
2
2
  * @angular-modernizer/api - Call Graph Builder
3
3
  *
4
- * Builds a call graph for a named method or function within a ts-morph Project.
4
+ * Builds a call graph for a named callable within a ts-morph Project.
5
5
  * Supports traversal upward (callers) and downward (callees) with configurable depth.
6
6
  *
7
+ * Supported targets (and graph nodes):
8
+ * - class methods and top-level functions
9
+ * - constructors (`'constructor'` or `'ClassName.constructor'`); a class
10
+ * without an explicit constructor is represented by its field initializers
11
+ * - get/set accessors
12
+ * - properties and top-level variables initialized with an arrow function or
13
+ * function expression
14
+ *
15
+ * Callees include:
16
+ * - call expressions (resolved via the type checker, name-based fallback
17
+ * only when the symbol cannot be resolved)
18
+ * - `new X()` (edge to X's constructor, including X's field initializers)
19
+ * - `super(...)` (edge to the base class constructor)
20
+ * - property reads/writes that resolve to a getter/setter
21
+ * - for constructors: instance field initializers (they run during
22
+ * construction)
23
+ *
24
+ * Callers are the enclosing callable of each reference: method, function,
25
+ * constructor, accessor, arrow-function property, or (for references in a
26
+ * field initializer) the owning class constructor. Callers of a constructor
27
+ * are the `new X()` sites and the constructors of subclasses.
28
+ *
7
29
  * @remarks
8
30
  * Used in bug investigation to understand how an error propagates through the
9
31
  * codebase. Combine with `find-usages` to trace the full call path from the
10
- * entry point to the failing code.
32
+ * entry point to the failing code, e.g. "what runs transitively while an
33
+ * interceptor is constructed" (ctor -> helper getter -> static config read).
11
34
  *
12
35
  * @example
13
36
  * ```typescript
@@ -18,15 +41,35 @@
18
41
  * });
19
42
  * // graph.callers → who calls getUser
20
43
  * // graph.callees → what getUser calls
44
+ *
45
+ * const ctorGraph = builder.buildCallGraph(
46
+ * 'AuthInterceptor.constructor',
47
+ * '/src/app/auth.interceptor.ts',
48
+ * { direction: 'down', depth: 3 },
49
+ * );
21
50
  * ```
22
51
  */
23
52
 
24
- import type { Project, MethodDeclaration, FunctionDeclaration } from 'ts-morph';
25
- import { SyntaxKind } from 'ts-morph';
53
+ import {
54
+ Node,
55
+ SyntaxKind,
56
+ type ClassDeclaration,
57
+ type Project,
58
+ } from 'ts-morph';
59
+
60
+ /** Kind of callable represented by a call graph node. */
61
+ export type CallableKind =
62
+ | 'method'
63
+ | 'function'
64
+ | 'constructor'
65
+ | 'getter'
66
+ | 'setter'
67
+ | 'property'
68
+ | 'variable';
26
69
 
27
70
  /** A node in the call graph tree. */
28
71
  export interface CallNode {
29
- /** Method or function name. */
72
+ /** Method or function name (`'constructor'` for constructors). */
30
73
  name: string;
31
74
 
32
75
  /** Absolute file path where this method is declared. */
@@ -35,6 +78,12 @@ export interface CallNode {
35
78
  /** 1-based line number of the declaration. */
36
79
  line: number;
37
80
 
81
+ /** Kind of callable (method, constructor, getter, ...). */
82
+ kind?: CallableKind;
83
+
84
+ /** Owning class name for class members. */
85
+ className?: string;
86
+
38
87
  /** Nested callers or callees (recursive to configured depth). */
39
88
  children: CallNode[];
40
89
  }
@@ -69,10 +118,24 @@ export interface CallGraphResult {
69
118
  callees: CallNode[];
70
119
  }
71
120
 
72
- type NamedCallable = MethodDeclaration | FunctionDeclaration;
121
+ /** Internal representation of anything that can run code. */
122
+ interface Callable {
123
+ /** Declaration node (class declaration for implicit constructors). */
124
+ node: Node;
125
+ name: string;
126
+ kind: CallableKind;
127
+ /** Nodes whose code runs when the callable runs. */
128
+ bodies: Node[];
129
+ /** Node used for reference lookup (callers). */
130
+ nameNode?: Node;
131
+ className?: string;
132
+ }
133
+
134
+ const CONSTRUCTOR_NAME = 'constructor';
73
135
 
74
136
  /**
75
- * Builds caller/callee graphs for methods and functions across a ts-morph Project.
137
+ * Builds caller/callee graphs for methods, functions, constructors,
138
+ * accessors and arrow-function properties across a ts-morph Project.
76
139
  */
77
140
  export class CallGraphBuilder {
78
141
  /**
@@ -81,10 +144,13 @@ export class CallGraphBuilder {
81
144
  constructor(private readonly project: Project) {}
82
145
 
83
146
  /**
84
- * Builds a call graph rooted at the named method or function.
147
+ * Builds a call graph rooted at the named callable.
85
148
  *
86
- * @param method - Name of the method or function to analyse.
87
- * @param file - Absolute path of the source file containing the method.
149
+ * @param method - Name of the callable to analyse. Accepts a method,
150
+ * function, accessor or arrow-function property name, `'constructor'`,
151
+ * or a class-qualified name such as `'AuthInterceptor.constructor'` or
152
+ * `'AuthInterceptor.headers'`.
153
+ * @param file - Absolute path of the source file containing the callable.
88
154
  * @param options - Traversal direction and depth. Defaults to `both` / depth `2`.
89
155
  * @returns A `CallGraphResult` with the target and its caller/callee trees.
90
156
  */
@@ -104,47 +170,214 @@ export class CallGraphBuilder {
104
170
  const targetInfo: Omit<CallNode, 'children'> = {
105
171
  name: method,
106
172
  file,
107
- line: target.getStartLineNumber(),
173
+ line: target.node.getStartLineNumber(),
174
+ kind: target.kind,
175
+ ...(target.className ? { className: target.className } : {}),
108
176
  };
109
177
 
178
+ const rootKey = this.keyOf(target);
179
+
110
180
  const callers =
111
181
  direction === 'down'
112
182
  ? []
113
- : this.buildCallers(target, depth, new Set([`${file}:${method}`]));
183
+ : this.buildCallers(target, depth, new Set([rootKey]));
114
184
 
115
185
  const callees =
116
186
  direction === 'up'
117
187
  ? []
118
- : this.buildCallees(target, depth, new Set([`${file}:${method}`]));
188
+ : this.buildCallees(target, depth, new Set([rootKey]));
119
189
 
120
190
  return { target: targetInfo, callers, callees };
121
191
  }
122
192
 
123
- private resolveTarget(name: string, file: string): NamedCallable | null {
193
+ // ---------------------------------------------------------------------------
194
+ // Target resolution
195
+ // ---------------------------------------------------------------------------
196
+
197
+ private resolveTarget(name: string, file: string): Callable | null {
124
198
  const sourceFile = this.project.getSourceFile(file);
125
199
  if (!sourceFile) {
126
200
  return null;
127
201
  }
128
202
 
129
- // Search class methods
130
- for (const cls of sourceFile.getClasses()) {
131
- const method = cls.getMethod(name);
132
- if (method) {
133
- return method;
203
+ const dotIndex = name.lastIndexOf('.');
204
+ const className = dotIndex > 0 ? name.slice(0, dotIndex) : undefined;
205
+ const memberName = dotIndex > 0 ? name.slice(dotIndex + 1) : name;
206
+
207
+ const classes = sourceFile
208
+ .getClasses()
209
+ .filter((cls) => !className || cls.getName() === className);
210
+
211
+ for (const cls of classes) {
212
+ const member = this.findClassMember(cls, memberName);
213
+ if (member) {
214
+ return member;
134
215
  }
135
216
  }
136
217
 
137
- // Search top-level functions
138
- const fn = sourceFile.getFunction(name);
218
+ if (className) {
219
+ return null;
220
+ }
221
+
222
+ const fn = sourceFile.getFunction(memberName);
139
223
  if (fn) {
140
- return fn;
224
+ return this.toCallable(fn);
225
+ }
226
+
227
+ const variable = sourceFile.getVariableDeclaration(memberName);
228
+ if (variable) {
229
+ return this.toCallable(variable);
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ private findClassMember(
236
+ cls: ClassDeclaration,
237
+ memberName: string,
238
+ ): Callable | null {
239
+ if (memberName === CONSTRUCTOR_NAME) {
240
+ return this.constructorOf(cls);
241
+ }
242
+
243
+ const candidates: (Node | undefined)[] = [
244
+ cls.getMethod(memberName),
245
+ cls.getGetAccessor(memberName),
246
+ cls.getSetAccessor(memberName),
247
+ cls.getProperty(memberName),
248
+ ];
249
+ for (const candidate of candidates) {
250
+ const callable = candidate ? this.toCallable(candidate) : null;
251
+ if (callable) {
252
+ return callable;
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Callable construction
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Converts a declaration node into a `Callable`, or `null` when the node
264
+ * cannot run code (e.g. a plain data property or an interface member).
265
+ */
266
+ private toCallable(node: Node): Callable | null {
267
+ if (Node.isConstructorDeclaration(node)) {
268
+ const cls = node.getParent();
269
+ return Node.isClassDeclaration(cls) ? this.constructorOf(cls) : null;
270
+ }
271
+
272
+ if (Node.isMethodDeclaration(node)) {
273
+ return {
274
+ node,
275
+ name: node.getName(),
276
+ kind: 'method',
277
+ bodies: this.compact([node.getBody()]),
278
+ nameNode: node.getNameNode(),
279
+ className: this.classNameOf(node),
280
+ };
281
+ }
282
+
283
+ if (Node.isFunctionDeclaration(node)) {
284
+ const name = node.getName();
285
+ if (!name) {
286
+ return null;
287
+ }
288
+ return {
289
+ node,
290
+ name,
291
+ kind: 'function',
292
+ bodies: this.compact([node.getBody()]),
293
+ nameNode: node.getNameNode(),
294
+ };
295
+ }
296
+
297
+ if (Node.isGetAccessorDeclaration(node) || Node.isSetAccessorDeclaration(node)) {
298
+ return {
299
+ node,
300
+ name: node.getName(),
301
+ kind: Node.isGetAccessorDeclaration(node) ? 'getter' : 'setter',
302
+ bodies: this.compact([node.getBody()]),
303
+ nameNode: node.getNameNode(),
304
+ className: this.classNameOf(node),
305
+ };
141
306
  }
142
307
 
308
+ if (Node.isPropertyDeclaration(node) || Node.isVariableDeclaration(node)) {
309
+ const fnBody = this.functionInitializerBody(node.getInitializer());
310
+ if (!fnBody) {
311
+ return null;
312
+ }
313
+ return {
314
+ node,
315
+ name: node.getName(),
316
+ kind: Node.isPropertyDeclaration(node) ? 'property' : 'variable',
317
+ bodies: [fnBody],
318
+ nameNode: node.getNameNode(),
319
+ className: Node.isPropertyDeclaration(node)
320
+ ? this.classNameOf(node)
321
+ : undefined,
322
+ };
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ /**
329
+ * Constructor callable for a class: the explicit constructor body (if any)
330
+ * plus all non-static, non-function field initializers.
331
+ */
332
+ private constructorOf(cls: ClassDeclaration): Callable {
333
+ const explicit = cls
334
+ .getConstructors()
335
+ .find((ctor) => ctor.getBody() !== undefined);
336
+ const fieldInitializers = cls
337
+ .getProperties()
338
+ .filter((prop) => !prop.isStatic())
339
+ .map((prop) => prop.getInitializer())
340
+ .filter(
341
+ (init): init is NonNullable<typeof init> =>
342
+ init !== undefined && !this.functionInitializerBody(init),
343
+ );
344
+
345
+ return {
346
+ node: explicit ?? cls,
347
+ name: CONSTRUCTOR_NAME,
348
+ kind: 'constructor',
349
+ bodies: [...this.compact([explicit?.getBody()]), ...fieldInitializers],
350
+ nameNode: cls.getNameNode(),
351
+ className: cls.getName(),
352
+ };
353
+ }
354
+
355
+ private functionInitializerBody(initializer: Node | undefined): Node | null {
356
+ if (
357
+ initializer &&
358
+ (Node.isArrowFunction(initializer) ||
359
+ Node.isFunctionExpression(initializer))
360
+ ) {
361
+ return initializer.getBody();
362
+ }
143
363
  return null;
144
364
  }
145
365
 
366
+ private classNameOf(node: Node): string | undefined {
367
+ const cls = node.getParent();
368
+ return Node.isClassDeclaration(cls) ? cls.getName() : undefined;
369
+ }
370
+
371
+ private compact(nodes: (Node | undefined)[]): Node[] {
372
+ return nodes.filter((n): n is Node => n !== undefined);
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // Callees
377
+ // ---------------------------------------------------------------------------
378
+
146
379
  private buildCallees(
147
- callable: NamedCallable,
380
+ callable: Callable,
148
381
  depth: number,
149
382
  visited: Set<string>,
150
383
  ): CallNode[] {
@@ -153,125 +386,302 @@ export class CallGraphBuilder {
153
386
  }
154
387
 
155
388
  const callees: CallNode[] = [];
156
- const body = callable.getBody();
157
- if (!body) {
158
- return [];
389
+ for (const target of this.collectCalleeCallables(callable)) {
390
+ const key = this.keyOf(target);
391
+ if (visited.has(key)) {
392
+ continue;
393
+ }
394
+ visited.add(key);
395
+
396
+ const children = this.buildCallees(target, depth - 1, new Set(visited));
397
+ callees.push(this.toCallNode(target, children));
159
398
  }
160
399
 
161
- body.forEachDescendant((node) => {
162
- if (node.getKind() !== SyntaxKind.CallExpression) {
163
- return;
164
- }
400
+ return this.deduplicateNodes(callees);
401
+ }
165
402
 
166
- const callExpr = node.asKindOrThrow(SyntaxKind.CallExpression);
167
- const expr = callExpr.getExpression();
168
- const calleeName = this.extractCallName(expr.getText());
169
- if (!calleeName) {
170
- return;
403
+ private collectCalleeCallables(callable: Callable): Callable[] {
404
+ const result: Callable[] = [];
405
+ for (const body of callable.bodies) {
406
+ for (const node of [body, ...body.getDescendants()]) {
407
+ result.push(...this.calleesAt(node));
171
408
  }
409
+ }
410
+ return result;
411
+ }
172
412
 
173
- const targetDecl = this.findDeclarationInProject(calleeName);
174
- if (!targetDecl) {
175
- return;
413
+ private calleesAt(node: Node): Callable[] {
414
+ if (Node.isCallExpression(node)) {
415
+ const expr = node.getExpression();
416
+ if (expr.getKind() === SyntaxKind.SuperKeyword) {
417
+ return this.superConstructor(node);
176
418
  }
419
+ return this.resolveCallExpressionTarget(expr);
420
+ }
177
421
 
178
- const targetFile = targetDecl.getSourceFile().getFilePath();
179
- const key = `${targetFile}:${calleeName}`;
180
- if (visited.has(key)) {
181
- return;
182
- }
183
- visited.add(key);
422
+ if (Node.isNewExpression(node)) {
423
+ return this.resolveNewExpressionTarget(node.getExpression());
424
+ }
184
425
 
185
- const children = this.buildCallees(
186
- targetDecl,
187
- depth - 1,
188
- new Set(visited),
189
- );
190
- callees.push({
191
- name: calleeName,
192
- file: targetFile,
193
- line: targetDecl.getStartLineNumber(),
194
- children,
195
- });
196
- });
426
+ if (Node.isPropertyAccessExpression(node)) {
427
+ return this.resolveAccessorTarget(node);
428
+ }
197
429
 
198
- return this.deduplicateNodes(callees);
430
+ return [];
199
431
  }
200
432
 
433
+ private resolveCallExpressionTarget(expr: Node): Callable[] {
434
+ const resolved = this.resolveSymbolDeclarations(expr);
435
+ if (resolved) {
436
+ return this.pickCallables(resolved);
437
+ }
438
+
439
+ // Fallback: name-based lookup when the checker cannot resolve the symbol
440
+ const calleeName = this.extractCallName(expr.getText());
441
+ if (!calleeName) {
442
+ return [];
443
+ }
444
+ const fallback = this.findDeclarationInProject(calleeName);
445
+ return fallback ? [fallback] : [];
446
+ }
447
+
448
+ private resolveNewExpressionTarget(expr: Node): Callable[] {
449
+ const declarations = this.resolveSymbolDeclarations(expr);
450
+ const classes = (declarations ?? []).filter((d) =>
451
+ Node.isClassDeclaration(d),
452
+ );
453
+ if (classes.length > 0) {
454
+ return classes.map((cls) => this.constructorOf(cls));
455
+ }
456
+ if (declarations) {
457
+ return [];
458
+ }
459
+
460
+ const className = this.extractCallName(expr.getText());
461
+ const cls = className ? this.findClassInProject(className) : null;
462
+ return cls ? [this.constructorOf(cls)] : [];
463
+ }
464
+
465
+ private resolveAccessorTarget(access: Node): Callable[] {
466
+ if (!Node.isPropertyAccessExpression(access)) {
467
+ return [];
468
+ }
469
+ const parent = access.getParent();
470
+ // Calls are handled by the CallExpression branch
471
+ if (Node.isCallExpression(parent) && parent.getExpression() === access) {
472
+ return [];
473
+ }
474
+
475
+ const declarations = this.resolveSymbolDeclarations(access);
476
+ if (!declarations) {
477
+ return [];
478
+ }
479
+
480
+ const isWrite =
481
+ Node.isBinaryExpression(parent) &&
482
+ parent.getLeft() === access &&
483
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken;
484
+
485
+ return declarations
486
+ .filter((d) =>
487
+ isWrite
488
+ ? Node.isSetAccessorDeclaration(d)
489
+ : Node.isGetAccessorDeclaration(d),
490
+ )
491
+ .map((d) => this.toCallable(d))
492
+ .filter((c): c is Callable => c !== null);
493
+ }
494
+
495
+ private superConstructor(call: Node): Callable[] {
496
+ const cls = call.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
497
+ const base = cls?.getBaseClass();
498
+ return base ? [this.constructorOf(base)] : [];
499
+ }
500
+
501
+ /**
502
+ * Resolves the declarations behind an expression via the type checker.
503
+ * Returns `undefined` when no symbol can be resolved (caller may fall back
504
+ * to name-based lookup) and an empty array when the symbol resolves to
505
+ * declarations that cannot be represented.
506
+ */
507
+ private resolveSymbolDeclarations(expr: Node): Node[] | undefined {
508
+ const target = Node.isPropertyAccessExpression(expr)
509
+ ? expr.getNameNode()
510
+ : expr;
511
+ let symbol = target.getSymbol();
512
+ if (!symbol) {
513
+ return undefined;
514
+ }
515
+ if (symbol.isAlias()) {
516
+ symbol = symbol.getAliasedSymbol() ?? symbol;
517
+ }
518
+ return symbol.getDeclarations();
519
+ }
520
+
521
+ /**
522
+ * Maps declarations to callables, preferring implementations (with body)
523
+ * over overload signatures and skipping ambient (.d.ts) declarations.
524
+ */
525
+ private pickCallables(declarations: Node[]): Callable[] {
526
+ const callables = declarations
527
+ .filter((d) => !d.getSourceFile().isDeclarationFile())
528
+ .map((d) => this.toCallable(d))
529
+ .filter((c): c is Callable => c !== null);
530
+ const withBody = callables.filter((c) => c.bodies.length > 0);
531
+ return withBody.length > 0 ? withBody : callables;
532
+ }
533
+
534
+ // ---------------------------------------------------------------------------
535
+ // Callers
536
+ // ---------------------------------------------------------------------------
537
+
201
538
  private buildCallers(
202
- callable: NamedCallable,
539
+ callable: Callable,
203
540
  depth: number,
204
541
  visited: Set<string>,
205
542
  ): CallNode[] {
206
- if (depth <= 0) {
543
+ if (depth <= 0 || !callable.nameNode) {
207
544
  return [];
208
545
  }
209
546
 
210
547
  const callers: CallNode[] = [];
211
- const name = callable.getName();
212
- if (!name) {
213
- return [];
548
+ for (const refNode of this.referenceNodes(callable)) {
549
+ const caller = this.callerForReference(callable, refNode);
550
+ if (!caller) {
551
+ continue;
552
+ }
553
+
554
+ const key = this.keyOf(caller);
555
+ if (visited.has(key)) {
556
+ continue;
557
+ }
558
+ visited.add(key);
559
+
560
+ const children = this.buildCallers(caller, depth - 1, new Set(visited));
561
+ callers.push(this.toCallNode(caller, children));
214
562
  }
215
563
 
216
- const refs = this.project.getLanguageService().findReferences(callable);
564
+ return this.deduplicateNodes(callers);
565
+ }
217
566
 
218
- for (const refSymbol of refs) {
219
- for (const ref of refSymbol.getReferences()) {
220
- if (ref.isDefinition()) {
221
- continue;
567
+ private referenceNodes(callable: Callable): Node[] {
568
+ if (!callable.nameNode) {
569
+ return [];
570
+ }
571
+ try {
572
+ const refs = this.project
573
+ .getLanguageService()
574
+ .findReferences(callable.nameNode);
575
+ const nodes: Node[] = [];
576
+ for (const refSymbol of refs) {
577
+ for (const ref of refSymbol.getReferences()) {
578
+ if (!ref.isDefinition()) {
579
+ nodes.push(ref.getNode());
580
+ }
222
581
  }
582
+ }
583
+ return nodes;
584
+ } catch {
585
+ return [];
586
+ }
587
+ }
223
588
 
224
- const refNode = ref.getNode();
225
- const parentMethod = refNode.getFirstAncestor(
226
- (n) =>
227
- n.getKind() === SyntaxKind.MethodDeclaration ||
228
- n.getKind() === SyntaxKind.FunctionDeclaration,
229
- ) as NamedCallable | undefined;
589
+ /**
590
+ * Maps a reference to the callable that performs the call. For
591
+ * constructor targets only `new X()` sites and subclass constructors
592
+ * (implicit or explicit `super()` calls) count.
593
+ */
594
+ private callerForReference(target: Callable, refNode: Node): Callable | null {
595
+ if (target.kind === 'constructor') {
596
+ const parent = refNode.getParent();
597
+ if (Node.isNewExpression(parent) && parent.getExpression() === refNode) {
598
+ return this.enclosingCallable(refNode);
599
+ }
600
+ const heritage = refNode.getFirstAncestorByKind(SyntaxKind.HeritageClause);
601
+ const subclass = heritage?.getParent();
602
+ if (
603
+ heritage?.getToken() === SyntaxKind.ExtendsKeyword &&
604
+ Node.isClassDeclaration(subclass)
605
+ ) {
606
+ return this.constructorOf(subclass);
607
+ }
608
+ return null;
609
+ }
610
+ return this.enclosingCallable(refNode);
611
+ }
230
612
 
231
- if (!parentMethod) {
232
- continue;
233
- }
613
+ /**
614
+ * Finds the callable that contains `node`. Anonymous callbacks are
615
+ * attributed to their enclosing named callable; references inside a
616
+ * non-function field initializer are attributed to the class constructor.
617
+ */
618
+ private enclosingCallable(node: Node): Callable | null {
619
+ for (
620
+ let current = node.getParent();
621
+ current !== undefined;
622
+ current = current.getParent()
623
+ ) {
624
+ if (
625
+ Node.isMethodDeclaration(current) ||
626
+ Node.isFunctionDeclaration(current) ||
627
+ Node.isConstructorDeclaration(current) ||
628
+ Node.isGetAccessorDeclaration(current) ||
629
+ Node.isSetAccessorDeclaration(current)
630
+ ) {
631
+ return this.toCallable(current);
632
+ }
234
633
 
235
- const callerName = parentMethod.getName();
236
- if (!callerName) {
237
- continue;
634
+ if (Node.isArrowFunction(current) || Node.isFunctionExpression(current)) {
635
+ const owner = current.getParent();
636
+ const isNamedOwner =
637
+ (Node.isPropertyDeclaration(owner) ||
638
+ Node.isVariableDeclaration(owner)) &&
639
+ owner.getInitializer() === current;
640
+ if (isNamedOwner) {
641
+ return this.toCallable(owner);
238
642
  }
643
+ continue;
644
+ }
239
645
 
240
- const callerFile = parentMethod.getSourceFile().getFilePath();
241
- const key = `${callerFile}:${callerName}`;
242
- if (visited.has(key)) {
243
- continue;
244
- }
245
- visited.add(key);
246
-
247
- const children = this.buildCallers(
248
- parentMethod,
249
- depth - 1,
250
- new Set(visited),
251
- );
252
- callers.push({
253
- name: callerName,
254
- file: callerFile,
255
- line: parentMethod.getStartLineNumber(),
256
- children,
257
- });
646
+ if (Node.isPropertyDeclaration(current)) {
647
+ const cls = current.getParent();
648
+ const runsInConstructor =
649
+ !current.isStatic() && Node.isClassDeclaration(cls);
650
+ return runsInConstructor ? this.constructorOf(cls) : null;
258
651
  }
259
- }
260
652
 
261
- return this.deduplicateNodes(callers);
653
+ if (Node.isClassDeclaration(current) || Node.isSourceFile(current)) {
654
+ return null;
655
+ }
656
+ }
657
+ return null;
262
658
  }
263
659
 
264
- private findDeclarationInProject(name: string): NamedCallable | null {
660
+ // ---------------------------------------------------------------------------
661
+ // Name-based fallback and helpers
662
+ // ---------------------------------------------------------------------------
663
+
664
+ private findDeclarationInProject(name: string): Callable | null {
265
665
  for (const sourceFile of this.project.getSourceFiles()) {
266
666
  for (const cls of sourceFile.getClasses()) {
267
667
  const method = cls.getMethod(name);
268
668
  if (method) {
269
- return method;
669
+ return this.toCallable(method);
270
670
  }
271
671
  }
272
672
  const fn = sourceFile.getFunction(name);
273
673
  if (fn) {
274
- return fn;
674
+ return this.toCallable(fn);
675
+ }
676
+ }
677
+ return null;
678
+ }
679
+
680
+ private findClassInProject(name: string): ClassDeclaration | null {
681
+ for (const sourceFile of this.project.getSourceFiles()) {
682
+ const cls = sourceFile.getClass(name);
683
+ if (cls) {
684
+ return cls;
275
685
  }
276
686
  }
277
687
  return null;
@@ -284,10 +694,25 @@ export class CallGraphBuilder {
284
694
  return last && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(last) ? last : null;
285
695
  }
286
696
 
697
+ private keyOf(callable: Callable): string {
698
+ return `${callable.node.getSourceFile().getFilePath()}:${callable.node.getStartLineNumber()}:${callable.name}`;
699
+ }
700
+
701
+ private toCallNode(callable: Callable, children: CallNode[]): CallNode {
702
+ return {
703
+ name: callable.name,
704
+ file: callable.node.getSourceFile().getFilePath(),
705
+ line: callable.node.getStartLineNumber(),
706
+ kind: callable.kind,
707
+ ...(callable.className ? { className: callable.className } : {}),
708
+ children,
709
+ };
710
+ }
711
+
287
712
  private deduplicateNodes(nodes: CallNode[]): CallNode[] {
288
713
  const seen = new Set<string>();
289
714
  return nodes.filter((n) => {
290
- const key = `${n.file}:${n.name}`;
715
+ const key = `${n.file}:${n.line}:${n.name}`;
291
716
  if (seen.has(key)) {
292
717
  return false;
293
718
  }