@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
  4. package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
  5. package/dist/analysis/complexity-metrics-calculator.js +195 -0
  6. package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
  7. package/dist/analysis/dependency-analyzer.d.ts +227 -0
  8. package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
  9. package/dist/analysis/dependency-analyzer.js +451 -0
  10. package/dist/analysis/dependency-analyzer.js.map +1 -0
  11. package/dist/analysis/external-template-loader.d.ts +44 -0
  12. package/dist/analysis/external-template-loader.d.ts.map +1 -0
  13. package/dist/analysis/external-template-loader.js +69 -0
  14. package/dist/analysis/external-template-loader.js.map +1 -0
  15. package/dist/analysis/selector-mapper.d.ts +77 -0
  16. package/dist/analysis/selector-mapper.d.ts.map +1 -0
  17. package/dist/analysis/selector-mapper.js +146 -0
  18. package/dist/analysis/selector-mapper.js.map +1 -0
  19. package/dist/analysis/service-detector.d.ts +164 -0
  20. package/dist/analysis/service-detector.d.ts.map +1 -0
  21. package/dist/analysis/service-detector.js +231 -0
  22. package/dist/analysis/service-detector.js.map +1 -0
  23. package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
  24. package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
  25. package/dist/analysis/service-pattern-recognizer.js +444 -0
  26. package/dist/analysis/service-pattern-recognizer.js.map +1 -0
  27. package/dist/analysis/symbol-locator.d.ts +81 -0
  28. package/dist/analysis/symbol-locator.d.ts.map +1 -0
  29. package/dist/analysis/symbol-locator.js +128 -0
  30. package/dist/analysis/symbol-locator.js.map +1 -0
  31. package/dist/analysis/template-analyzer-usage.d.ts +144 -0
  32. package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
  33. package/dist/analysis/template-analyzer-usage.js +474 -0
  34. package/dist/analysis/template-analyzer-usage.js.map +1 -0
  35. package/dist/analysis/template-analyzer.d.ts +80 -0
  36. package/dist/analysis/template-analyzer.d.ts.map +1 -0
  37. package/dist/analysis/template-analyzer.js +146 -0
  38. package/dist/analysis/template-analyzer.js.map +1 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +22 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/investigation/angular-symbol-finder.d.ts +76 -0
  44. package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
  45. package/dist/investigation/angular-symbol-finder.js +199 -0
  46. package/dist/investigation/angular-symbol-finder.js.map +1 -0
  47. package/dist/investigation/call-graph-builder.d.ts +85 -0
  48. package/dist/investigation/call-graph-builder.d.ts.map +1 -0
  49. package/dist/investigation/call-graph-builder.js +196 -0
  50. package/dist/investigation/call-graph-builder.js.map +1 -0
  51. package/dist/investigation/codebase-searcher.d.ts +99 -0
  52. package/dist/investigation/codebase-searcher.d.ts.map +1 -0
  53. package/dist/investigation/codebase-searcher.js +201 -0
  54. package/dist/investigation/codebase-searcher.js.map +1 -0
  55. package/dist/investigation/stack-trace-parser.d.ts +70 -0
  56. package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
  57. package/dist/investigation/stack-trace-parser.js +117 -0
  58. package/dist/investigation/stack-trace-parser.js.map +1 -0
  59. package/dist/investigation/type-resolver.d.ts +59 -0
  60. package/dist/investigation/type-resolver.d.ts.map +1 -0
  61. package/dist/investigation/type-resolver.js +111 -0
  62. package/dist/investigation/type-resolver.js.map +1 -0
  63. package/dist/investigation/usage-finder.d.ts +69 -0
  64. package/dist/investigation/usage-finder.d.ts.map +1 -0
  65. package/dist/investigation/usage-finder.js +132 -0
  66. package/dist/investigation/usage-finder.js.map +1 -0
  67. package/dist/metrics/complexity-metrics.d.ts +196 -0
  68. package/dist/metrics/complexity-metrics.d.ts.map +1 -0
  69. package/dist/metrics/complexity-metrics.js +445 -0
  70. package/dist/metrics/complexity-metrics.js.map +1 -0
  71. package/dist/public-api.d.ts +77 -0
  72. package/dist/public-api.d.ts.map +1 -0
  73. package/dist/public-api.js +50 -0
  74. package/dist/public-api.js.map +1 -0
  75. package/dist/transformation/import-manager.d.ts +276 -0
  76. package/dist/transformation/import-manager.d.ts.map +1 -0
  77. package/dist/transformation/import-manager.js +488 -0
  78. package/dist/transformation/import-manager.js.map +1 -0
  79. package/dist/transformation/ng-morph-adapter.d.ts +111 -0
  80. package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
  81. package/dist/transformation/ng-morph-adapter.js +179 -0
  82. package/dist/transformation/ng-morph-adapter.js.map +1 -0
  83. package/dist/transformation/ngmodule-manager.d.ts +145 -0
  84. package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
  85. package/dist/transformation/ngmodule-manager.js +356 -0
  86. package/dist/transformation/ngmodule-manager.js.map +1 -0
  87. package/package.json +62 -0
  88. package/src/analysis/complexity-metrics-calculator.ts +253 -0
  89. package/src/analysis/dependency-analyzer.ts +720 -0
  90. package/src/analysis/external-template-loader.ts +79 -0
  91. package/src/analysis/selector-mapper.ts +186 -0
  92. package/src/analysis/service-detector.ts +280 -0
  93. package/src/analysis/service-pattern-recognizer.ts +545 -0
  94. package/src/analysis/symbol-locator.ts +170 -0
  95. package/src/analysis/template-analyzer-usage.ts +614 -0
  96. package/src/analysis/template-analyzer.ts +208 -0
  97. package/src/index.ts +51 -0
  98. package/src/investigation/angular-symbol-finder.ts +263 -0
  99. package/src/investigation/call-graph-builder.ts +298 -0
  100. package/src/investigation/codebase-searcher.ts +286 -0
  101. package/src/investigation/stack-trace-parser.ts +166 -0
  102. package/src/investigation/type-resolver.ts +144 -0
  103. package/src/investigation/usage-finder.ts +198 -0
  104. package/src/metrics/complexity-metrics.ts +617 -0
  105. package/src/public-api.ts +119 -0
  106. package/src/transformation/import-manager.ts +626 -0
  107. package/src/transformation/ng-morph-adapter.ts +252 -0
  108. 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
+ }