@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.
- package/.specify/bugs/bug-004-fix-summary.md +59 -0
- package/.specify/bugs/bug-004-raw-sql-detection.md +158 -0
- package/.specify/bugs/bug-005-queue-processor-detection.md +197 -0
- package/.specify/tasks/task-005-cli-optimization.md +284 -0
- package/.specify/tasks/task-005-completion.md +99 -0
- package/README.md +150 -36
- package/cli.js +68 -0
- package/config/default-config.js +5 -9
- package/core/detectors/database-detector.js +408 -3
- package/core/utils/logger.js +2 -1
- package/core/utils/method-call-graph.js +249 -4
- package/index.js +7 -1
- package/package.json +5 -2
|
@@ -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
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
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": [
|