@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,545 @@
1
+ /**
2
+ * @angular-modernizer/api - Service Pattern Recognizer
3
+ *
4
+ * Centralized service for recognizing and classifying Angular service patterns.
5
+ * Uses ng-morph for Angular-specific detection and ts-morph for fallback analysis.
6
+ *
7
+ * MIGRATION NOTE: Created as part of ng-morph integration Phase 4.
8
+ * - Centralizes service pattern recognition across orchestrators
9
+ * - Uses ng-morph for Angular services (`` decorated classes)
10
+ * - Falls back to ts-morph for complex edge cases
11
+ * - Replaces scattered service suffix checks in multiple orchestrators
12
+ * - Expected code reduction: ~40% in orchestrators using this service
13
+ */
14
+
15
+ import type { SourceFile, ClassDeclaration } from 'ts-morph';
16
+ import type { NgMorphAdapter } from '../transformation/ng-morph-adapter.js';
17
+
18
+ /**
19
+ * Service pattern types that can be recognized.
20
+ */
21
+ export enum ServicePattern {
22
+ /** Angular service decorated with `` */
23
+ INJECTABLE = 'injectable',
24
+
25
+ /** Service class with 'Service' suffix */
26
+ SERVICE = 'service',
27
+
28
+ /** Repository pattern class with 'Repository' suffix */
29
+ REPOSITORY = 'repository',
30
+
31
+ /** Provider class with 'Provider' suffix */
32
+ PROVIDER = 'provider',
33
+
34
+ /** Facade pattern class with 'Facade' suffix */
35
+ FACADE = 'facade',
36
+
37
+ /** Store class (state management) with 'Store' suffix */
38
+ STORE = 'store',
39
+
40
+ /** Helper class with 'Helper' suffix */
41
+ HELPER = 'helper',
42
+
43
+ /** Utility class with 'Util' or 'Utils' suffix */
44
+ UTILITY = 'utility',
45
+
46
+ /** Unknown pattern - not recognized as service */
47
+ UNKNOWN = 'unknown',
48
+ }
49
+
50
+ /**
51
+ * Service classification result.
52
+ */
53
+ export interface ServiceClassification {
54
+ /** The service pattern type */
55
+ pattern: ServicePattern;
56
+
57
+ /** Whether this is an Angular `` service */
58
+ isInjectable: boolean;
59
+
60
+ /** Class name */
61
+ className: string;
62
+
63
+ /** File path */
64
+ filePath: string;
65
+
66
+ /** Whether this is a state management service */
67
+ isStateful: boolean;
68
+
69
+ /** Confidence level (0-1) for the classification */
70
+ confidence: number;
71
+ }
72
+
73
+ /**
74
+ * Service detection result with confidence scoring.
75
+ */
76
+ export interface ServiceDetectionResult {
77
+ /** Whether this is a service */
78
+ isService: boolean;
79
+
80
+ /** Confidence level (0-1) for the detection */
81
+ confidence: number;
82
+
83
+ /** Detection method used */
84
+ detectionMethod: 'decorator' | 'filename' | 'classname' | 'combined' | 'none';
85
+ }
86
+
87
+ /**
88
+ * Service Pattern Recognizer - centralized service pattern detection.
89
+ *
90
+ * HYBRID APPROACH:
91
+ * - Uses ng-morph to detect `` decorated classes
92
+ * - Falls back to ts-morph for suffix-based pattern matching
93
+ * - Provides confidence scores for classifications
94
+ *
95
+ * This replaces scattered service detection logic across orchestrators.
96
+ */
97
+ export class ServicePatternRecognizer {
98
+ private readonly ngMorphAdapter?: NgMorphAdapter;
99
+
100
+ /**
101
+ * Constructor for ServicePatternRecognizer.
102
+ *
103
+ * @param ngMorphAdapter - Optional ng-morph adapter for Angular-specific operations
104
+ *
105
+ * BACKWARD COMPATIBILITY: Constructor is optional - if not provided, falls back to pure ts-morph.
106
+ */
107
+ constructor(ngMorphAdapter?: NgMorphAdapter) {
108
+ this.ngMorphAdapter = ngMorphAdapter;
109
+ }
110
+
111
+ /**
112
+ * Classify a source file as a service pattern.
113
+ *
114
+ * Uses ng-morph to detect ``, then analyzes class structure
115
+ * and naming to determine the specific service pattern.
116
+ *
117
+ * @param sourceFile - The source file to analyze
118
+ * @returns Service classification result
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const classification = recognizer.classifyService(sourceFile);
123
+ * if (classification.pattern === ServicePattern.INJECTABLE) {
124
+ * console.info('This is an Angular service');
125
+ * }
126
+ * ```
127
+ */
128
+ classifyService(sourceFile: SourceFile): ServiceClassification {
129
+ const filePath = sourceFile.getFilePath();
130
+
131
+ // Try ng-morph first for Angular services
132
+ if (this.ngMorphAdapter) {
133
+ const injectableClass = this.detectInjectableUsingNgMorph(filePath);
134
+ if (injectableClass) {
135
+ return this.classifyInjectableService(injectableClass, filePath);
136
+ }
137
+ }
138
+
139
+ // Fall back to ts-morph for suffix-based detection
140
+ return this.classifyUsingTsMorph(sourceFile);
141
+ }
142
+
143
+ /**
144
+ * Check if a source file contains a service class.
145
+ *
146
+ * Quick check without full classification.
147
+ *
148
+ * @param sourceFile - The source file to check
149
+ * @returns True if the file contains a service class
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * if (recognizer.isServiceFile(sourceFile)) {
154
+ * // Perform service-specific analysis
155
+ * }
156
+ * ```
157
+ */
158
+ isServiceFile(sourceFile: SourceFile): boolean {
159
+ const classification = this.classifyService(sourceFile);
160
+ return classification.pattern !== ServicePattern.UNKNOWN;
161
+ }
162
+
163
+ /**
164
+ * Get all service patterns in the source file.
165
+ *
166
+ * Some files may contain multiple service classes.
167
+ *
168
+ * @param sourceFile - The source file to analyze
169
+ * @returns Array of service classifications
170
+ */
171
+ getAllServicePatterns(sourceFile: SourceFile): ServiceClassification[] {
172
+ const filePath = sourceFile.getFilePath();
173
+ const results: ServiceClassification[] = [];
174
+
175
+ // Try ng-morph for all @Injectable classes
176
+ if (this.ngMorphAdapter) {
177
+ const injectables = this.getAllInjectablesUsingNgMorph(filePath);
178
+ results.push(
179
+ ...injectables.map((cls) =>
180
+ this.classifyInjectableService(cls, filePath),
181
+ ),
182
+ );
183
+ }
184
+
185
+ // Also check for non-@Injectable service classes
186
+ const classes = sourceFile.getClasses();
187
+ for (const cls of classes) {
188
+ const className = cls.getName();
189
+ if (!className) {
190
+ continue;
191
+ }
192
+
193
+ // Skip if already classified as @Injectable
194
+ if (results.some((r) => r.className === className)) {
195
+ continue;
196
+ }
197
+
198
+ const pattern = this.detectPatternFromName(className);
199
+ if (pattern !== ServicePattern.UNKNOWN) {
200
+ results.push({
201
+ pattern,
202
+ isInjectable: false,
203
+ className,
204
+ filePath,
205
+ isStateful: this.isStatefulPattern(pattern),
206
+ confidence: 0.7, // Lower confidence for suffix-based detection
207
+ });
208
+ }
209
+ }
210
+
211
+ return results;
212
+ }
213
+
214
+ /**
215
+ * Detect if a class is a service with confidence scoring.
216
+ *
217
+ * This method provides a more detailed analysis than isService(),
218
+ * including confidence levels and detection methodology.
219
+ *
220
+ * @param classDecl - The class declaration to analyze
221
+ * @returns Service detection result with confidence and method
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const result = recognizer.detectService(classDecl);
226
+ * if (result.isService && result.confidence > 0.8) {
227
+ * console.info(`Detected service via ${result.detectionMethod}`);
228
+ * }
229
+ * ```
230
+ */
231
+ detectService(classDecl: ClassDeclaration): ServiceDetectionResult {
232
+ // Priority 1: Check for @Injectable decorator (highest confidence)
233
+ if (this.hasInjectableDecorator(classDecl)) {
234
+ return {
235
+ isService: true,
236
+ confidence: 1.0,
237
+ detectionMethod: 'decorator',
238
+ };
239
+ }
240
+
241
+ // Priority 2: Check class name pattern
242
+ const className = classDecl.getName();
243
+ if (className) {
244
+ const pattern = this.detectPatternFromName(className);
245
+ if (pattern !== ServicePattern.UNKNOWN) {
246
+ return {
247
+ isService: true,
248
+ confidence: 0.7,
249
+ detectionMethod: 'classname',
250
+ };
251
+ }
252
+ }
253
+
254
+ // Priority 3: Check file name pattern
255
+ const sourceFile = classDecl.getSourceFile();
256
+ const filePath = sourceFile.getFilePath().toLowerCase();
257
+ if (filePath.includes('.service.') || filePath.endsWith('service.ts')) {
258
+ return {
259
+ isService: true,
260
+ confidence: 0.6,
261
+ detectionMethod: 'filename',
262
+ };
263
+ }
264
+
265
+ // Not detected as service
266
+ return {
267
+ isService: false,
268
+ confidence: 0.0,
269
+ detectionMethod: 'none',
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Check if a class declaration has the `` decorator.
275
+ *
276
+ * Uses ng-morph adapter if available for enhanced detection,
277
+ * falls back to legacy ts-morph detection otherwise.
278
+ *
279
+ * @param classDecl - The class declaration to check
280
+ * @returns True if the class has `` decorator
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * if (recognizer.hasInjectableDecorator(classDecl)) {
285
+ * console.info('This is an Angular service');
286
+ * }
287
+ * ```
288
+ */
289
+ hasInjectableDecorator(classDecl: ClassDeclaration): boolean {
290
+ // Try ng-morph approach if adapter is available
291
+ if (this.ngMorphAdapter) {
292
+ try {
293
+ const filePath = classDecl.getSourceFile().getFilePath();
294
+ const ngMorphClass = this.detectInjectableUsingNgMorph(filePath);
295
+ if (ngMorphClass && ngMorphClass.getName() === classDecl.getName()) {
296
+ return true;
297
+ }
298
+ } catch {
299
+ // Fall through to legacy detection
300
+ }
301
+ }
302
+
303
+ // Fall back to legacy ts-morph detection
304
+ return this.legacyHasInjectableDecorator(classDecl);
305
+ }
306
+
307
+ /**
308
+ * Legacy method to check for `` decorator using ts-morph.
309
+ *
310
+ * This is the original detection logic, kept for backward compatibility
311
+ * and as a fallback when ng-morph is not available.
312
+ *
313
+ * @param classDecl - The class declaration to check
314
+ * @returns True if the class has `` decorator
315
+ */
316
+ private legacyHasInjectableDecorator(classDecl: ClassDeclaration): boolean {
317
+ return classDecl.getDecorator('Injectable') !== undefined;
318
+ }
319
+
320
+ /**
321
+ * Simple check if a class is a service.
322
+ *
323
+ * This is a convenience method that wraps detectService()
324
+ * for backward compatibility with existing code.
325
+ *
326
+ * @param classDecl - The class declaration to check
327
+ * @returns True if the class is detected as a service
328
+ *
329
+ * @example
330
+ * ```typescript
331
+ * if (recognizer.isService(classDecl)) {
332
+ * // Handle service class
333
+ * }
334
+ * ```
335
+ */
336
+ isService(classDecl: ClassDeclaration): boolean {
337
+ return this.detectService(classDecl).isService;
338
+ }
339
+
340
+ /**
341
+ * Detect `` service using ng-morph.
342
+ *
343
+ * IMPLEMENTATION NOTE: ng-morph doesn't have a dedicated API for finding
344
+ * `` services. We use ts-morph to search for the `` decorator
345
+ * directly, which provides HIGH CONFIDENCE (1.0) classification.
346
+ *
347
+ * @param filePath - File path to check
348
+ * @returns Class declaration if found, undefined otherwise
349
+ */
350
+ private detectInjectableUsingNgMorph(
351
+ filePath: string,
352
+ ): ClassDeclaration | undefined {
353
+ try {
354
+ // Get the source file from ts-morph project
355
+ const project = this.ngMorphAdapter?.getProject();
356
+ const sourceFile = project?.getSourceFile(filePath);
357
+
358
+ if (!sourceFile) {
359
+ return undefined;
360
+ }
361
+
362
+ // Find the first class with @Injectable decorator
363
+ // Note: ng-morph (as of v4.8.4) doesn't provide a specialized API
364
+ // for services, so we use ts-morph's decorator detection
365
+ for (const cls of sourceFile.getClasses()) {
366
+ const hasInjectableDecorator =
367
+ cls.getDecorator('Injectable') !== undefined;
368
+ if (hasInjectableDecorator) {
369
+ return cls;
370
+ }
371
+ }
372
+
373
+ return undefined;
374
+ } catch {
375
+ return undefined;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Get all `` classes using ng-morph.
381
+ *
382
+ * @param filePath - File path to check
383
+ * @returns Array of class declarations
384
+ */
385
+ private getAllInjectablesUsingNgMorph(filePath: string): ClassDeclaration[] {
386
+ try {
387
+ const classes: ClassDeclaration[] = [];
388
+ const project = this.ngMorphAdapter?.getProject();
389
+ const sourceFile = project?.getSourceFile(filePath);
390
+
391
+ if (!sourceFile) {
392
+ return [];
393
+ }
394
+
395
+ // Check all classes in the file
396
+ for (const cls of sourceFile.getClasses()) {
397
+ const hasInjectableDecorator =
398
+ cls.getDecorator('Injectable') !== undefined;
399
+ if (hasInjectableDecorator) {
400
+ classes.push(cls);
401
+ }
402
+ }
403
+
404
+ return classes;
405
+ } catch {
406
+ return [];
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Classify an `` service.
412
+ *
413
+ * @param classDecl - The class declaration
414
+ * @param filePath - The file path
415
+ * @returns Service classification
416
+ */
417
+ private classifyInjectableService(
418
+ classDecl: ClassDeclaration,
419
+ filePath: string,
420
+ ): ServiceClassification {
421
+ const className = classDecl.getName() ?? 'Unknown';
422
+ const pattern = this.detectPatternFromName(className);
423
+
424
+ return {
425
+ pattern:
426
+ pattern === ServicePattern.UNKNOWN
427
+ ? ServicePattern.INJECTABLE
428
+ : pattern,
429
+ isInjectable: true,
430
+ className,
431
+ filePath,
432
+ isStateful: this.isStatefulClass(classDecl),
433
+ confidence: 1.0, // High confidence for @Injectable
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Classify service using ts-morph (fallback).
439
+ *
440
+ * @param sourceFile - The source file
441
+ * @returns Service classification
442
+ */
443
+ private classifyUsingTsMorph(sourceFile: SourceFile): ServiceClassification {
444
+ const filePath = sourceFile.getFilePath();
445
+ const classes = sourceFile.getClasses();
446
+
447
+ for (const cls of classes) {
448
+ const className = cls.getName();
449
+ if (!className) {
450
+ continue;
451
+ }
452
+
453
+ const pattern = this.detectPatternFromName(className);
454
+ if (pattern !== ServicePattern.UNKNOWN) {
455
+ return {
456
+ pattern,
457
+ isInjectable: false,
458
+ className,
459
+ filePath,
460
+ isStateful: this.isStatefulClass(cls),
461
+ confidence: 0.7, // Lower confidence for suffix-based
462
+ };
463
+ }
464
+ }
465
+
466
+ return {
467
+ pattern: ServicePattern.UNKNOWN,
468
+ isInjectable: false,
469
+ className: 'Unknown',
470
+ filePath,
471
+ isStateful: false,
472
+ confidence: 0.0,
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Detect service pattern from class name.
478
+ *
479
+ * @param className - The class name to analyze
480
+ * @returns Service pattern
481
+ */
482
+ private detectPatternFromName(className: string): ServicePattern {
483
+ const lowerName = className.toLowerCase();
484
+
485
+ if (lowerName.endsWith('service')) {
486
+ return ServicePattern.SERVICE;
487
+ }
488
+ if (lowerName.endsWith('repository')) {
489
+ return ServicePattern.REPOSITORY;
490
+ }
491
+ if (lowerName.endsWith('provider')) {
492
+ return ServicePattern.PROVIDER;
493
+ }
494
+ if (lowerName.endsWith('facade')) {
495
+ return ServicePattern.FACADE;
496
+ }
497
+ if (lowerName.endsWith('store')) {
498
+ return ServicePattern.STORE;
499
+ }
500
+ if (lowerName.endsWith('helper')) {
501
+ return ServicePattern.HELPER;
502
+ }
503
+ if (lowerName.endsWith('util') || lowerName.endsWith('utils')) {
504
+ return ServicePattern.UTILITY;
505
+ }
506
+
507
+ return ServicePattern.UNKNOWN;
508
+ }
509
+
510
+ /**
511
+ * Check if a pattern represents stateful services.
512
+ *
513
+ * @param pattern - The service pattern
514
+ * @returns True if stateful
515
+ */
516
+ private isStatefulPattern(pattern: ServicePattern): boolean {
517
+ return (
518
+ pattern === ServicePattern.STORE || pattern === ServicePattern.FACADE
519
+ );
520
+ }
521
+
522
+ /**
523
+ * Check if a class is stateful (has state properties).
524
+ *
525
+ * @param classDecl - The class declaration
526
+ * @returns True if stateful
527
+ */
528
+ private isStatefulClass(classDecl: ClassDeclaration): boolean {
529
+ const properties = classDecl.getProperties();
530
+
531
+ // Check for state-related property names
532
+ for (const prop of properties) {
533
+ const propName = prop.getName().toLowerCase();
534
+ if (
535
+ propName.includes('state') ||
536
+ propName.includes('store') ||
537
+ propName.includes('cache')
538
+ ) {
539
+ return true;
540
+ }
541
+ }
542
+
543
+ return false;
544
+ }
545
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @angular-modernizer/api - Symbol Locator
3
+ *
4
+ * Service for locating symbols (classes, functions, etc.) across the entire project.
5
+ * Builds a project-wide index of exported classes for fast lookups.
6
+ *
7
+ * This enables resolving project-local dependencies (components, directives, pipes)
8
+ * and calculating the correct import paths for them.
9
+ */
10
+
11
+ import type { Project, SourceFile, ClassDeclaration } from 'ts-morph';
12
+
13
+ /**
14
+ * Symbol information from the project-wide index.
15
+ */
16
+ export interface SymbolInfo {
17
+ /**
18
+ * The class name
19
+ */
20
+ className: string;
21
+
22
+ /**
23
+ * The source file where this class is declared
24
+ */
25
+ sourceFile: SourceFile;
26
+
27
+ /**
28
+ * Whether this is an exported class
29
+ */
30
+ isExported: boolean;
31
+ }
32
+
33
+ /**
34
+ * Symbol Locator - builds and maintains a project-wide symbol index.
35
+ *
36
+ * This service scans the entire project to create a comprehensive map of all
37
+ * exported classes, enabling fast lookup of where any symbol is defined.
38
+ */
39
+ export class SymbolLocator {
40
+ /**
41
+ * Build a project-wide symbol map.
42
+ *
43
+ * This method scans every source file in the project and indexes all exported
44
+ * classes by their name. The resulting map can be used to quickly find where
45
+ * any class is defined, which is essential for generating correct import paths.
46
+ *
47
+ * @param project - The ts-morph Project instance
48
+ * @returns Map from class name to SymbolInfo
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const symbolMap = symbolLocator.buildProjectSymbolMap(project);
53
+ * const symbol = symbolMap.get('AdduserEntryComponent');
54
+ * if (symbol) {
55
+ * console.info(`Found at: ${symbol.sourceFile.getFilePath()}`);
56
+ * }
57
+ * ```
58
+ */
59
+ buildProjectSymbolMap(project: Project): Map<string, SymbolInfo> {
60
+ const symbolMap = new Map<string, SymbolInfo>();
61
+
62
+ // Get all source files in the project
63
+ const sourceFiles = project.getSourceFiles();
64
+
65
+ for (const sourceFile of sourceFiles) {
66
+ // Skip declaration files and node_modules
67
+ const filePath = sourceFile.getFilePath();
68
+ if (filePath.includes('node_modules') || filePath.endsWith('.d.ts')) {
69
+ continue;
70
+ }
71
+
72
+ // Find all classes in this file
73
+ const classes = sourceFile.getClasses();
74
+
75
+ for (const classDecl of classes) {
76
+ const className = classDecl.getName();
77
+ if (!className) {
78
+ continue; // Skip anonymous classes
79
+ }
80
+
81
+ // Check if the class is exported
82
+ const isExported = this.isClassExported(classDecl);
83
+
84
+ // Only index exported classes (or all classes - configurable)
85
+ if (isExported) {
86
+ // If a class with this name already exists, keep the first occurrence
87
+ // (In real projects, same names in different modules are possible,
88
+ // but for standalone migration, we prioritize the first match)
89
+ if (!symbolMap.has(className)) {
90
+ symbolMap.set(className, {
91
+ className,
92
+ sourceFile,
93
+ isExported,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return symbolMap;
101
+ }
102
+
103
+ /**
104
+ * Check if a class declaration is exported.
105
+ *
106
+ * @param classDecl - The class declaration to check
107
+ * @returns true if the class is exported
108
+ */
109
+ private isClassExported(classDecl: ClassDeclaration): boolean {
110
+ return classDecl.isExported();
111
+ }
112
+
113
+ /**
114
+ * Calculate the relative import path from one file to another.
115
+ *
116
+ * Given a source file and a target file, this calculates the correct
117
+ * relative import path (e.g., '../dialogs/xyz.component').
118
+ *
119
+ * @param fromFile - The file that will contain the import statement
120
+ * @param toFile - The file to import from
121
+ * @returns The relative import path (without extension)
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // From: /project/src/app/user/user.component.ts
126
+ * // To: /project/src/app/dialogs/xyz.component.ts
127
+ * // Result: '../dialogs/xyz.component'
128
+ * ```
129
+ */
130
+ calculateRelativeImportPath(
131
+ fromFile: SourceFile,
132
+ toFile: SourceFile,
133
+ ): string {
134
+ const fromPath = fromFile.getFilePath();
135
+ const toPath = toFile.getFilePath();
136
+
137
+ // Get directory paths
138
+ const fromDir = fromPath.substring(0, fromPath.lastIndexOf('/'));
139
+ const toPathWithoutExt = toPath.replace(/\.ts$/, '');
140
+
141
+ // Calculate relative path
142
+ const fromParts = fromDir.split('/');
143
+ const toParts = toPathWithoutExt.split('/');
144
+
145
+ // Find common ancestor
146
+ let commonLength = 0;
147
+ while (
148
+ commonLength < fromParts.length &&
149
+ commonLength < toParts.length &&
150
+ fromParts[commonLength] === toParts[commonLength]
151
+ ) {
152
+ commonLength++;
153
+ }
154
+
155
+ // Build relative path
156
+ const upLevels = fromParts.length - commonLength;
157
+ const downPath = toParts.slice(commonLength).join('/');
158
+
159
+ let relativePath = '';
160
+ if (upLevels > 0) {
161
+ relativePath = '../'.repeat(upLevels);
162
+ } else {
163
+ relativePath = './';
164
+ }
165
+
166
+ relativePath += downPath;
167
+
168
+ return relativePath;
169
+ }
170
+ }