@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,720 @@
1
+ /**
2
+ * @angular-modernizer/api - Dependency Analyzer
3
+ *
4
+ * The "All-Knowing Cartographer" - builds comprehensive symbol maps for Angular modules.
5
+ * Recursively walks NgModule imports to create a complete map of available symbols.
6
+ *
7
+ * Philosophy: "Trust the Map"
8
+ * - Builds the definitive map of what's available in a module's scope
9
+ * - Handles the critical edge case: `` on setters with aliases
10
+ * - Plugins use this map to make intelligent decisions
11
+ */
12
+
13
+ import type {
14
+ SourceFile,
15
+ ClassDeclaration,
16
+ Decorator,
17
+ Project,
18
+ PropertyDeclaration,
19
+ SetAccessorDeclaration,
20
+ GetAccessorDeclaration,
21
+ } from 'ts-morph';
22
+
23
+ /**
24
+ * Represents a dependency between two artifacts.
25
+ */
26
+ export interface Dependency {
27
+ /**
28
+ * Source file path.
29
+ */
30
+ from: string;
31
+
32
+ /**
33
+ * Target file path.
34
+ */
35
+ to: string;
36
+
37
+ /**
38
+ * Type of dependency.
39
+ */
40
+ type: 'import' | 'component-usage' | 'directive-usage' | 'service-injection';
41
+
42
+ /**
43
+ * Symbol name being used.
44
+ */
45
+ symbol?: string;
46
+
47
+ /**
48
+ * Additional metadata.
49
+ */
50
+ metadata?: Record<string, unknown>;
51
+ }
52
+
53
+ /**
54
+ * Represents a symbol in the Angular module system.
55
+ */
56
+ export interface SymbolInfo {
57
+ /**
58
+ * Class name of the symbol.
59
+ */
60
+ className: string;
61
+
62
+ /**
63
+ * Selector(s) for components/directives.
64
+ * For components: ['app-root']
65
+ * For directives: ['[myDirective]', 'myDirective']
66
+ */
67
+ selectors: string[];
68
+
69
+ /**
70
+ * Input property names (including aliases).
71
+ * For ``('customValue') set value(...), this includes 'customValue'.
72
+ */
73
+ inputs: string[];
74
+
75
+ /**
76
+ * Output property names (including aliases).
77
+ */
78
+ outputs: string[];
79
+
80
+ /**
81
+ * Type of Angular artifact.
82
+ */
83
+ type: 'component' | 'directive' | 'pipe' | 'module';
84
+
85
+ /**
86
+ * Source file path.
87
+ */
88
+ filePath: string;
89
+ }
90
+
91
+ /**
92
+ * Complete symbol map for a module's scope.
93
+ */
94
+ export interface SymbolMap {
95
+ /**
96
+ * Symbols declared in this module.
97
+ */
98
+ declared: Map<string, SymbolInfo>;
99
+
100
+ /**
101
+ * Symbols imported into this module (from other modules).
102
+ */
103
+ imported: Map<string, SymbolInfo>;
104
+
105
+ /**
106
+ * Symbols exported by this module (available to consumers).
107
+ */
108
+ exported: Map<string, SymbolInfo>;
109
+
110
+ /**
111
+ * All effectively available symbols in this module's scope.
112
+ * This is the union of declared + imported symbols.
113
+ */
114
+ available: Map<string, SymbolInfo>;
115
+ }
116
+
117
+ /**
118
+ * Dependency Analyzer - extracts dependency relationships and builds symbol maps.
119
+ *
120
+ * This is the "All-Knowing Cartographer" that creates comprehensive maps of
121
+ * what's available in an NgModule's scope.
122
+ */
123
+ export class DependencyAnalyzer {
124
+ /**
125
+ * Analyze all dependencies from a source file.
126
+ * Extracts all named imports and creates dependency objects for each.
127
+ *
128
+ * @param sourceFile - The source file to analyze
129
+ * @returns Array of dependencies
130
+ */
131
+ analyzeDependencies(sourceFile: SourceFile): Dependency[] {
132
+ const filePath = sourceFile.getFilePath();
133
+
134
+ return sourceFile.getImportDeclarations().flatMap((importDecl) => {
135
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
136
+ return importDecl.getNamedImports().map((namedImport) => ({
137
+ from: filePath,
138
+ to: moduleSpecifier,
139
+ type: 'import' as const,
140
+ symbol: namedImport.getName(),
141
+ }));
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Check if a file depends on another file.
147
+ *
148
+ * @param sourceFile - The source file
149
+ * @param targetPath - The target file path
150
+ * @returns True if dependency exists
151
+ */
152
+ dependsOn(sourceFile: SourceFile, targetPath: string): boolean {
153
+ const dependencies = this.analyzeDependencies(sourceFile);
154
+ return dependencies.some((dep) => dep.to === targetPath);
155
+ }
156
+
157
+ /**
158
+ * Build a comprehensive symbol map for an NgModule.
159
+ *
160
+ * This is the heart of the "Trust the Map" architecture.
161
+ * It creates the definitive map of what's available in a module's scope.
162
+ *
163
+ * @param moduleFile - The source file containing the NgModule
164
+ * @param project - Optional ts-morph Project to resolve symbols across files
165
+ * @returns Complete symbol map
166
+ */
167
+ buildSymbolMap(moduleFile: SourceFile, project?: Project): SymbolMap {
168
+ const symbolMap: SymbolMap = {
169
+ declared: new Map(),
170
+ imported: new Map(),
171
+ exported: new Map(),
172
+ available: new Map(),
173
+ };
174
+
175
+ // Find the NgModule class
176
+ const moduleClass = this.findNgModuleClass(moduleFile);
177
+ if (!moduleClass) {
178
+ return symbolMap;
179
+ }
180
+
181
+ // Extract symbols from the @NgModule decorator
182
+ const decorator = this.findDecorator(moduleClass, 'NgModule');
183
+ if (!decorator) {
184
+ return symbolMap;
185
+ }
186
+
187
+ // Parse the decorator to find declarations, imports, exports
188
+ const decoratorText = decorator.getText();
189
+
190
+ // Extract declared symbols
191
+ const declarations = this.extractArrayProperty(
192
+ decoratorText,
193
+ 'declarations',
194
+ );
195
+
196
+ declarations.forEach((className) => {
197
+ let symbolInfo = this.extractSymbolInfo(moduleFile, className);
198
+
199
+ if (!symbolInfo && project) {
200
+ const foundFile = project
201
+ .getSourceFiles()
202
+ .find((sf) => this.extractSymbolInfo(sf, className));
203
+ symbolInfo = foundFile
204
+ ? this.extractSymbolInfo(foundFile, className)
205
+ : undefined;
206
+ }
207
+
208
+ if (symbolInfo) {
209
+ symbolMap.declared.set(className, symbolInfo);
210
+ symbolMap.available.set(className, symbolInfo);
211
+ }
212
+ });
213
+
214
+ // Extract imported symbols (from other modules)
215
+ const imports = this.extractArrayProperty(decoratorText, 'imports');
216
+ const knownModules = new Set(['CommonModule', 'FormsModule']);
217
+
218
+ imports.forEach((moduleName) => {
219
+ if (knownModules.has(moduleName)) {
220
+ const builtInSymbol: SymbolInfo = {
221
+ className: moduleName,
222
+ selectors: [],
223
+ inputs: [],
224
+ outputs: [],
225
+ type: 'module',
226
+ filePath: '@angular/common',
227
+ };
228
+ symbolMap.imported.set(moduleName, builtInSymbol);
229
+ symbolMap.available.set(moduleName, builtInSymbol);
230
+ }
231
+
232
+ if (project && !knownModules.has(moduleName)) {
233
+ const importedModuleSymbols = this.loadModuleExports(
234
+ moduleName,
235
+ project,
236
+ moduleFile,
237
+ );
238
+ importedModuleSymbols.forEach((symbolInfo, symbolName) => {
239
+ symbolMap.imported.set(symbolName, symbolInfo);
240
+ symbolMap.available.set(symbolName, symbolInfo);
241
+ });
242
+ }
243
+ });
244
+
245
+ // Extract exported symbols
246
+ const exports = this.extractArrayProperty(decoratorText, 'exports');
247
+ exports.forEach((className) => {
248
+ const symbolInfo =
249
+ symbolMap.declared.get(className) ?? symbolMap.imported.get(className);
250
+ if (symbolInfo) {
251
+ symbolMap.exported.set(className, symbolInfo);
252
+ }
253
+ });
254
+
255
+ return symbolMap;
256
+ }
257
+
258
+ /**
259
+ * Load exported symbols from an imported module.
260
+ * Recursively resolves module imports to build complete symbol map.
261
+ *
262
+ * @param moduleName - The module class name to load
263
+ * @param project - The ts-morph project
264
+ * @param _currentFile - The current source file (reserved for future use in resolving relative imports)
265
+ * @returns Map of exported symbols
266
+ */
267
+ private loadModuleExports(
268
+ moduleName: string,
269
+ project: Project,
270
+ _currentFile: SourceFile,
271
+ ): Map<string, SymbolInfo> {
272
+ const exports = new Map<string, SymbolInfo>();
273
+ const allSourceFiles = project.getSourceFiles();
274
+
275
+ // Find the module class across all source files
276
+ const moduleResult = allSourceFiles
277
+ .map((sf) => ({ file: sf, class: sf.getClass(moduleName) }))
278
+ .find(({ class: cls }) => cls && this.findDecorator(cls, 'NgModule'));
279
+
280
+ if (!moduleResult?.class) {
281
+ return exports;
282
+ }
283
+
284
+ const { file: moduleFile, class: moduleClass } = moduleResult;
285
+
286
+ // Get the @NgModule decorator
287
+ const decorator = this.findDecorator(moduleClass, 'NgModule');
288
+ if (!decorator) {
289
+ return exports;
290
+ }
291
+
292
+ // Extract exported symbols from the module
293
+ const exportedSymbols = this.extractArrayProperty(
294
+ decorator.getText(),
295
+ 'exports',
296
+ );
297
+
298
+ exportedSymbols.forEach((symbolName) => {
299
+ let symbolInfo = this.extractSymbolInfo(moduleFile, symbolName);
300
+
301
+ if (!symbolInfo) {
302
+ const foundFile = allSourceFiles.find((sf) =>
303
+ this.extractSymbolInfo(sf, symbolName),
304
+ );
305
+ symbolInfo = foundFile
306
+ ? this.extractSymbolInfo(foundFile, symbolName)
307
+ : undefined;
308
+ }
309
+
310
+ if (symbolInfo) {
311
+ exports.set(symbolName, symbolInfo);
312
+ }
313
+ });
314
+
315
+ return exports;
316
+ }
317
+
318
+ /**
319
+ * Get all transitive dependencies for a source file.
320
+ * Follows dependency chain recursively.
321
+ *
322
+ * @param sourceFile - The source file
323
+ * @param project - The ts-morph project
324
+ * @param visited - Set of already visited files (for cycle detection)
325
+ * @returns Array of all transitive dependencies
326
+ */
327
+ getTransitiveDependencies(
328
+ sourceFile: SourceFile,
329
+ project: Project,
330
+ visited = new Set<string>(),
331
+ ): Dependency[] {
332
+ const filePath = sourceFile.getFilePath();
333
+
334
+ // Prevent infinite recursion
335
+ if (visited.has(filePath)) {
336
+ return [];
337
+ }
338
+ visited.add(filePath);
339
+
340
+ const directDependencies = this.analyzeDependencies(sourceFile);
341
+ const localDeps = directDependencies.filter(
342
+ (dep) => dep.to.startsWith('.') || dep.to.startsWith('/'),
343
+ );
344
+
345
+ const transitiveDeps = localDeps.flatMap((dep) => {
346
+ const depPath = dep.to.replace(/^\.\//, '').replace(/\.ts$/, '');
347
+ const depSourceFile = project.getSourceFiles().find((sf) => {
348
+ const sfPath = sf.getFilePath();
349
+ return (
350
+ sfPath.includes(depPath + '.ts') ||
351
+ sfPath.endsWith('/' + depPath + '.ts')
352
+ );
353
+ });
354
+
355
+ return depSourceFile
356
+ ? this.getTransitiveDependencies(depSourceFile, project, visited)
357
+ : [];
358
+ });
359
+
360
+ return [...directDependencies, ...transitiveDeps];
361
+ }
362
+
363
+ /**
364
+ * Detect circular dependencies in a project.
365
+ *
366
+ * @param project - The ts-morph project
367
+ * @returns Array of circular dependency chains
368
+ */
369
+ detectCircularDependencies(
370
+ project: Project,
371
+ ): { cycle: string[]; severity: 'warning' | 'error' }[] {
372
+ const cycles: { cycle: string[]; severity: 'warning' | 'error' }[] = [];
373
+ const sourceFiles = project.getSourceFiles();
374
+
375
+ for (const sourceFile of sourceFiles) {
376
+ const filePath = sourceFile.getFilePath();
377
+ const visited = new Set<string>();
378
+ const path: string[] = [];
379
+
380
+ this.detectCyclesFromFile(
381
+ sourceFile,
382
+ project,
383
+ visited,
384
+ path,
385
+ cycles,
386
+ filePath,
387
+ );
388
+ }
389
+
390
+ // Deduplicate cycles (same cycle can be found from different starting points)
391
+ return this.deduplicateCycles(cycles);
392
+ }
393
+
394
+ /**
395
+ * Detect cycles starting from a specific file.
396
+ * Recursively traverses local dependencies to find circular import chains.
397
+ * Severity based on cycle length: >3 files = error, ≤3 files = warning.
398
+ */
399
+ private detectCyclesFromFile(
400
+ sourceFile: SourceFile,
401
+ project: Project,
402
+ visited: Set<string>,
403
+ path: string[],
404
+ cycles: { cycle: string[]; severity: 'warning' | 'error' }[],
405
+ targetFile: string,
406
+ ): void {
407
+ const filePath = sourceFile.getFilePath();
408
+
409
+ // If we've returned to the target file, we found a cycle
410
+ if (path.length > 0 && filePath === targetFile) {
411
+ cycles.push({
412
+ cycle: [...path, filePath],
413
+ severity: path.length > 3 ? 'error' : 'warning',
414
+ });
415
+ return;
416
+ }
417
+
418
+ // Prevent infinite recursion on already visited paths
419
+ if (visited.has(filePath)) {
420
+ return;
421
+ }
422
+ visited.add(filePath);
423
+ path.push(filePath);
424
+
425
+ // Follow dependencies - filter to local project files only (not node_modules)
426
+ const localDeps = this.analyzeDependencies(sourceFile).filter(
427
+ (dep) => dep.to.startsWith('.') || dep.to.startsWith('/'),
428
+ );
429
+
430
+ localDeps.forEach((dep) => {
431
+ // Normalize import path: strip leading ./ and .ts extension
432
+ const depPath = dep.to.replace(/^\.\//, '').replace(/\.ts$/, '');
433
+ const depSourceFile = project.getSourceFiles().find((sf) => {
434
+ const sfPath = sf.getFilePath();
435
+ // Handle both relative imports with and without .ts extension
436
+ return (
437
+ sfPath.includes(depPath + '.ts') ||
438
+ sfPath.endsWith('/' + depPath + '.ts')
439
+ );
440
+ });
441
+
442
+ if (depSourceFile) {
443
+ // Recurse with fresh visited set and path copy to track independent branches
444
+ this.detectCyclesFromFile(
445
+ depSourceFile,
446
+ project,
447
+ new Set(visited),
448
+ [...path],
449
+ cycles,
450
+ targetFile,
451
+ );
452
+ }
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Deduplicate cycle arrays (same cycle in different order).
458
+ * Example: [A→B→C→A] and [B→C→A→B] represent the same cycle.
459
+ * Uses sorted paths to create unique signatures for comparison.
460
+ */
461
+ private deduplicateCycles(
462
+ cycles: { cycle: string[]; severity: 'warning' | 'error' }[],
463
+ ): { cycle: string[]; severity: 'warning' | 'error' }[] {
464
+ const seen = new Set<string>();
465
+
466
+ return cycles.filter((item) => {
467
+ // Normalize cycle by sorting paths and creating unique signature
468
+ const sorted = [...item.cycle].sort();
469
+ const signature = sorted.join('->');
470
+
471
+ if (seen.has(signature)) {
472
+ return false;
473
+ }
474
+ seen.add(signature);
475
+ return true;
476
+ });
477
+ }
478
+
479
+ /**
480
+ * Extract comprehensive symbol information from a class.
481
+ * Identifies Angular artifact types (``, ``, ``) and extracts metadata.
482
+ *
483
+ * CRITICAL: Handles the edge case of `` on setters with aliases.
484
+ *
485
+ * @param sourceFile - The source file
486
+ * @param className - The class name to analyze
487
+ * @returns Symbol information or undefined
488
+ */
489
+ extractSymbolInfo(
490
+ sourceFile: SourceFile,
491
+ className: string,
492
+ ): SymbolInfo | undefined {
493
+ const classDecl = sourceFile.getClass(className);
494
+ if (!classDecl) {
495
+ return undefined;
496
+ }
497
+
498
+ const filePath = sourceFile.getFilePath();
499
+
500
+ // Map decorator types to extraction methods
501
+ const decoratorHandlers: {
502
+ name: string;
503
+ extract: (
504
+ cls: ClassDeclaration,
505
+ dec: Decorator,
506
+ path: string,
507
+ ) => SymbolInfo;
508
+ }[] = [
509
+ { name: 'Component', extract: this.extractComponentInfo.bind(this) },
510
+ { name: 'Directive', extract: this.extractDirectiveInfo.bind(this) },
511
+ { name: 'Pipe', extract: this.extractPipeInfo.bind(this) },
512
+ ];
513
+
514
+ for (const { name, extract } of decoratorHandlers) {
515
+ const decorator = this.findDecorator(classDecl, name);
516
+ if (decorator) {
517
+ return extract(classDecl, decorator, filePath);
518
+ }
519
+ }
520
+
521
+ return undefined;
522
+ }
523
+
524
+ /**
525
+ * Extract component information including selectors and inputs.
526
+ */
527
+ private extractComponentInfo(
528
+ classDecl: ClassDeclaration,
529
+ decorator: Decorator,
530
+ filePath: string,
531
+ ): SymbolInfo {
532
+ const decoratorText = decorator.getText();
533
+ const selector = this.extractStringProperty(decoratorText, 'selector');
534
+
535
+ return {
536
+ className: classDecl.getName() ?? 'UnknownComponent',
537
+ selectors: selector ? [selector] : [],
538
+ inputs: this.extractInputs(classDecl),
539
+ outputs: this.extractOutputs(classDecl),
540
+ type: 'component',
541
+ filePath,
542
+ };
543
+ }
544
+
545
+ /**
546
+ * Extract directive information including selectors and inputs.
547
+ */
548
+ private extractDirectiveInfo(
549
+ classDecl: ClassDeclaration,
550
+ decorator: Decorator,
551
+ filePath: string,
552
+ ): SymbolInfo {
553
+ const decoratorText = decorator.getText();
554
+ const selector = this.extractStringProperty(decoratorText, 'selector');
555
+
556
+ // For attribute selectors like '[myDirective]', we want both forms
557
+ const selectors: string[] = [];
558
+ if (selector) {
559
+ selectors.push(selector);
560
+ // If it's an attribute selector, also add the bare name
561
+ const bareSelector = selector.replace(/^\[|\]$/g, '');
562
+ if (bareSelector !== selector) {
563
+ selectors.push(bareSelector);
564
+ }
565
+ }
566
+
567
+ return {
568
+ className: classDecl.getName() ?? 'UnknownDirective',
569
+ selectors,
570
+ inputs: this.extractInputs(classDecl),
571
+ outputs: this.extractOutputs(classDecl),
572
+ type: 'directive',
573
+ filePath,
574
+ };
575
+ }
576
+
577
+ /**
578
+ * Extract pipe information.
579
+ */
580
+ private extractPipeInfo(
581
+ classDecl: ClassDeclaration,
582
+ decorator: Decorator,
583
+ filePath: string,
584
+ ): SymbolInfo {
585
+ const decoratorText = decorator.getText();
586
+ const name = this.extractStringProperty(decoratorText, 'name');
587
+
588
+ return {
589
+ className: classDecl.getName() ?? 'UnknownPipe',
590
+ selectors: name ? [name] : [],
591
+ inputs: [],
592
+ outputs: [],
593
+ type: 'pipe',
594
+ filePath,
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Extract all `` properties from a class.
600
+ *
601
+ * CRITICAL: Handles the edge case of `` on setters with aliases.
602
+ * Example: ``('customValue') set value(val: any) { ... }
603
+ * This should extract 'customValue', not 'value'.
604
+ */
605
+ private extractInputs(classDecl: ClassDeclaration): string[] {
606
+ // Helper to extract decorated names with optional aliases
607
+ const extractDecorated = (
608
+ nodes: (
609
+ | PropertyDeclaration
610
+ | SetAccessorDeclaration
611
+ | GetAccessorDeclaration
612
+ )[],
613
+ decoratorName: string,
614
+ ): string[] => {
615
+ const results: string[] = [];
616
+ nodes.forEach((node) => {
617
+ const decorator = this.findDecorator(node, decoratorName);
618
+ if (decorator) {
619
+ const alias = this.extractDecoratorAlias(decorator);
620
+ results.push(alias ?? node.getName());
621
+ }
622
+ });
623
+ return results;
624
+ };
625
+
626
+ // Extract from properties, setters (CRITICAL for fixture test!), and getters
627
+ return [
628
+ ...extractDecorated(classDecl.getProperties(), 'Input'),
629
+ ...extractDecorated(classDecl.getSetAccessors(), 'Input'),
630
+ ...extractDecorated(classDecl.getGetAccessors(), 'Input'),
631
+ ];
632
+ }
633
+
634
+ /**
635
+ * Extract all `` properties from a class.
636
+ */
637
+ private extractOutputs(classDecl: ClassDeclaration): string[] {
638
+ const outputs: string[] = [];
639
+
640
+ classDecl.getProperties().forEach((prop) => {
641
+ const outputDecorator = this.findDecorator(prop, 'Output');
642
+ if (outputDecorator) {
643
+ const alias = this.extractDecoratorAlias(outputDecorator);
644
+ outputs.push(alias ?? prop.getName());
645
+ }
646
+ });
647
+
648
+ return outputs;
649
+ }
650
+
651
+ /**
652
+ * Extract alias from a decorator.
653
+ * Example: ``('customValue') -> 'customValue'
654
+ */
655
+ private extractDecoratorAlias(decorator: Decorator): string | undefined {
656
+ const args = decorator.getArguments();
657
+ const firstArg = args[0];
658
+
659
+ // Remove quotes from string literal if argument exists
660
+ return firstArg?.getText().replace(/^['"`]|['"`]$/g, '');
661
+ }
662
+
663
+ /**
664
+ * Find a decorator on a node by name.
665
+ */
666
+ private findDecorator(
667
+ node:
668
+ | ClassDeclaration
669
+ | PropertyDeclaration
670
+ | SetAccessorDeclaration
671
+ | GetAccessorDeclaration,
672
+ name: string,
673
+ ): Decorator | undefined {
674
+ const decorators = node.getDecorators();
675
+ return decorators.find((d: Decorator) => d.getName() === name);
676
+ }
677
+
678
+ /**
679
+ * Find the NgModule class in a source file.
680
+ */
681
+ private findNgModuleClass(
682
+ sourceFile: SourceFile,
683
+ ): ClassDeclaration | undefined {
684
+ const classes = sourceFile.getClasses();
685
+ return classes.find(
686
+ (cls) => this.findDecorator(cls, 'NgModule') !== undefined,
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Extract a string property value from decorator text.
692
+ * Example: selector: 'app-root' -> 'app-root'
693
+ */
694
+ private extractStringProperty(
695
+ text: string,
696
+ propertyName: string,
697
+ ): string | undefined {
698
+ const regex = new RegExp(`${propertyName}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`);
699
+ const match = text.match(regex);
700
+ return match ? match[1] : undefined;
701
+ }
702
+
703
+ /**
704
+ * Extract an array property value from decorator text.
705
+ * Example: declarations: [Foo, Bar] -> ['Foo', 'Bar']
706
+ */
707
+ private extractArrayProperty(text: string, propertyName: string): string[] {
708
+ const regex = new RegExp(`${propertyName}\\s*:\\s*\\[([^\\]]*)\\]`);
709
+ const match = text.match(regex);
710
+ if (!match?.[1]) {
711
+ return [];
712
+ }
713
+
714
+ const arrayContent = match[1];
715
+ return arrayContent
716
+ .split(',')
717
+ .map((item) => item.trim())
718
+ .filter((item) => item.length > 0);
719
+ }
720
+ }