@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.
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/investigation/call-graph-builder.d.ts +95 -8
- package/dist/investigation/call-graph-builder.d.ts.map +1 -1
- package/dist/investigation/call-graph-builder.js +424 -81
- package/dist/investigation/call-graph-builder.js.map +1 -1
- package/dist/investigation/codebase-searcher.d.ts +68 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -1
- package/dist/investigation/codebase-searcher.js +154 -10
- package/dist/investigation/codebase-searcher.js.map +1 -1
- package/dist/investigation/template-usage-finder.d.ts +64 -0
- package/dist/investigation/template-usage-finder.d.ts.map +1 -0
- package/dist/investigation/template-usage-finder.js +279 -0
- package/dist/investigation/template-usage-finder.js.map +1 -0
- package/dist/investigation/template-usage-scanner.d.ts +69 -0
- package/dist/investigation/template-usage-scanner.d.ts.map +1 -0
- package/dist/investigation/template-usage-scanner.js +375 -0
- package/dist/investigation/template-usage-scanner.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +51 -5
- package/dist/investigation/usage-finder.d.ts.map +1 -1
- package/dist/investigation/usage-finder.js +129 -36
- package/dist/investigation/usage-finder.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +16 -0
- package/src/investigation/call-graph-builder.ts +528 -103
- package/src/investigation/codebase-searcher.ts +240 -11
- package/src/investigation/template-usage-finder.ts +389 -0
- package/src/investigation/template-usage-scanner.ts +529 -0
- 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
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
147
|
+
* Builds a call graph rooted at the named callable.
|
|
85
148
|
*
|
|
86
|
-
* @param method - Name of the
|
|
87
|
-
*
|
|
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([
|
|
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([
|
|
188
|
+
: this.buildCallees(target, depth, new Set([rootKey]));
|
|
119
189
|
|
|
120
190
|
return { target: targetInfo, callers, callees };
|
|
121
191
|
}
|
|
122
192
|
|
|
123
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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:
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
400
|
+
return this.deduplicateNodes(callees);
|
|
401
|
+
}
|
|
165
402
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
visited.add(key);
|
|
422
|
+
if (Node.isNewExpression(node)) {
|
|
423
|
+
return this.resolveNewExpressionTarget(node.getExpression());
|
|
424
|
+
}
|
|
184
425
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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:
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
564
|
+
return this.deduplicateNodes(callers);
|
|
565
|
+
}
|
|
217
566
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
653
|
+
if (Node.isClassDeclaration(current) || Node.isSourceFile(current)) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
262
658
|
}
|
|
263
659
|
|
|
264
|
-
|
|
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
|
}
|