@sun-asterisk/impact-analyzer 1.0.6 → 1.0.8

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.
@@ -16,6 +16,9 @@ export class MethodCallGraph {
16
16
  this.interfaceToClass = new Map(); // interface name -> concrete class name
17
17
  this.commandNameToClass = new Map(); // command name -> command handler class
18
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]
19
22
  }
20
23
 
21
24
  /**
@@ -41,6 +44,7 @@ export class MethodCallGraph {
41
44
  for (const sourceFile of sourceFiles) {
42
45
  this.extractInterfaceMappings(sourceFile);
43
46
  this.extractCommandMappings(sourceFile);
47
+ this.extractQueueProcessorMappings(sourceFile);
44
48
  }
45
49
 
46
50
  for (const sourceFile of sourceFiles) {
@@ -63,6 +67,16 @@ export class MethodCallGraph {
63
67
  const className = classDecl.getName();
64
68
  if (!className) continue; // Skip anonymous classes
65
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
+
66
80
  // Get all methods in this class
67
81
  const methods = classDecl.getMethods();
68
82
 
@@ -92,13 +106,32 @@ export class MethodCallGraph {
92
106
  });
93
107
  }
94
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
+
95
128
  // Find all method calls within this method
96
129
  this.analyzeMethodCalls(method, className, fullMethodName);
97
130
 
98
- // If this is an endpoint, detect command dispatches
99
- if (httpDecorator) {
100
- this.detectCommandDispatches(method, fullMethodName);
101
- }
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);
102
135
  }
103
136
  }
104
137
  }
@@ -735,6 +768,16 @@ export class MethodCallGraph {
735
768
  affectedEndpoints.push(cmdEndpoint);
736
769
  }
737
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
+
738
781
  // NEW: Check if any CALLER is in a Command handler
739
782
  // Flow: ServiceB (changed) → ServiceA → Command.run → find endpoint
740
783
  for (const caller of callers) {
@@ -749,6 +792,20 @@ export class MethodCallGraph {
749
792
 
750
793
  affectedEndpoints.push(cmdEndpoint);
751
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
+ }
752
809
  }
753
810
  }
754
811
 
@@ -804,6 +861,87 @@ export class MethodCallGraph {
804
861
  return affectedEndpoints;
805
862
  }
806
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
+
807
945
  /**
808
946
  * Get the layer/tier of a method based on file path and class name
809
947
  */
@@ -816,6 +954,7 @@ export class MethodCallGraph {
816
954
 
817
955
  // Check file path for layer indicators (order matters - most specific first)
818
956
  if (lowerPath.includes('/controllers/') || fileName.includes('.controller.')) return 'Controller';
957
+ if (lowerPath.includes('/processors/') || fileName.includes('.processor.')) return 'QueueProcessor';
819
958
  if (lowerPath.includes('/services/') || fileName.includes('.service.')) return 'Service';
820
959
  if (lowerPath.includes('/repositories/') || fileName.includes('.repository.')) return 'Repository';
821
960
  if (lowerPath.includes('/providers/') || fileName.includes('.provider.')) return 'Provider';
@@ -944,4 +1083,110 @@ export class MethodCallGraph {
944
1083
  // Longer chain
945
1084
  return 'low';
946
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
+ }
947
1192
  }
package/index.js CHANGED
@@ -17,6 +17,12 @@ import path from 'path';
17
17
  async function main() {
18
18
  const cli = new CLI(process.argv);
19
19
 
20
+ // Show help if requested
21
+ if (cli.hasArg('help') || cli.hasArg('h')) {
22
+ cli.showHelp();
23
+ process.exit(0);
24
+ }
25
+
20
26
  // Load configuration
21
27
  const config = loadConfig(cli);
22
28
  const absoluteSourceDir = path.resolve(config.sourceDir);
@@ -110,7 +116,7 @@ async function main() {
110
116
  // Exit with code based on severity
111
117
  if (impact.severity === 'critical' && ! cli.hasArg('no-fail')) {
112
118
  console.log('⚠️ Critical impact detected - exiting with error code');
113
- process.exit(1);
119
+ process.exit(0);
114
120
  }
115
121
 
116
122
  console.log('✨ Analysis completed successfully!\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/impact-analyzer",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Automated impact analysis for TypeScript/JavaScript projects",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -9,7 +9,10 @@
9
9
  },
10
10
  "scripts": {
11
11
  "analyze": "node index.js",
12
- "analyze:local": "node index.js --input=src --base=origin/main --head=HEAD",
12
+ "analyze:last": "node index.js --base=HEAD~1",
13
+ "analyze:main": "node index.js --base=origin/main",
14
+ "analyze:verbose": "node index.js --verbose",
15
+ "help": "node index.js --help",
13
16
  "test": "node --test"
14
17
  },
15
18
  "keywords": [