@angular-modernizer/api 0.1.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/LICENSE +21 -0
- package/README.md +507 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
- package/dist/analysis/complexity-metrics-calculator.js +195 -0
- package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
- package/dist/analysis/dependency-analyzer.d.ts +227 -0
- package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
- package/dist/analysis/dependency-analyzer.js +451 -0
- package/dist/analysis/dependency-analyzer.js.map +1 -0
- package/dist/analysis/external-template-loader.d.ts +44 -0
- package/dist/analysis/external-template-loader.d.ts.map +1 -0
- package/dist/analysis/external-template-loader.js +69 -0
- package/dist/analysis/external-template-loader.js.map +1 -0
- package/dist/analysis/selector-mapper.d.ts +77 -0
- package/dist/analysis/selector-mapper.d.ts.map +1 -0
- package/dist/analysis/selector-mapper.js +146 -0
- package/dist/analysis/selector-mapper.js.map +1 -0
- package/dist/analysis/service-detector.d.ts +164 -0
- package/dist/analysis/service-detector.d.ts.map +1 -0
- package/dist/analysis/service-detector.js +231 -0
- package/dist/analysis/service-detector.js.map +1 -0
- package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
- package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
- package/dist/analysis/service-pattern-recognizer.js +444 -0
- package/dist/analysis/service-pattern-recognizer.js.map +1 -0
- package/dist/analysis/symbol-locator.d.ts +81 -0
- package/dist/analysis/symbol-locator.d.ts.map +1 -0
- package/dist/analysis/symbol-locator.js +128 -0
- package/dist/analysis/symbol-locator.js.map +1 -0
- package/dist/analysis/template-analyzer-usage.d.ts +144 -0
- package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
- package/dist/analysis/template-analyzer-usage.js +474 -0
- package/dist/analysis/template-analyzer-usage.js.map +1 -0
- package/dist/analysis/template-analyzer.d.ts +80 -0
- package/dist/analysis/template-analyzer.d.ts.map +1 -0
- package/dist/analysis/template-analyzer.js +146 -0
- package/dist/analysis/template-analyzer.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation/angular-symbol-finder.d.ts +76 -0
- package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
- package/dist/investigation/angular-symbol-finder.js +199 -0
- package/dist/investigation/angular-symbol-finder.js.map +1 -0
- package/dist/investigation/call-graph-builder.d.ts +85 -0
- package/dist/investigation/call-graph-builder.d.ts.map +1 -0
- package/dist/investigation/call-graph-builder.js +196 -0
- package/dist/investigation/call-graph-builder.js.map +1 -0
- package/dist/investigation/codebase-searcher.d.ts +99 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -0
- package/dist/investigation/codebase-searcher.js +201 -0
- package/dist/investigation/codebase-searcher.js.map +1 -0
- package/dist/investigation/stack-trace-parser.d.ts +70 -0
- package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
- package/dist/investigation/stack-trace-parser.js +117 -0
- package/dist/investigation/stack-trace-parser.js.map +1 -0
- package/dist/investigation/type-resolver.d.ts +59 -0
- package/dist/investigation/type-resolver.d.ts.map +1 -0
- package/dist/investigation/type-resolver.js +111 -0
- package/dist/investigation/type-resolver.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +69 -0
- package/dist/investigation/usage-finder.d.ts.map +1 -0
- package/dist/investigation/usage-finder.js +132 -0
- package/dist/investigation/usage-finder.js.map +1 -0
- package/dist/metrics/complexity-metrics.d.ts +196 -0
- package/dist/metrics/complexity-metrics.d.ts.map +1 -0
- package/dist/metrics/complexity-metrics.js +445 -0
- package/dist/metrics/complexity-metrics.js.map +1 -0
- package/dist/public-api.d.ts +77 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +50 -0
- package/dist/public-api.js.map +1 -0
- package/dist/transformation/import-manager.d.ts +276 -0
- package/dist/transformation/import-manager.d.ts.map +1 -0
- package/dist/transformation/import-manager.js +488 -0
- package/dist/transformation/import-manager.js.map +1 -0
- package/dist/transformation/ng-morph-adapter.d.ts +111 -0
- package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
- package/dist/transformation/ng-morph-adapter.js +179 -0
- package/dist/transformation/ng-morph-adapter.js.map +1 -0
- package/dist/transformation/ngmodule-manager.d.ts +145 -0
- package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
- package/dist/transformation/ngmodule-manager.js +356 -0
- package/dist/transformation/ngmodule-manager.js.map +1 -0
- package/package.json +62 -0
- package/src/analysis/complexity-metrics-calculator.ts +253 -0
- package/src/analysis/dependency-analyzer.ts +720 -0
- package/src/analysis/external-template-loader.ts +79 -0
- package/src/analysis/selector-mapper.ts +186 -0
- package/src/analysis/service-detector.ts +280 -0
- package/src/analysis/service-pattern-recognizer.ts +545 -0
- package/src/analysis/symbol-locator.ts +170 -0
- package/src/analysis/template-analyzer-usage.ts +614 -0
- package/src/analysis/template-analyzer.ts +208 -0
- package/src/index.ts +51 -0
- package/src/investigation/angular-symbol-finder.ts +263 -0
- package/src/investigation/call-graph-builder.ts +298 -0
- package/src/investigation/codebase-searcher.ts +286 -0
- package/src/investigation/stack-trace-parser.ts +166 -0
- package/src/investigation/type-resolver.ts +144 -0
- package/src/investigation/usage-finder.ts +198 -0
- package/src/metrics/complexity-metrics.ts +617 -0
- package/src/public-api.ts +119 -0
- package/src/transformation/import-manager.ts +626 -0
- package/src/transformation/ng-morph-adapter.ts +252 -0
- package/src/transformation/ngmodule-manager.ts +450 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Call Graph Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds a call graph for a named method or function within a ts-morph Project.
|
|
5
|
+
* Supports traversal upward (callers) and downward (callees) with configurable depth.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Used in bug investigation to understand how an error propagates through the
|
|
9
|
+
* codebase. Combine with `find-usages` to trace the full call path from the
|
|
10
|
+
* entry point to the failing code.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const builder = new CallGraphBuilder(project);
|
|
15
|
+
* const graph = builder.buildCallGraph('getUser', '/src/app/user.service.ts', {
|
|
16
|
+
* direction: 'both',
|
|
17
|
+
* depth: 2,
|
|
18
|
+
* });
|
|
19
|
+
* // graph.callers → who calls getUser
|
|
20
|
+
* // graph.callees → what getUser calls
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Project, MethodDeclaration, FunctionDeclaration } from 'ts-morph';
|
|
25
|
+
import { SyntaxKind } from 'ts-morph';
|
|
26
|
+
|
|
27
|
+
/** A node in the call graph tree. */
|
|
28
|
+
export interface CallNode {
|
|
29
|
+
/** Method or function name. */
|
|
30
|
+
name: string;
|
|
31
|
+
|
|
32
|
+
/** Absolute file path where this method is declared. */
|
|
33
|
+
file: string;
|
|
34
|
+
|
|
35
|
+
/** 1-based line number of the declaration. */
|
|
36
|
+
line: number;
|
|
37
|
+
|
|
38
|
+
/** Nested callers or callees (recursive to configured depth). */
|
|
39
|
+
children: CallNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Options for `buildCallGraph`. */
|
|
43
|
+
export interface CallGraphOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Traversal direction.
|
|
46
|
+
* - `'up'` — callers only (who calls the target)
|
|
47
|
+
* - `'down'` — callees only (what the target calls)
|
|
48
|
+
* - `'both'` — both directions
|
|
49
|
+
* @default 'both'
|
|
50
|
+
*/
|
|
51
|
+
direction?: 'up' | 'down' | 'both';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* How many levels deep to traverse.
|
|
55
|
+
* @default 2
|
|
56
|
+
*/
|
|
57
|
+
depth?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Result of `buildCallGraph`. */
|
|
61
|
+
export interface CallGraphResult {
|
|
62
|
+
/** The resolved target node, or `null` when not found. */
|
|
63
|
+
target: Omit<CallNode, 'children'> | null;
|
|
64
|
+
|
|
65
|
+
/** Nodes that call the target (populated for `'up'` and `'both'`). */
|
|
66
|
+
callers: CallNode[];
|
|
67
|
+
|
|
68
|
+
/** Nodes called by the target (populated for `'down'` and `'both'`). */
|
|
69
|
+
callees: CallNode[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type NamedCallable = MethodDeclaration | FunctionDeclaration;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds caller/callee graphs for methods and functions across a ts-morph Project.
|
|
76
|
+
*/
|
|
77
|
+
export class CallGraphBuilder {
|
|
78
|
+
/**
|
|
79
|
+
* @param project - A ts-morph `Project` instance with source files already added.
|
|
80
|
+
*/
|
|
81
|
+
constructor(private readonly project: Project) {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Builds a call graph rooted at the named method or function.
|
|
85
|
+
*
|
|
86
|
+
* @param method - Name of the method or function to analyse.
|
|
87
|
+
* @param file - Absolute path of the source file containing the method.
|
|
88
|
+
* @param options - Traversal direction and depth. Defaults to `both` / depth `2`.
|
|
89
|
+
* @returns A `CallGraphResult` with the target and its caller/callee trees.
|
|
90
|
+
*/
|
|
91
|
+
buildCallGraph(
|
|
92
|
+
method: string,
|
|
93
|
+
file: string,
|
|
94
|
+
options: CallGraphOptions = {},
|
|
95
|
+
): CallGraphResult {
|
|
96
|
+
const direction = options.direction ?? 'both';
|
|
97
|
+
const depth = options.depth ?? 2;
|
|
98
|
+
|
|
99
|
+
const target = this.resolveTarget(method, file);
|
|
100
|
+
if (!target) {
|
|
101
|
+
return { target: null, callers: [], callees: [] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const targetInfo: Omit<CallNode, 'children'> = {
|
|
105
|
+
name: method,
|
|
106
|
+
file,
|
|
107
|
+
line: target.getStartLineNumber(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const callers =
|
|
111
|
+
direction === 'down'
|
|
112
|
+
? []
|
|
113
|
+
: this.buildCallers(target, depth, new Set([`${file}:${method}`]));
|
|
114
|
+
|
|
115
|
+
const callees =
|
|
116
|
+
direction === 'up'
|
|
117
|
+
? []
|
|
118
|
+
: this.buildCallees(target, depth, new Set([`${file}:${method}`]));
|
|
119
|
+
|
|
120
|
+
return { target: targetInfo, callers, callees };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private resolveTarget(name: string, file: string): NamedCallable | null {
|
|
124
|
+
const sourceFile = this.project.getSourceFile(file);
|
|
125
|
+
if (!sourceFile) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Search class methods
|
|
130
|
+
for (const cls of sourceFile.getClasses()) {
|
|
131
|
+
const method = cls.getMethod(name);
|
|
132
|
+
if (method) {
|
|
133
|
+
return method;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Search top-level functions
|
|
138
|
+
const fn = sourceFile.getFunction(name);
|
|
139
|
+
if (fn) {
|
|
140
|
+
return fn;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private buildCallees(
|
|
147
|
+
callable: NamedCallable,
|
|
148
|
+
depth: number,
|
|
149
|
+
visited: Set<string>,
|
|
150
|
+
): CallNode[] {
|
|
151
|
+
if (depth <= 0) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const callees: CallNode[] = [];
|
|
156
|
+
const body = callable.getBody();
|
|
157
|
+
if (!body) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
body.forEachDescendant((node) => {
|
|
162
|
+
if (node.getKind() !== SyntaxKind.CallExpression) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const callExpr = node.asKindOrThrow(SyntaxKind.CallExpression);
|
|
167
|
+
const expr = callExpr.getExpression();
|
|
168
|
+
const calleeName = this.extractCallName(expr.getText());
|
|
169
|
+
if (!calleeName) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const targetDecl = this.findDeclarationInProject(calleeName);
|
|
174
|
+
if (!targetDecl) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const targetFile = targetDecl.getSourceFile().getFilePath();
|
|
179
|
+
const key = `${targetFile}:${calleeName}`;
|
|
180
|
+
if (visited.has(key)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
visited.add(key);
|
|
184
|
+
|
|
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
|
+
});
|
|
197
|
+
|
|
198
|
+
return this.deduplicateNodes(callees);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private buildCallers(
|
|
202
|
+
callable: NamedCallable,
|
|
203
|
+
depth: number,
|
|
204
|
+
visited: Set<string>,
|
|
205
|
+
): CallNode[] {
|
|
206
|
+
if (depth <= 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const callers: CallNode[] = [];
|
|
211
|
+
const name = callable.getName();
|
|
212
|
+
if (!name) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const refs = this.project.getLanguageService().findReferences(callable);
|
|
217
|
+
|
|
218
|
+
for (const refSymbol of refs) {
|
|
219
|
+
for (const ref of refSymbol.getReferences()) {
|
|
220
|
+
if (ref.isDefinition()) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
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;
|
|
230
|
+
|
|
231
|
+
if (!parentMethod) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const callerName = parentMethod.getName();
|
|
236
|
+
if (!callerName) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
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
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return this.deduplicateNodes(callers);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private findDeclarationInProject(name: string): NamedCallable | null {
|
|
265
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
266
|
+
for (const cls of sourceFile.getClasses()) {
|
|
267
|
+
const method = cls.getMethod(name);
|
|
268
|
+
if (method) {
|
|
269
|
+
return method;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const fn = sourceFile.getFunction(name);
|
|
273
|
+
if (fn) {
|
|
274
|
+
return fn;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private extractCallName(exprText: string): string | null {
|
|
281
|
+
// svc.method() → method, method() → method
|
|
282
|
+
const parts = exprText.split('.');
|
|
283
|
+
const last = parts.at(-1);
|
|
284
|
+
return last && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(last) ? last : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private deduplicateNodes(nodes: CallNode[]): CallNode[] {
|
|
288
|
+
const seen = new Set<string>();
|
|
289
|
+
return nodes.filter((n) => {
|
|
290
|
+
const key = `${n.file}:${n.name}`;
|
|
291
|
+
if (seen.has(key)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
seen.add(key);
|
|
295
|
+
return true;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Codebase Searcher
|
|
3
|
+
*
|
|
4
|
+
* Searches across all source files in a ts-morph Project using three modes:
|
|
5
|
+
* - `text` — literal string search (case-sensitive substring)
|
|
6
|
+
* - `pattern` — regular-expression search
|
|
7
|
+
* - `symbol` — TypeScript declaration search (class, function, interface,
|
|
8
|
+
* enum, type alias, variable)
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* The project must already have the relevant source files added before calling
|
|
12
|
+
* `search`. For `symbol` mode only top-level named declarations are indexed;
|
|
13
|
+
* local variables and anonymous expressions are not returned.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const searcher = new CodebaseSearcher(project);
|
|
18
|
+
*
|
|
19
|
+
* // Text search
|
|
20
|
+
* const hits = searcher.search('TODO:', { mode: 'text' });
|
|
21
|
+
*
|
|
22
|
+
* // Regex search
|
|
23
|
+
* const selectors = searcher.search('selector:\\s*["\']([^"\']+)["\']', { mode: 'pattern' });
|
|
24
|
+
*
|
|
25
|
+
* // Symbol search
|
|
26
|
+
* const classes = searcher.search('UserService', { mode: 'symbol' });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { Project } from 'ts-morph';
|
|
31
|
+
|
|
32
|
+
/** The kind of named declaration returned in symbol-mode results. */
|
|
33
|
+
export type SymbolKind =
|
|
34
|
+
| 'class'
|
|
35
|
+
| 'function'
|
|
36
|
+
| 'interface'
|
|
37
|
+
| 'enum'
|
|
38
|
+
| 'typeAlias'
|
|
39
|
+
| 'variable';
|
|
40
|
+
|
|
41
|
+
/** A single search result. */
|
|
42
|
+
export interface SearchResult {
|
|
43
|
+
/** Absolute file path. */
|
|
44
|
+
file: string;
|
|
45
|
+
|
|
46
|
+
/** 1-based line number of the match. */
|
|
47
|
+
line: number;
|
|
48
|
+
|
|
49
|
+
/** 1-based column of the match start. */
|
|
50
|
+
col: number;
|
|
51
|
+
|
|
52
|
+
/** The source line (trimmed) containing the match. */
|
|
53
|
+
snippet: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set in `symbol` mode to classify the declaration type.
|
|
57
|
+
* Absent in `text` and `pattern` modes.
|
|
58
|
+
*/
|
|
59
|
+
symbolKind?: SymbolKind;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Options for `CodebaseSearcher.search()`. */
|
|
63
|
+
export interface SearchOptions {
|
|
64
|
+
/**
|
|
65
|
+
* - `text` — literal substring match
|
|
66
|
+
* - `pattern` — regular expression match
|
|
67
|
+
* - `symbol` — TypeScript named declaration lookup
|
|
68
|
+
*/
|
|
69
|
+
mode: 'text' | 'pattern' | 'symbol';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Optional glob pattern (minimatch) to restrict which files are searched.
|
|
73
|
+
* E.g. `'**\/*.service.ts'`. When omitted all project source files are
|
|
74
|
+
* searched.
|
|
75
|
+
*/
|
|
76
|
+
include?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of results to return. When omitted all matches are
|
|
80
|
+
* returned.
|
|
81
|
+
*/
|
|
82
|
+
maxResults?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Converts a glob pattern (`**\/*.service.ts`) to a RegExp and tests `filePath`.
|
|
87
|
+
* Supports `**` (any path segments) and `*` (single segment wildcard).
|
|
88
|
+
*/
|
|
89
|
+
function matchesGlob(filePath: string, pattern: string): boolean {
|
|
90
|
+
const escaped = pattern
|
|
91
|
+
.replaceAll(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
92
|
+
.replaceAll('**', '\u0000')
|
|
93
|
+
.replaceAll('*', '[^/]*')
|
|
94
|
+
.replaceAll('\u0000', '.*');
|
|
95
|
+
return new RegExp(`(^|/)${escaped}$`).test(filePath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Searches source files in a ts-morph `Project` by text, regex, or symbol
|
|
100
|
+
* name.
|
|
101
|
+
*/
|
|
102
|
+
export class CodebaseSearcher {
|
|
103
|
+
constructor(private readonly project: Project) { }
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Search for `query` across all (filtered) source files.
|
|
107
|
+
*
|
|
108
|
+
* @param query - The text, regex pattern, or symbol name to find.
|
|
109
|
+
* @param options - Search mode and optional file filter / result limit.
|
|
110
|
+
* @returns Array of `SearchResult` objects ordered by file then line.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* searcher.search('UserService', { mode: 'symbol' });
|
|
115
|
+
* searcher.search('TODO', { mode: 'text', include: 'src/**\/*.ts' });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
search(query: string, options: SearchOptions): SearchResult[] {
|
|
119
|
+
const sourceFiles = this.project
|
|
120
|
+
.getSourceFiles()
|
|
121
|
+
.filter(
|
|
122
|
+
(sf) =>
|
|
123
|
+
options.include === undefined ||
|
|
124
|
+
matchesGlob(sf.getFilePath(), options.include),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const results: SearchResult[] =
|
|
128
|
+
options.mode === 'symbol'
|
|
129
|
+
? this.searchSymbols(query, sourceFiles)
|
|
130
|
+
: this.searchText(query, options.mode, sourceFiles);
|
|
131
|
+
|
|
132
|
+
if (options.maxResults !== undefined) {
|
|
133
|
+
return results.slice(0, options.maxResults);
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Text or pattern search: splits each file into lines and tests each line.
|
|
140
|
+
*/
|
|
141
|
+
private searchText(
|
|
142
|
+
query: string,
|
|
143
|
+
mode: 'text' | 'pattern',
|
|
144
|
+
sourceFiles: ReturnType<Project['getSourceFiles']>,
|
|
145
|
+
): SearchResult[] {
|
|
146
|
+
const results: SearchResult[] = [];
|
|
147
|
+
const regex = mode === 'pattern' ? new RegExp(query) : null;
|
|
148
|
+
|
|
149
|
+
for (const sf of sourceFiles) {
|
|
150
|
+
const filePath = sf.getFilePath();
|
|
151
|
+
const lines = sf.getFullText().split('\n');
|
|
152
|
+
|
|
153
|
+
lines.forEach((lineText, index) => {
|
|
154
|
+
const matched =
|
|
155
|
+
regex === null ? lineText.includes(query) : regex.test(lineText);
|
|
156
|
+
|
|
157
|
+
if (!matched) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const col =
|
|
162
|
+
regex === null
|
|
163
|
+
? lineText.indexOf(query) + 1
|
|
164
|
+
: lineText.search(regex) + 1;
|
|
165
|
+
|
|
166
|
+
results.push({
|
|
167
|
+
file: filePath,
|
|
168
|
+
line: index + 1,
|
|
169
|
+
col,
|
|
170
|
+
snippet: lineText.trim(),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Symbol search: finds top-level named declarations whose name equals
|
|
180
|
+
* `query`.
|
|
181
|
+
*/
|
|
182
|
+
private searchSymbols(
|
|
183
|
+
query: string,
|
|
184
|
+
sourceFiles: ReturnType<Project['getSourceFiles']>,
|
|
185
|
+
): SearchResult[] {
|
|
186
|
+
const results: SearchResult[] = [];
|
|
187
|
+
|
|
188
|
+
for (const sf of sourceFiles) {
|
|
189
|
+
const filePath = sf.getFilePath();
|
|
190
|
+
|
|
191
|
+
// Classes
|
|
192
|
+
for (const decl of sf.getClasses()) {
|
|
193
|
+
if (decl.getName() === query) {
|
|
194
|
+
const pos = decl.getNameNode()?.getStart() ?? decl.getStart();
|
|
195
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
196
|
+
results.push({
|
|
197
|
+
file: filePath,
|
|
198
|
+
line,
|
|
199
|
+
col: column,
|
|
200
|
+
snippet: (decl.getText().split('\n')[0] ?? '').trim(),
|
|
201
|
+
symbolKind: 'class',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Functions
|
|
207
|
+
for (const decl of sf.getFunctions()) {
|
|
208
|
+
if (decl.getName() === query) {
|
|
209
|
+
const pos = decl.getNameNode()?.getStart() ?? decl.getStart();
|
|
210
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
211
|
+
results.push({
|
|
212
|
+
file: filePath,
|
|
213
|
+
line,
|
|
214
|
+
col: column,
|
|
215
|
+
snippet: (decl.getText().split('\n')[0] ?? '').trim(),
|
|
216
|
+
symbolKind: 'function',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Interfaces
|
|
222
|
+
for (const decl of sf.getInterfaces()) {
|
|
223
|
+
if (decl.getName() === query) {
|
|
224
|
+
const pos = decl.getNameNode().getStart();
|
|
225
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
226
|
+
results.push({
|
|
227
|
+
file: filePath,
|
|
228
|
+
line,
|
|
229
|
+
col: column,
|
|
230
|
+
snippet: (decl.getText().split('\n')[0] ?? '').trim(),
|
|
231
|
+
symbolKind: 'interface',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Enums
|
|
237
|
+
for (const decl of sf.getEnums()) {
|
|
238
|
+
if (decl.getName() === query) {
|
|
239
|
+
const pos = decl.getNameNode().getStart();
|
|
240
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
241
|
+
results.push({
|
|
242
|
+
file: filePath,
|
|
243
|
+
line,
|
|
244
|
+
col: column,
|
|
245
|
+
snippet: (decl.getText().split('\n')[0] ?? '').trim(),
|
|
246
|
+
symbolKind: 'enum',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Type aliases
|
|
252
|
+
for (const decl of sf.getTypeAliases()) {
|
|
253
|
+
if (decl.getName() === query) {
|
|
254
|
+
const pos = decl.getNameNode().getStart();
|
|
255
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
256
|
+
results.push({
|
|
257
|
+
file: filePath,
|
|
258
|
+
line,
|
|
259
|
+
col: column,
|
|
260
|
+
snippet: (decl.getText().split('\n')[0] ?? '').trim(),
|
|
261
|
+
symbolKind: 'typeAlias',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Variables (top-level const/let/var declarations)
|
|
267
|
+
for (const varStmt of sf.getVariableStatements()) {
|
|
268
|
+
for (const decl of varStmt.getDeclarations()) {
|
|
269
|
+
if (decl.getName() === query) {
|
|
270
|
+
const pos = decl.getNameNode().getStart();
|
|
271
|
+
const { line, column } = sf.getLineAndColumnAtPos(pos);
|
|
272
|
+
results.push({
|
|
273
|
+
file: filePath,
|
|
274
|
+
line,
|
|
275
|
+
col: column,
|
|
276
|
+
snippet: (varStmt.getText().split('\n')[0] ?? '').trim(),
|
|
277
|
+
symbolKind: 'variable',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return results;
|
|
285
|
+
}
|
|
286
|
+
}
|