@sun-asterisk/sunlint 1.3.34 → 1.3.35

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 (90) hide show
  1. package/core/architecture-integration.js +16 -7
  2. package/core/auto-performance-manager.js +1 -1
  3. package/core/cli-action-handler.js +92 -2
  4. package/core/cli-program.js +96 -138
  5. package/core/file-targeting-service.js +62 -4
  6. package/core/git-utils.js +19 -12
  7. package/core/github-annotate-service.js +326 -11
  8. package/core/html-report-generator.js +326 -731
  9. package/core/impact-integration.js +433 -0
  10. package/core/output-service.js +293 -21
  11. package/core/scoring-service.js +3 -2
  12. package/engines/arch-detect/core/analyzer.js +413 -0
  13. package/engines/arch-detect/core/index.js +22 -0
  14. package/engines/arch-detect/engine/hybrid-detector.js +176 -0
  15. package/engines/arch-detect/engine/index.js +24 -0
  16. package/engines/arch-detect/engine/rule-executor.js +228 -0
  17. package/engines/arch-detect/engine/score-calculator.js +214 -0
  18. package/engines/arch-detect/engine/violation-detector.js +616 -0
  19. package/engines/arch-detect/index.js +50 -0
  20. package/engines/arch-detect/rules/base-rule.js +187 -0
  21. package/engines/arch-detect/rules/index.js +35 -0
  22. package/engines/arch-detect/rules/layered/index.js +28 -0
  23. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
  24. package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
  25. package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
  26. package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
  27. package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
  28. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
  29. package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
  30. package/engines/arch-detect/rules/modular/index.js +27 -0
  31. package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
  32. package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
  33. package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
  34. package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
  35. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
  36. package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
  37. package/engines/arch-detect/rules/presentation/index.js +27 -0
  38. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
  39. package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
  40. package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
  41. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
  42. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
  43. package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
  44. package/engines/arch-detect/rules/project-scanner/index.js +31 -0
  45. package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
  46. package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
  47. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
  48. package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
  49. package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
  50. package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
  51. package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
  52. package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
  53. package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
  54. package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
  55. package/engines/arch-detect/rules/rule-registry.js +111 -0
  56. package/engines/arch-detect/types/context.types.js +60 -0
  57. package/engines/arch-detect/types/enums.js +161 -0
  58. package/engines/arch-detect/types/index.js +25 -0
  59. package/engines/arch-detect/types/result.types.js +7 -0
  60. package/engines/arch-detect/types/rule.types.js +7 -0
  61. package/engines/arch-detect/utils/file-scanner.js +411 -0
  62. package/engines/arch-detect/utils/index.js +23 -0
  63. package/engines/arch-detect/utils/pattern-matcher.js +328 -0
  64. package/engines/impact/cli.js +106 -0
  65. package/engines/impact/config/default-config.js +54 -0
  66. package/engines/impact/core/change-detector.js +258 -0
  67. package/engines/impact/core/detectors/database-detector.js +1317 -0
  68. package/engines/impact/core/detectors/endpoint-detector.js +55 -0
  69. package/engines/impact/core/impact-analyzer.js +124 -0
  70. package/engines/impact/core/report-generator.js +462 -0
  71. package/engines/impact/core/utils/ast-parser.js +241 -0
  72. package/engines/impact/core/utils/dependency-graph.js +159 -0
  73. package/engines/impact/core/utils/file-utils.js +116 -0
  74. package/engines/impact/core/utils/git-utils.js +203 -0
  75. package/engines/impact/core/utils/logger.js +13 -0
  76. package/engines/impact/core/utils/method-call-graph.js +1192 -0
  77. package/engines/impact/index.js +135 -0
  78. package/engines/impact/package.json +29 -0
  79. package/package.json +18 -43
  80. package/scripts/build-release.sh +0 -0
  81. package/scripts/copy-impact-analyzer.js +135 -0
  82. package/scripts/install.sh +0 -0
  83. package/scripts/manual-release.sh +0 -0
  84. package/scripts/pre-release-test.sh +0 -0
  85. package/scripts/prepare-release.sh +0 -0
  86. package/scripts/quick-performance-test.js +0 -0
  87. package/scripts/setup-github-registry.sh +0 -0
  88. package/scripts/trigger-release.sh +0 -0
  89. package/scripts/verify-install.sh +0 -0
  90. package/templates/combined-report.html +1418 -0
@@ -0,0 +1,1192 @@
1
+ /**
2
+ * Method-Level Call Graph using ts-morph
3
+ * Tracks which methods call which other methods across files
4
+ */
5
+
6
+ import { Project, SyntaxKind } from 'ts-morph';
7
+ import path from 'path';
8
+
9
+ export class MethodCallGraph {
10
+ constructor() {
11
+ this.project = null;
12
+ this.methodCallMap = new Map(); // method -> [callers] (reverse map)
13
+ this.methodCallsMap = new Map(); // method -> [methods it calls] (forward map)
14
+ this.methodToFile = new Map(); // method -> file path
15
+ this.methodToEndpoint = new Map(); // method -> endpoint info
16
+ this.interfaceToClass = new Map(); // interface name -> concrete class name
17
+ this.commandNameToClass = new Map(); // command name -> command handler class
18
+ this.endpointToCommandNames = new Map(); // endpoint method -> [command names]
19
+ this.queueNameToProcessor = new Map(); // queue name -> processor class name
20
+ this.methodToQueueProcessor = new Map(); // method -> queue processor info
21
+ this.endpointToQueueNames = new Map(); // endpoint method -> [queue names]
22
+ }
23
+
24
+ /**
25
+ * Initialize ts-morph project and build call graph
26
+ */
27
+ async initialize(sourceDir, excludePaths = [], verbose = false) {
28
+ this.verbose = verbose;
29
+
30
+ this.project = new Project({
31
+ tsConfigFilePath: path.join(sourceDir, '../tsconfig.json'),
32
+ skipAddingFilesFromTsConfig: true,
33
+ });
34
+
35
+ // Add source files
36
+ this.project.addSourceFilesAtPaths(`${sourceDir}/**/*.ts`);
37
+
38
+ // Filter out excluded paths
39
+ const sourceFiles = this.project.getSourceFiles().filter(sf => {
40
+ const filePath = sf.getFilePath();
41
+ return !excludePaths.some(ex => filePath.includes(ex));
42
+ });
43
+
44
+ for (const sourceFile of sourceFiles) {
45
+ this.extractInterfaceMappings(sourceFile);
46
+ this.extractCommandMappings(sourceFile);
47
+ this.extractQueueProcessorMappings(sourceFile);
48
+ }
49
+
50
+ for (const sourceFile of sourceFiles) {
51
+ this.analyzeFile(sourceFile);
52
+ }
53
+
54
+ return this;
55
+ }
56
+
57
+ /**
58
+ * Analyze a single file to extract method definitions and calls
59
+ */
60
+ analyzeFile(sourceFile) {
61
+ const filePath = sourceFile.getFilePath();
62
+
63
+ // Get all classes
64
+ const classes = sourceFile.getClasses();
65
+
66
+ for (const classDecl of classes) {
67
+ const className = classDecl.getName();
68
+ if (!className) continue; // Skip anonymous classes
69
+
70
+ // Check if this class is a queue processor
71
+ const classDecorators = classDecl.getDecorators();
72
+ const processorDecorator = classDecorators.find(d => d.getName() === 'Processor');
73
+ let processorQueueName = null;
74
+
75
+ if (processorDecorator) {
76
+ const args = processorDecorator.getArguments();
77
+ processorQueueName = args[0]?.getText().replace(/['"]/g, '');
78
+ }
79
+
80
+ // Get all methods in this class
81
+ const methods = classDecl.getMethods();
82
+
83
+ for (const method of methods) {
84
+ const methodName = method.getName();
85
+ const fullMethodName = `${className}.${methodName}`;
86
+
87
+ // Store method location
88
+ this.methodToFile.set(fullMethodName, filePath);
89
+
90
+ // Check if this is an endpoint method (has HTTP decorator)
91
+ const decorators = method.getDecorators();
92
+ const httpDecorator = decorators.find(d =>
93
+ ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head', 'All'].includes(d.getName())
94
+ );
95
+
96
+ if (httpDecorator) {
97
+ const decoratorName = httpDecorator.getName();
98
+ const args = httpDecorator.getArguments();
99
+ const route = args[0]?.getText().replace(/['"]/g, '') || '/';
100
+
101
+ this.methodToEndpoint.set(fullMethodName, {
102
+ method: decoratorName.toUpperCase(),
103
+ path: route,
104
+ controller: className,
105
+ file: filePath,
106
+ });
107
+ }
108
+
109
+ // Check if this is a queue processor method (has @Process decorator)
110
+ const processDecorator = decorators.find(d => d.getName() === 'Process');
111
+ if (processDecorator && processorQueueName) {
112
+ const args = processDecorator.getArguments();
113
+ const jobName = args[0]?.getText().replace(/['"]/g, '') || '';
114
+
115
+ this.methodToQueueProcessor.set(fullMethodName, {
116
+ queueName: processorQueueName,
117
+ jobName: jobName,
118
+ processor: className,
119
+ method: methodName,
120
+ file: filePath,
121
+ });
122
+
123
+ if (this.verbose) {
124
+ console.log(` 🔄 Found queue processor: ${processorQueueName}${jobName ? `/${jobName}` : ''} → ${fullMethodName}`);
125
+ }
126
+ }
127
+
128
+ // Find all method calls within this method
129
+ this.analyzeMethodCalls(method, className, fullMethodName);
130
+
131
+ // Detect command dispatches and queue jobs in ALL methods (not just endpoints)
132
+ // This allows us to track: Controller → Service → Queue → Processor
133
+ this.detectCommandDispatches(method, fullMethodName);
134
+ this.detectQueueDispatches(method, fullMethodName);
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Detect command dispatches in endpoint methods
141
+ * Handles both patterns:
142
+ * 1. Direct string: publishMessage('commandName', ...)
143
+ * 2. Object with command property: publishMessage({ message: { command: 'commandName' } })
144
+ */
145
+ detectCommandDispatches(method, fullMethodName) {
146
+ const callExpressions = method.getDescendantsOfKind(SyntaxKind.CallExpression);
147
+ const commandNames = [];
148
+
149
+ for (const call of callExpressions) {
150
+ const expression = call.getExpression();
151
+
152
+ // Look for method calls that might dispatch commands
153
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
154
+ const methodName = expression.getName();
155
+
156
+ // Common command dispatch patterns (partial match)
157
+ const dispatchMethods = [
158
+ 'sendCommand', 'send', 'execute', 'dispatch',
159
+ 'publish', 'emit', 'trigger', 'enqueue'
160
+ ];
161
+
162
+ // Check if method name contains or matches any dispatch pattern
163
+ const isDispatchMethod = dispatchMethods.some(pattern => {
164
+ const lowerMethodName = methodName.toLowerCase();
165
+ const lowerPattern = pattern.toLowerCase();
166
+
167
+ // Exact match
168
+ if (lowerMethodName === lowerPattern) return true;
169
+
170
+ // Contains pattern (e.g., publishMessage contains 'publish')
171
+ if (lowerMethodName.includes(lowerPattern)) return true;
172
+
173
+ return false;
174
+ });
175
+
176
+ if (isDispatchMethod) {
177
+ const args = call.getArguments();
178
+
179
+ if (args.length > 0) {
180
+ const firstArg = args[0];
181
+
182
+ // Pattern 1: Direct string literal
183
+ // Example: publishMessage('commandName', ...)
184
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
185
+ const commandName = firstArg.getText().replace(/['"]/g, '');
186
+ commandNames.push(commandName);
187
+
188
+ if (this.verbose) {
189
+ console.log(` 📨 Found command dispatch: ${fullMethodName} → '${commandName}' (via ${methodName})`);
190
+ }
191
+ }
192
+
193
+ // Pattern 2: Object literal with command property
194
+ // Example: publishMessage({ message: { command: 'commandName' } })
195
+ else if (firstArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
196
+ const commandName = this.extractCommandFromObject(firstArg, method);
197
+
198
+ if (commandName) {
199
+ commandNames.push(commandName);
200
+
201
+ if (this.verbose) {
202
+ console.log(` 📨 Found command dispatch: ${fullMethodName} → '${commandName}' (via ${methodName}, object pattern)`);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+
211
+ if (commandNames.length > 0) {
212
+ this.endpointToCommandNames.set(fullMethodName, commandNames);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Extract command name from object literal
218
+ * Handles patterns like:
219
+ * - { command: 'xxx' }
220
+ * - { message: { command: 'xxx' } }
221
+ * - { command: variable } (resolves variable value if possible)
222
+ * - { command } (ES6 shorthand - resolves variable)
223
+ */
224
+ extractCommandFromObject(objectLiteral, method) {
225
+ const properties = objectLiteral.getProperties();
226
+
227
+ for (const prop of properties) {
228
+ const propKind = prop.getKind();
229
+
230
+ // Handle ES6 shorthand: { command } → same as { command: command }
231
+ if (propKind === SyntaxKind.ShorthandPropertyAssignment) {
232
+ const propName = prop.getName();
233
+
234
+ if (propName === 'command') {
235
+ // The property name IS the variable name in shorthand
236
+ const variableName = propName;
237
+
238
+ // Try to resolve the variable value
239
+ const resolvedValue = this.resolveVariableValue(variableName, method);
240
+
241
+ if (resolvedValue) {
242
+ return resolvedValue;
243
+ }
244
+
245
+ // Fallback: return variable name if can't resolve
246
+ return variableName;
247
+ }
248
+ }
249
+
250
+ // Handle regular property assignment: { command: 'xxx' } or { command: variable }
251
+ if (propKind === SyntaxKind.PropertyAssignment) {
252
+ const propName = prop.getName();
253
+ const initializer = prop.getInitializer();
254
+
255
+ // Direct: { command: 'xxx' }
256
+ if (propName === 'command') {
257
+ if (initializer && initializer.getKind() === SyntaxKind.StringLiteral) {
258
+ return initializer.getText().replace(/['"]/g, '');
259
+ }
260
+
261
+ // { command: variable } - resolve variable value
262
+ if (initializer && initializer.getKind() === SyntaxKind.Identifier) {
263
+ const variableName = initializer.getText();
264
+
265
+ // Try to resolve the variable value
266
+ const resolvedValue = this.resolveVariableValue(variableName, method);
267
+
268
+ if (resolvedValue) {
269
+ return resolvedValue;
270
+ }
271
+
272
+ // Fallback: return variable name if can't resolve
273
+ return variableName;
274
+ }
275
+ }
276
+
277
+ // Nested: { message: { command: 'xxx' } } or { message: { command } }
278
+ if (propName === 'message' && initializer &&
279
+ initializer.getKind() === SyntaxKind.ObjectLiteralExpression) {
280
+ const nestedCommand = this.extractCommandFromObject(initializer, method);
281
+ if (nestedCommand) return nestedCommand;
282
+ }
283
+ }
284
+ }
285
+
286
+ return null;
287
+ }
288
+
289
+ /**
290
+ * Resolve variable value from its declaration
291
+ * Example: const command = 'exportCsv'; → returns 'exportCsv'
292
+ */
293
+ resolveVariableValue(variableName, method) {
294
+ if (!method) return null;
295
+
296
+ // Get all variable declarations in this method
297
+ const variableDeclarations = method.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
298
+
299
+ for (const varDecl of variableDeclarations) {
300
+ if (varDecl.getName() === variableName) {
301
+ const initializer = varDecl.getInitializer();
302
+
303
+ // If initialized with string literal
304
+ if (initializer && initializer.getKind() === SyntaxKind.StringLiteral) {
305
+ return initializer.getText().replace(/['"]/g, '');
306
+ }
307
+
308
+ // If initialized with another identifier, try to resolve that too (one level)
309
+ if (initializer && initializer.getKind() === SyntaxKind.Identifier) {
310
+ const nestedVarName = initializer.getText();
311
+ return this.resolveVariableValue(nestedVarName, method);
312
+ }
313
+ }
314
+ }
315
+
316
+ // Check method parameters
317
+ const parameters = method.getParameters();
318
+ for (const param of parameters) {
319
+ if (param.getName() === variableName) {
320
+ // Can't resolve parameter values statically
321
+ return variableName;
322
+ }
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ /**
329
+ * Extract command name -> class mappings from @Command decorators
330
+ */
331
+ extractCommandMappings(sourceFile) {
332
+ const classes = sourceFile.getClasses();
333
+
334
+ for (const classDecl of classes) {
335
+ const commandDecorator = classDecl.getDecorator('Command');
336
+ if (!commandDecorator) continue;
337
+
338
+ const className = classDecl.getName();
339
+ if (!className) continue;
340
+
341
+ const args = commandDecorator.getArguments();
342
+ if (args.length === 0) continue;
343
+
344
+ const configObj = args[0];
345
+ if (!configObj) continue;
346
+
347
+ const text = configObj.getText();
348
+
349
+ // Pattern: { name: 'commandName', ... }
350
+ const nameMatch = text.match(/name:\s*['"]([^'"]+)['"]/);
351
+
352
+ if (nameMatch) {
353
+ const commandName = nameMatch[1];
354
+ this.commandNameToClass.set(commandName, className);
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Extract interface -> class mappings from NestJS modules
361
+ */
362
+ extractInterfaceMappings(sourceFile) {
363
+ // Look for @Module decorators
364
+ const classes = sourceFile.getClasses();
365
+
366
+ for (const classDecl of classes) {
367
+ const moduleDecorator = classDecl.getDecorator('Module');
368
+ if (!moduleDecorator) continue;
369
+
370
+ const args = moduleDecorator.getArguments();
371
+ if (args.length === 0) continue;
372
+
373
+ const configObj = args[0];
374
+ if (!configObj) continue;
375
+
376
+ // Parse the configuration object
377
+ const text = configObj.getText();
378
+
379
+ // Pattern: { provide: 'IXxx', useClass: Xxx }
380
+ // or: { provide: 'IXxx', useClass: Xxx, }
381
+ const providerPattern = /\{\s*provide:\s*['"](\w+)['"]\s*,\s*useClass:\s*(\w+)/g;
382
+
383
+ let match;
384
+ while ((match = providerPattern.exec(text)) !== null) {
385
+ const interfaceName = match[1];
386
+ const className = match[2];
387
+
388
+ this.interfaceToClass.set(interfaceName, className);
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Analyze all method calls within a method
395
+ */
396
+ analyzeMethodCalls(method, className, fullMethodName) {
397
+ const callExpressions = method.getDescendantsOfKind(SyntaxKind.CallExpression);
398
+
399
+ for (const call of callExpressions) {
400
+ const expression = call.getExpression();
401
+
402
+ // Handle method calls: this.method(), service.method(), this.repo.method()
403
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
404
+ const calledMethod = expression.getName();
405
+ const objExpression = expression.getExpression();
406
+
407
+ let calledClassName = null;
408
+
409
+ // Special handling for 'this' keyword
410
+ if (objExpression.getKind() === SyntaxKind.ThisKeyword) {
411
+ calledClassName = className;
412
+ } else {
413
+ // Use TypeScript's type checker to resolve the type
414
+ const type = objExpression.getType();
415
+ const symbol = type.getSymbol();
416
+
417
+ if (symbol) {
418
+ calledClassName = symbol.getName();
419
+
420
+ // Check if this is an interface - map to concrete class
421
+ if (this.interfaceToClass.has(calledClassName)) {
422
+ const concreteClass = this.interfaceToClass.get(calledClassName);
423
+ calledClassName = concreteClass;
424
+ }
425
+
426
+ // Handle interface naming convention (IXxx -> Xxx)
427
+ if (calledClassName.startsWith('I') && calledClassName.length > 1) {
428
+ const possibleClassName = calledClassName.substring(1);
429
+ if (this.classExists(possibleClassName)) {
430
+ calledClassName = possibleClassName;
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ if (calledClassName) {
437
+ this.recordMethodCall(fullMethodName, calledClassName, calledMethod);
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Check if a class exists in the project
445
+ */
446
+ classExists(className) {
447
+ for (const [methodName] of this.methodToFile) {
448
+ if (methodName.startsWith(`${className}.`)) {
449
+ return true;
450
+ }
451
+ }
452
+ return false;
453
+ }
454
+
455
+ /**
456
+ * Record a method call in both forward and reverse maps
457
+ */
458
+ recordMethodCall(callerFullName, calleeClassName, calleeMethodName) {
459
+ const calleeFullName = `${calleeClassName}.${calleeMethodName}`;
460
+
461
+ // Add to reverse map (who calls this method)
462
+ if (!this.methodCallMap.has(calleeFullName)) {
463
+ this.methodCallMap.set(calleeFullName, []);
464
+ }
465
+
466
+ const callers = this.methodCallMap.get(calleeFullName);
467
+ if (!callers.includes(callerFullName)) {
468
+ callers.push(callerFullName);
469
+ }
470
+
471
+ // Add to forward map (what this method calls)
472
+ if (!this.methodCallsMap.has(callerFullName)) {
473
+ this.methodCallsMap.set(callerFullName, []);
474
+ }
475
+
476
+ const calls = this.methodCallsMap.get(callerFullName);
477
+ if (!calls.includes(calleeFullName)) {
478
+ calls.push(calleeFullName);
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Get changed methods from git diff
484
+ * Uses ts-morph AST to detect which specific methods contain changes
485
+ */
486
+ getChangedMethods(diff, filePath) {
487
+ if (!diff || !filePath) return [];
488
+
489
+ const sourceFile = this.project.getSourceFile(filePath);
490
+ if (!sourceFile) return [];
491
+
492
+ // Extract changed line numbers from diff
493
+ const changedLines = this.extractChangedLineNumbers(diff);
494
+ if (changedLines.length === 0) return [];
495
+
496
+ const changedMethods = [];
497
+
498
+ // Get all classes in the file
499
+ const classes = sourceFile.getClasses();
500
+
501
+ for (const classDecl of classes) {
502
+ const className = classDecl.getName();
503
+ if (!className) continue;
504
+
505
+ const methods = classDecl.getMethods();
506
+
507
+ for (const method of methods) {
508
+ const methodName = method.getName();
509
+
510
+ // Check if this specific method contains changes
511
+ if (this.methodContainsChanges(method, changedLines)) {
512
+ changedMethods.push({
513
+ className: className,
514
+ methodName: methodName,
515
+ file: filePath,
516
+ fullName: `${className}.${methodName}`
517
+ });
518
+ }
519
+ }
520
+ }
521
+
522
+ return changedMethods;
523
+ }
524
+
525
+ /**
526
+ * Check if a method contains actual code changes using ts-morph AST
527
+ */
528
+ methodContainsChanges(method, changedLines) {
529
+ const body = method.getBody();
530
+ if (!body) return false;
531
+
532
+ // Get the method body block (excluding signature and decorators)
533
+ const bodyStartLine = body.getStartLineNumber();
534
+ const bodyEndLine = body.getEndLineNumber();
535
+
536
+ // Check if any changed line is within the method body
537
+ const hasChangesInBody = changedLines.some(
538
+ line => line >= bodyStartLine && line <= bodyEndLine
539
+ );
540
+
541
+ return hasChangesInBody;
542
+ }
543
+
544
+ /**
545
+ * Extract line numbers that were changed from git diff
546
+ */
547
+ extractChangedLineNumbers(diff) {
548
+ const changedLines = [];
549
+ const lines = diff.split('\n');
550
+ let currentLine = 0;
551
+
552
+ for (const line of lines) {
553
+ // Parse diff hunk headers: @@ -old +new @@
554
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
555
+ if (hunkMatch) {
556
+ currentLine = parseInt(hunkMatch[1], 10);
557
+ continue;
558
+ }
559
+
560
+ // Track line numbers for added/modified lines
561
+ if (line.startsWith('+') && !line.startsWith('+++')) {
562
+ changedLines.push(currentLine);
563
+ currentLine++;
564
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
565
+ // Don't increment for deleted lines (they don't exist in new file)
566
+ continue;
567
+ } else if (!line.startsWith('\\')) {
568
+ // Context lines (no +/-)
569
+ currentLine++;
570
+ }
571
+ }
572
+
573
+ return changedLines;
574
+ }
575
+
576
+ /**
577
+ * Resolve object type from variable/property name
578
+ */
579
+ resolveObjectType(objectName, sourceFile, classes) {
580
+ // Handle 'this.property' pattern
581
+ if (objectName.startsWith('this.')) {
582
+ const propertyName = objectName.replace('this.', '');
583
+ if (classes.length > 0) {
584
+ return this.resolvePropertyType(classes[0], propertyName);
585
+ }
586
+ return null;
587
+ }
588
+
589
+ // Check if it's 'this' or 'super'
590
+ if (objectName === 'this' && classes.length > 0) {
591
+ return classes[0].getName();
592
+ }
593
+
594
+ // Try to find the type from the current class
595
+ for (const classDecl of classes) {
596
+ // Check constructor parameters (DI)
597
+ const constructor = classDecl.getConstructors()[0];
598
+ if (constructor) {
599
+ const params = constructor.getParameters();
600
+ for (const param of params) {
601
+ const paramName = param.getName();
602
+ if (paramName === objectName) {
603
+ const type = param.getType();
604
+ const symbol = type.getSymbol();
605
+ if (symbol) {
606
+ return symbol.getName();
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // Check class properties
613
+ const properties = classDecl.getProperties();
614
+ for (const prop of properties) {
615
+ if (prop.getName() === objectName) {
616
+ const type = prop.getType();
617
+ const symbol = type.getSymbol();
618
+ if (symbol) {
619
+ return symbol.getName();
620
+ }
621
+ }
622
+ }
623
+
624
+ // Check local variables in methods (simplified)
625
+ const methods = classDecl.getMethods();
626
+ for (const method of methods) {
627
+ const variableDeclarations = method.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
628
+ for (const varDecl of variableDeclarations) {
629
+ if (varDecl.getName() === objectName) {
630
+ const type = varDecl.getType();
631
+ const symbol = type.getSymbol();
632
+ if (symbol) {
633
+ return symbol.getName();
634
+ }
635
+ }
636
+ }
637
+ }
638
+ }
639
+
640
+ // If we can't resolve, try to guess from naming convention
641
+ return this.guessTypeFromVariableName(objectName);
642
+ }
643
+
644
+ /**
645
+ * Guess class name from variable name using naming conventions
646
+ */
647
+ guessTypeFromVariableName(variableName) {
648
+ // Convert camelCase to PascalCase
649
+ return variableName.charAt(0).toUpperCase() + variableName.slice(1);
650
+ }
651
+
652
+ /**
653
+ * Get direct callers of a method
654
+ */
655
+ getCallers(methodName) {
656
+ return this.methodCallMap.get(methodName) || [];
657
+ }
658
+
659
+ /**
660
+ * Find all callers of a method (recursive)
661
+ */
662
+ findAllCallers(methodName, visited = new Set(), depth = 0) {
663
+ if (visited.has(methodName)) return [];
664
+
665
+ visited.add(methodName);
666
+ const directCallers = this.methodCallMap.get(methodName) || [];
667
+ let allCallers = [...directCallers];
668
+
669
+ if (this.verbose && depth === 0) {
670
+ console.log(` Tracing callers of: ${methodName}`);
671
+ console.log(` Direct callers: ${directCallers.join(', ') || 'none'}`);
672
+ }
673
+
674
+ // Recursively find callers of callers
675
+ for (const caller of directCallers) {
676
+ if (this.verbose && depth === 0) {
677
+ console.log(` Tracing up from: ${caller}`);
678
+ }
679
+ const indirectCallers = this.findAllCallers(caller, visited, depth + 1);
680
+ allCallers = allCallers.concat(indirectCallers);
681
+ }
682
+
683
+ return [...new Set(allCallers)]; // Remove duplicates
684
+ }
685
+
686
+ /**
687
+ * Find affected endpoints from changed methods
688
+ * Traverses up the call chain: Repository → Service → Controller → Endpoint
689
+ */
690
+ findAffectedEndpoints(changedMethods) {
691
+ const affectedEndpoints = [];
692
+ const processedMethods = new Set();
693
+
694
+ const methodNames = changedMethods.map(m =>
695
+ typeof m === 'string' ? m : m.fullName
696
+ );
697
+
698
+ if (this.verbose) {
699
+ console.log('\n 🔍 Finding affected endpoints...');
700
+ console.log(` Changed methods: ${methodNames.join(', ')}`);
701
+ }
702
+
703
+ for (const changedMethod of methodNames) {
704
+ const startLayer = this.getMethodLayer(changedMethod);
705
+
706
+ if (this.verbose) {
707
+ console.log(`\n 📌 Processing: ${changedMethod} (${startLayer})`);
708
+ }
709
+
710
+ const callers = this.findAllCallers(changedMethod);
711
+
712
+ if (this.verbose) {
713
+ console.log(` All callers (${callers.length}): ${callers.join(', ') || 'none'}`);
714
+ }
715
+
716
+ const endpointCallers = this.filterUpToEndpoints(changedMethod, callers);
717
+
718
+ if (this.verbose) {
719
+ console.log(` Endpoint callers (${endpointCallers.length}): ${endpointCallers.join(', ') || 'none'}`);
720
+ }
721
+
722
+ for (const caller of endpointCallers) {
723
+ if (processedMethods.has(caller)) continue;
724
+ processedMethods.add(caller);
725
+
726
+ const endpoint = this.methodToEndpoint.get(caller);
727
+ if (endpoint) {
728
+ const callChain = this.getCallChain(changedMethod, caller);
729
+
730
+ if (this.verbose) {
731
+ console.log(` ✅ Found endpoint: ${endpoint.method} ${endpoint.path}`);
732
+ console.log(` Call chain: ${callChain.join(' → ')}`);
733
+ }
734
+
735
+ affectedEndpoints.push({
736
+ ...endpoint,
737
+ affectedBy: changedMethod,
738
+ callChain: callChain,
739
+ layers: this.getCallChainLayers(callChain),
740
+ impactLevel: this.calculateImpactLevel(callChain),
741
+ });
742
+ }
743
+ }
744
+
745
+ const endpoint = this.methodToEndpoint.get(changedMethod);
746
+ if (endpoint && !processedMethods.has(changedMethod)) {
747
+ processedMethods.add(changedMethod);
748
+
749
+ if (this.verbose) {
750
+ console.log(` ✅ Changed method itself is an endpoint: ${endpoint.method} ${endpoint.path}`);
751
+ }
752
+
753
+ affectedEndpoints.push({
754
+ ...endpoint,
755
+ affectedBy: changedMethod,
756
+ callChain: [changedMethod],
757
+ layers: [startLayer],
758
+ impactLevel: 'high', // Direct endpoint change is always high impact
759
+ });
760
+ }
761
+
762
+ // Check if changed method is in a Command handler
763
+ const commandEndpoints = this.findEndpointsByCommand(changedMethod);
764
+ for (const cmdEndpoint of commandEndpoints) {
765
+ if (processedMethods.has(cmdEndpoint.endpointMethod)) continue;
766
+ processedMethods.add(cmdEndpoint.endpointMethod);
767
+
768
+ affectedEndpoints.push(cmdEndpoint);
769
+ }
770
+
771
+ // NEW: Check if changed method is in a Queue Processor
772
+ const queueEndpoints = this.findEndpointsByQueue(changedMethod);
773
+ for (const queueEndpoint of queueEndpoints) {
774
+ const endpointKey = queueEndpoint.endpointMethod || queueEndpoint.path;
775
+ if (processedMethods.has(endpointKey)) continue;
776
+ processedMethods.add(endpointKey);
777
+
778
+ affectedEndpoints.push(queueEndpoint);
779
+ }
780
+
781
+ // NEW: Check if any CALLER is in a Command handler
782
+ // Flow: ServiceB (changed) → ServiceA → Command.run → find endpoint
783
+ for (const caller of callers) {
784
+ const callerCommandEndpoints = this.findEndpointsByCommand(caller);
785
+ for (const cmdEndpoint of callerCommandEndpoints) {
786
+ if (processedMethods.has(cmdEndpoint.endpointMethod)) continue;
787
+ processedMethods.add(cmdEndpoint.endpointMethod);
788
+
789
+ if (this.verbose) {
790
+ console.log(` 🔗 Caller '${caller}' is in a command handler`);
791
+ }
792
+
793
+ affectedEndpoints.push(cmdEndpoint);
794
+ }
795
+
796
+ // NEW: Check if any CALLER is in a Queue Processor
797
+ const callerQueueEndpoints = this.findEndpointsByQueue(caller);
798
+ for (const queueEndpoint of callerQueueEndpoints) {
799
+ const endpointKey = queueEndpoint.endpointMethod || queueEndpoint.path;
800
+ if (processedMethods.has(endpointKey)) continue;
801
+ processedMethods.add(endpointKey);
802
+
803
+ if (this.verbose) {
804
+ console.log(` 🔗 Caller '${caller}' is in a queue processor`);
805
+ }
806
+
807
+ affectedEndpoints.push(queueEndpoint);
808
+ }
809
+ }
810
+ }
811
+
812
+ return affectedEndpoints;
813
+ }
814
+
815
+ /**
816
+ * Find endpoints that dispatch commands handled by the changed method
817
+ */
818
+ findEndpointsByCommand(changedMethod) {
819
+ const affectedEndpoints = [];
820
+
821
+ // Get the class name from the changed method
822
+ const className = changedMethod.split('.')[0];
823
+
824
+ // Check if this class is a command handler
825
+ let commandName = null;
826
+ for (const [cmdName, cmdClass] of this.commandNameToClass) {
827
+ if (cmdClass === className) {
828
+ commandName = cmdName;
829
+ break;
830
+ }
831
+ }
832
+
833
+ if (!commandName) {
834
+ return affectedEndpoints;
835
+ }
836
+
837
+ // Find endpoints that dispatch this command
838
+ for (const [endpointMethod, commandNames] of this.endpointToCommandNames) {
839
+ if (commandNames.includes(commandName)) {
840
+ const endpoint = this.methodToEndpoint.get(endpointMethod);
841
+
842
+ if (endpoint) {
843
+ if (this.verbose) {
844
+ console.log(` ✅ Found endpoint via command: ${endpoint.method} ${endpoint.path}`);
845
+ console.log(` Command chain: ${changedMethod} ← '${commandName}' ← ${endpointMethod}`);
846
+ }
847
+
848
+ affectedEndpoints.push({
849
+ ...endpoint,
850
+ affectedBy: changedMethod,
851
+ callChain: [changedMethod, `Command: '${commandName}'`, endpointMethod],
852
+ layers: [this.getMethodLayer(changedMethod), 'Command', this.getMethodLayer(endpointMethod)],
853
+ viaCommand: commandName,
854
+ endpointMethod: endpointMethod,
855
+ impactLevel: this.calculateImpactLevel([changedMethod, `Command: '${commandName}'`, endpointMethod]),
856
+ });
857
+ }
858
+ }
859
+ }
860
+
861
+ return affectedEndpoints;
862
+ }
863
+
864
+ /**
865
+ * Find endpoints that dispatch queue jobs handled by the changed method
866
+ * Handles two patterns:
867
+ * 1. Controller → Queue.add() → Processor (direct)
868
+ * 2. Controller → Service → Queue.add() → Processor (indirect)
869
+ */
870
+ findEndpointsByQueue(changedMethod) {
871
+ const affectedEndpoints = [];
872
+
873
+ // Check if this method is a queue processor method
874
+ const queueProcessorInfo = this.methodToQueueProcessor.get(changedMethod);
875
+
876
+ if (!queueProcessorInfo) {
877
+ return affectedEndpoints;
878
+ }
879
+
880
+ const { queueName, jobName } = queueProcessorInfo;
881
+ const queueLabel = jobName ? `${queueName}/${jobName}` : queueName;
882
+
883
+ // Find all methods (endpoints OR services) that dispatch to this queue
884
+ for (const [methodName, queueNames] of this.endpointToQueueNames) {
885
+ if (queueNames.includes(queueName)) {
886
+ const endpoint = this.methodToEndpoint.get(methodName);
887
+
888
+ if (endpoint) {
889
+ // Pattern 1: Direct endpoint → queue
890
+ if (this.verbose) {
891
+ console.log(` ✅ Found endpoint via queue (direct): ${endpoint.method} ${endpoint.path}`);
892
+ console.log(` Queue chain: ${changedMethod} ← Queue '${queueLabel}' ← ${methodName}`);
893
+ }
894
+
895
+ affectedEndpoints.push({
896
+ ...endpoint,
897
+ affectedBy: changedMethod,
898
+ callChain: [changedMethod, `Queue: '${queueLabel}'`, methodName],
899
+ layers: [this.getMethodLayer(changedMethod), 'Queue', this.getMethodLayer(methodName)],
900
+ viaQueue: queueLabel,
901
+ endpointMethod: methodName,
902
+ impactLevel: this.calculateImpactLevel([changedMethod, `Queue: '${queueLabel}'`, methodName]),
903
+ });
904
+ } else {
905
+ // Pattern 2: Service method triggers queue, find controllers that call this service
906
+ // Flow: Processor ← Queue ← Service ← Controller
907
+ const serviceCallers = this.findAllCallers(methodName);
908
+ const endpointCallers = this.filterUpToEndpoints(methodName, serviceCallers);
909
+
910
+ if (this.verbose && endpointCallers.length > 0) {
911
+ console.log(` 🔗 Service method '${methodName}' triggers queue '${queueLabel}'`);
912
+ console.log(` Finding controllers that call this service...`);
913
+ }
914
+
915
+ for (const endpointCaller of endpointCallers) {
916
+ const endpointInfo = this.methodToEndpoint.get(endpointCaller);
917
+
918
+ if (endpointInfo) {
919
+ const callChain = this.getCallChain(methodName, endpointCaller);
920
+ const fullChain = [changedMethod, `Queue: '${queueLabel}'`, ...callChain];
921
+
922
+ if (this.verbose) {
923
+ console.log(` ✅ Found endpoint via queue (indirect): ${endpointInfo.method} ${endpointInfo.path}`);
924
+ console.log(` Full chain: ${fullChain.join(' → ')}`);
925
+ }
926
+
927
+ affectedEndpoints.push({
928
+ ...endpointInfo,
929
+ affectedBy: changedMethod,
930
+ callChain: fullChain,
931
+ layers: [this.getMethodLayer(changedMethod), 'Queue', ...this.getCallChainLayers(callChain)],
932
+ viaQueue: queueLabel,
933
+ endpointMethod: endpointCaller,
934
+ impactLevel: this.calculateImpactLevel(fullChain),
935
+ });
936
+ }
937
+ }
938
+ }
939
+ }
940
+ }
941
+
942
+ return affectedEndpoints;
943
+ }
944
+
945
+ /**
946
+ * Get the layer/tier of a method based on file path and class name
947
+ */
948
+ getMethodLayer(methodName) {
949
+ const filePath = this.methodToFile.get(methodName);
950
+ if (!filePath) return 'Unknown';
951
+
952
+ const lowerPath = filePath.toLowerCase();
953
+ const fileName = filePath.split('/').pop().toLowerCase();
954
+
955
+ // Check file path for layer indicators (order matters - most specific first)
956
+ if (lowerPath.includes('/controllers/') || fileName.includes('.controller.')) return 'Controller';
957
+ if (lowerPath.includes('/processors/') || fileName.includes('.processor.')) return 'QueueProcessor';
958
+ if (lowerPath.includes('/services/') || fileName.includes('.service.')) return 'Service';
959
+ if (lowerPath.includes('/repositories/') || fileName.includes('.repository.')) return 'Repository';
960
+ if (lowerPath.includes('/providers/') || fileName.includes('.provider.')) return 'Provider';
961
+ if (lowerPath.includes('/commands/') || fileName.includes('.command.')) return 'Command';
962
+ if (lowerPath.includes('/queries/') || fileName.includes('.query.')) return 'Query';
963
+ if (lowerPath.includes('/handlers/') || fileName.includes('.handler.')) return 'Handler';
964
+
965
+ // Additional layers you mentioned
966
+ if (lowerPath.includes('/dto/') || fileName.includes('.dto.')) return 'DTO';
967
+ if (lowerPath.includes('/request/') || fileName.includes('.request.')) return 'Request';
968
+ if (lowerPath.includes('/response/') || fileName.includes('.response.')) return 'Response';
969
+ if (lowerPath.includes('/config/') || fileName.includes('.config.')) return 'Config';
970
+ if (lowerPath.includes('/modules/') || fileName.includes('.module.')) return 'Module';
971
+ if (lowerPath.includes('/entities/') || fileName.includes('.entity.')) return 'Entity';
972
+ if (lowerPath.includes('/models/') || fileName.includes('.model.')) return 'Model';
973
+
974
+ // Common utility/helper patterns
975
+ if (lowerPath.includes('/helpers/') || fileName.includes('.helper.')) return 'Helper';
976
+ if (lowerPath.includes('/utils/') || fileName.includes('.util.')) return 'Utility';
977
+ if (lowerPath.includes('/middleware/') || fileName.includes('.middleware.')) return 'Middleware';
978
+ if (lowerPath.includes('/guards/') || fileName.includes('.guard.')) return 'Guard';
979
+ if (lowerPath.includes('/interceptors/') || fileName.includes('.interceptor.')) return 'Interceptor';
980
+ if (lowerPath.includes('/decorators/') || fileName.includes('.decorator.')) return 'Decorator';
981
+ if (lowerPath.includes('/pipes/') || fileName.includes('.pipe.')) return 'Pipe';
982
+ if (lowerPath.includes('/filters/') || fileName.includes('.filter.')) return 'Filter';
983
+
984
+ // Check class name
985
+ const className = methodName.split('.')[0];
986
+ if (className.endsWith('Controller')) return 'Controller';
987
+ if (className.endsWith('Service')) return 'Service';
988
+ if (className.endsWith('Repository')) return 'Repository';
989
+ if (className.endsWith('Provider')) return 'Provider';
990
+ if (className.endsWith('Command')) return 'Command';
991
+ if (className.endsWith('Query')) return 'Query';
992
+ if (className.endsWith('Handler')) return 'Handler';
993
+ if (className.endsWith('Dto') || className.endsWith('DTO')) return 'DTO';
994
+ if (className.endsWith('Request')) return 'Request';
995
+ if (className.endsWith('Response')) return 'Response';
996
+ if (className.endsWith('Config')) return 'Config';
997
+ if (className.endsWith('Module')) return 'Module';
998
+ if (className.endsWith('Entity')) return 'Entity';
999
+ if (className.endsWith('Model')) return 'Model';
1000
+ if (className.endsWith('Helper')) return 'Helper';
1001
+ if (className.endsWith('Util') || className.endsWith('Utils')) return 'Utility';
1002
+ if (className.endsWith('Middleware')) return 'Middleware';
1003
+ if (className.endsWith('Guard')) return 'Guard';
1004
+ if (className.endsWith('Interceptor')) return 'Interceptor';
1005
+ if (className.endsWith('Decorator')) return 'Decorator';
1006
+ if (className.endsWith('Pipe')) return 'Pipe';
1007
+ if (className.endsWith('Filter')) return 'Filter';
1008
+
1009
+ return 'Other';
1010
+ }
1011
+
1012
+ /**
1013
+ * Filter callers up through layers to find endpoints
1014
+ * Repository → Service → Controller (endpoint)
1015
+ */
1016
+ filterUpToEndpoints(changedMethod, allCallers) {
1017
+ return allCallers.filter(caller => this.methodToEndpoint.has(caller));
1018
+ }
1019
+
1020
+ /**
1021
+ * Get layers for each method in call chain
1022
+ */
1023
+ getCallChainLayers(callChain) {
1024
+ return callChain.map(method => {
1025
+ if (method === '...') return '...';
1026
+ return this.getMethodLayer(method);
1027
+ });
1028
+ }
1029
+
1030
+ /**
1031
+ * Get the call chain from changed method to endpoint
1032
+ */
1033
+ getCallChain(fromMethod, toMethod) {
1034
+ // Simple BFS to find path
1035
+ const queue = [[fromMethod]];
1036
+ const visited = new Set();
1037
+
1038
+ while (queue.length > 0) {
1039
+ const path = queue.shift();
1040
+ const current = path[path.length - 1];
1041
+
1042
+ if (current === toMethod) {
1043
+ return path;
1044
+ }
1045
+
1046
+ if (visited.has(current)) continue;
1047
+ visited.add(current);
1048
+
1049
+ const callers = this.methodCallMap.get(current) || [];
1050
+ for (const caller of callers) {
1051
+ queue.push([...path, caller]);
1052
+ }
1053
+ }
1054
+
1055
+ return [fromMethod, '...', toMethod];
1056
+ }
1057
+
1058
+ /**
1059
+ * Get statistics
1060
+ */
1061
+ getStats() {
1062
+ return {
1063
+ totalMethods: this.methodToFile.size,
1064
+ totalEndpoints: this.methodToEndpoint.size,
1065
+ totalCallRelationships: Array.from(this.methodCallMap.values())
1066
+ .reduce((sum, callers) => sum + callers.length, 0),
1067
+ };
1068
+ }
1069
+
1070
+ /**
1071
+ * Calculate impact level based on call chain length
1072
+ * Shorter chain = higher impact (closer to endpoint)
1073
+ */
1074
+ calculateImpactLevel(callChain) {
1075
+ const chainLength = callChain.length;
1076
+
1077
+ // Direct endpoint change or very short chain
1078
+ if (chainLength <= 1) return 'high';
1079
+
1080
+ // Short chain (2-3 hops)
1081
+ if (chainLength <= 3) return 'medium';
1082
+
1083
+ // Longer chain
1084
+ return 'low';
1085
+ }
1086
+
1087
+ /**
1088
+ * Extract queue name -> processor class mappings from @Processor decorators
1089
+ */
1090
+ extractQueueProcessorMappings(sourceFile) {
1091
+ const classes = sourceFile.getClasses();
1092
+
1093
+ for (const classDecl of classes) {
1094
+ const className = classDecl.getName();
1095
+ if (!className) continue;
1096
+
1097
+ const decorators = classDecl.getDecorators();
1098
+ const processorDecorator = decorators.find(d => d.getName() === 'Processor');
1099
+
1100
+ if (processorDecorator) {
1101
+ const args = processorDecorator.getArguments();
1102
+ if (args.length > 0) {
1103
+ const queueName = args[0].getText().replace(/['"]/g, '');
1104
+ this.queueNameToProcessor.set(queueName, className);
1105
+
1106
+ if (this.verbose) {
1107
+ console.log(` 🔄 Mapped queue processor: '${queueName}' → ${className}`);
1108
+ }
1109
+ }
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ /**
1115
+ * Detect queue job dispatches in endpoint methods
1116
+ * Handles patterns like:
1117
+ * - await this.queueName.add('job-name', data)
1118
+ * - await queue.add(data)
1119
+ */
1120
+ detectQueueDispatches(method, fullMethodName) {
1121
+ const callExpressions = method.getDescendantsOfKind(SyntaxKind.CallExpression);
1122
+ const queueNames = [];
1123
+
1124
+ for (const call of callExpressions) {
1125
+ const expression = call.getExpression();
1126
+
1127
+ // Look for .add() method calls
1128
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
1129
+ const methodName = expression.getName();
1130
+
1131
+ if (methodName === 'add') {
1132
+ // Get the object being called (e.g., 'this.queueName' or 'queue')
1133
+ const objectExpr = expression.getExpression();
1134
+
1135
+ // Check if it's a property access like 'this.queueName'
1136
+ if (objectExpr.getKind() === SyntaxKind.PropertyAccessExpression) {
1137
+ const queuePropertyName = objectExpr.getName();
1138
+
1139
+ // Try to find @InjectQueue decorator in constructor
1140
+ const classDecl = method.getParent();
1141
+ if (classDecl && classDecl.getKind() === SyntaxKind.ClassDeclaration) {
1142
+ const constructor = classDecl.getConstructors()[0];
1143
+ if (constructor) {
1144
+ const queueName = this.findQueueNameFromConstructor(constructor, queuePropertyName);
1145
+ if (queueName && !queueNames.includes(queueName)) {
1146
+ queueNames.push(queueName);
1147
+
1148
+ if (this.verbose) {
1149
+ console.log(` 📤 Found queue dispatch: ${fullMethodName} → queue '${queueName}'`);
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ if (queueNames.length > 0) {
1160
+ this.endpointToQueueNames.set(fullMethodName, queueNames);
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Find queue name from @InjectQueue decorator in constructor
1166
+ */
1167
+ findQueueNameFromConstructor(constructor, propertyName) {
1168
+ const parameters = constructor.getParameters();
1169
+
1170
+ for (const param of parameters) {
1171
+ // Check if parameter name matches (e.g., 'queueName' parameter)
1172
+ const paramName = param.getName();
1173
+
1174
+ // Match patterns like:
1175
+ // - private readonly queueName: Queue
1176
+ // - private queueName: Queue
1177
+ if (paramName === propertyName || paramName.includes(propertyName)) {
1178
+ const decorators = param.getDecorators();
1179
+ const injectQueueDecorator = decorators.find(d => d.getName() === 'InjectQueue');
1180
+
1181
+ if (injectQueueDecorator) {
1182
+ const args = injectQueueDecorator.getArguments();
1183
+ if (args.length > 0) {
1184
+ return args[0].getText().replace(/['"]/g, '');
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ return null;
1191
+ }
1192
+ }