@sun-asterisk/impact-analyzer 1.0.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.
- package/README.md +506 -0
- package/cli.js +38 -0
- package/config/default-config.js +56 -0
- package/index.js +128 -0
- package/modules/change-detector.js +258 -0
- package/modules/detectors/database-detector.js +182 -0
- package/modules/detectors/endpoint-detector.js +52 -0
- package/modules/impact-analyzer.js +124 -0
- package/modules/report-generator.js +373 -0
- package/modules/utils/ast-parser.js +241 -0
- package/modules/utils/dependency-graph.js +159 -0
- package/modules/utils/file-utils.js +116 -0
- package/modules/utils/git-utils.js +198 -0
- package/modules/utils/method-call-graph.js +952 -0
- package/package.json +26 -0
- package/run-impact-analysis.sh +124 -0
package/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Impact Analyzer - Main Entry Point
|
|
5
|
+
* Orchestrates the entire impact analysis workflow
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CLI } from './cli.js';
|
|
9
|
+
import { loadConfig } from './config/default-config.js';
|
|
10
|
+
import { ChangeDetector } from './modules/change-detector.js';
|
|
11
|
+
import { ImpactAnalyzer } from './modules/impact-analyzer.js';
|
|
12
|
+
import { ReportGenerator } from './modules/report-generator.js';
|
|
13
|
+
import { GitUtils } from './modules/utils/git-utils.js';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const cli = new CLI(process.argv);
|
|
19
|
+
|
|
20
|
+
// Load configuration
|
|
21
|
+
const config = loadConfig(cli);
|
|
22
|
+
const absoluteSourceDir = path.resolve(config.sourceDir);
|
|
23
|
+
|
|
24
|
+
console.log('đ Starting Impact Analysis...\n');
|
|
25
|
+
console.log('Configuration:');
|
|
26
|
+
console.log(` Source Dir: ${config.sourceDir}`);
|
|
27
|
+
console.log(` Absolute Path: ${absoluteSourceDir}`);
|
|
28
|
+
console.log(` Base Ref: ${config.baseRef}`);
|
|
29
|
+
console.log(` Head Ref: ${config.headRef}`);
|
|
30
|
+
console.log(` Exclude: ${config.excludePaths.join(', ')}`);
|
|
31
|
+
console.log('');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// ============================================
|
|
35
|
+
// Validation
|
|
36
|
+
// ============================================
|
|
37
|
+
|
|
38
|
+
// Check if source directory exists
|
|
39
|
+
if (! fs.existsSync(absoluteSourceDir)) {
|
|
40
|
+
throw new Error(`Source directory does not exist: ${absoluteSourceDir}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if it's a git repository
|
|
44
|
+
if (!GitUtils.isGitRepo(absoluteSourceDir)) {
|
|
45
|
+
throw new Error(`Source directory is not a git repository: ${absoluteSourceDir}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if refs exist
|
|
49
|
+
if (!GitUtils.refExists(config.baseRef, absoluteSourceDir)) {
|
|
50
|
+
throw new Error(`Base ref does not exist: ${config.baseRef}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('â Validation passed\n');
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// Step 1: Detect Changes
|
|
57
|
+
// ============================================
|
|
58
|
+
console.log('đ Step 1: Detecting changes...');
|
|
59
|
+
const detector = new ChangeDetector(config);
|
|
60
|
+
|
|
61
|
+
const changedFiles = detector.detectChangedFiles();
|
|
62
|
+
console.log(` â Found ${changedFiles.length} changed files`);
|
|
63
|
+
|
|
64
|
+
const changedSymbols = detector.detectChangedSymbols(changedFiles);
|
|
65
|
+
console.log(` â Found ${changedSymbols.length} changed symbols\n`);
|
|
66
|
+
|
|
67
|
+
const changes = {
|
|
68
|
+
changedFiles,
|
|
69
|
+
changedSymbols,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// Step 2: Analyze Impact
|
|
74
|
+
// ============================================
|
|
75
|
+
console.log('đ Step 2: Analyzing impact...');
|
|
76
|
+
const analyzer = new ImpactAnalyzer(config);
|
|
77
|
+
|
|
78
|
+
// Initialize method-level call graph for precise tracking
|
|
79
|
+
await analyzer.initializeMethodCallGraph();
|
|
80
|
+
|
|
81
|
+
const impact = await analyzer.analyzeImpact(changes);
|
|
82
|
+
console.log(` â Impact score: ${impact.impactScore}`);
|
|
83
|
+
console.log(` â Severity: ${impact.severity.toUpperCase()}\n`);
|
|
84
|
+
|
|
85
|
+
// ============================================
|
|
86
|
+
// Step 3: Generate Reports
|
|
87
|
+
// ============================================
|
|
88
|
+
console.log('đ Step 3: Generating reports...\n');
|
|
89
|
+
const reporter = new ReportGenerator();
|
|
90
|
+
|
|
91
|
+
// Console report
|
|
92
|
+
reporter.generateConsoleReport(changes, impact);
|
|
93
|
+
|
|
94
|
+
// Markdown report
|
|
95
|
+
const markdownReport = reporter.generateMarkdownReport(changes, impact);
|
|
96
|
+
|
|
97
|
+
// Save to file
|
|
98
|
+
const outputFile = cli.getArg('output', 'impact-report.md');
|
|
99
|
+
fs.writeFileSync(outputFile, markdownReport);
|
|
100
|
+
console.log(`â
Report saved to: ${outputFile}\n`);
|
|
101
|
+
|
|
102
|
+
// JSON output (optional)
|
|
103
|
+
if (cli.hasArg('json')) {
|
|
104
|
+
const jsonOutput = cli.getArg('json');
|
|
105
|
+
fs.writeFileSync(jsonOutput, JSON.stringify({ changes, impact }, null, 2));
|
|
106
|
+
console.log(`â
JSON report saved to: ${jsonOutput}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Exit with code based on severity
|
|
110
|
+
if (impact.severity === 'critical' && ! cli.hasArg('no-fail')) {
|
|
111
|
+
console.log('â ī¸ Critical impact detected - exiting with error code');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('⨠Analysis completed successfully!\n');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('â Error during analysis:', error.message);
|
|
120
|
+
if (cli.hasArg('verbose')) {
|
|
121
|
+
console.error(error.stack);
|
|
122
|
+
}
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Run if executed directly
|
|
128
|
+
main();
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change Detector Module
|
|
3
|
+
* Detects file and symbol changes between git refs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GitUtils } from './utils/git-utils.js';
|
|
7
|
+
import { FileUtils } from './utils/file-utils.js';
|
|
8
|
+
import { ASTParser } from './utils/ast-parser.js';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
|
|
12
|
+
export class ChangeDetector {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
// Resolve absolute path for source directory
|
|
16
|
+
this.absoluteSourceDir = path.resolve(config.sourceDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect all changed files in PR/commit range
|
|
21
|
+
*/
|
|
22
|
+
detectChangedFiles() {
|
|
23
|
+
// Get git changes from the source directory
|
|
24
|
+
const gitChanges = GitUtils.getChangedFiles(
|
|
25
|
+
this.config.baseRef,
|
|
26
|
+
this.config.headRef,
|
|
27
|
+
this.absoluteSourceDir
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (this.config.verbose) {
|
|
31
|
+
console.log(`\nđ Detecting changed files...`);
|
|
32
|
+
console.log(` Base: ${this.config.baseRef}`);
|
|
33
|
+
console.log(` Head: ${this.config.headRef}`);
|
|
34
|
+
console.log(` Git detected ${gitChanges.length} changed files`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const changedFiles = [];
|
|
38
|
+
|
|
39
|
+
for (const change of gitChanges) {
|
|
40
|
+
const filePath = change.path;
|
|
41
|
+
|
|
42
|
+
// Filter based on configuration
|
|
43
|
+
if (!this.shouldAnalyzeFile(filePath)) {
|
|
44
|
+
if (this.config.verbose) {
|
|
45
|
+
console.log(` â Skipped: ${filePath} (excluded or not source file)`);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get file diff to analyze actual changes
|
|
51
|
+
const diff = GitUtils.getFileDiff(
|
|
52
|
+
filePath,
|
|
53
|
+
this.config.baseRef,
|
|
54
|
+
this.config.headRef,
|
|
55
|
+
this.absoluteSourceDir
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Skip files with only whitespace/comment changes
|
|
59
|
+
if (!this.hasSignificantChanges(diff, filePath)) {
|
|
60
|
+
if (this.config.verbose) {
|
|
61
|
+
console.log(` â Skipped: ${filePath} (only whitespace/comment changes)`);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get file contents from the source directory
|
|
67
|
+
const content = GitUtils.getFileContent(
|
|
68
|
+
filePath,
|
|
69
|
+
this.config.headRef,
|
|
70
|
+
this.absoluteSourceDir
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const oldContent = change.status !== 'added'
|
|
74
|
+
? GitUtils.getFileContent(
|
|
75
|
+
filePath,
|
|
76
|
+
this.config.baseRef,
|
|
77
|
+
this.absoluteSourceDir
|
|
78
|
+
)
|
|
79
|
+
: '';
|
|
80
|
+
|
|
81
|
+
// Calculate line changes
|
|
82
|
+
const changes = FileUtils.calculateLineChanges(oldContent, content);
|
|
83
|
+
|
|
84
|
+
const fileInfo = {
|
|
85
|
+
path: filePath,
|
|
86
|
+
absolutePath: path.join(this.absoluteSourceDir, filePath),
|
|
87
|
+
status: change.status,
|
|
88
|
+
type: FileUtils.categorizeFile(filePath),
|
|
89
|
+
changes,
|
|
90
|
+
content,
|
|
91
|
+
oldContent,
|
|
92
|
+
diff, // Store the diff for further analysis
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (this.config.verbose) {
|
|
96
|
+
console.log(` â ${change.status.toUpperCase()}: ${filePath} (+${changes.added} -${changes.deleted})`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
changedFiles.push(fileInfo);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.config.verbose) {
|
|
103
|
+
console.log(` Total files to analyze: ${changedFiles.length}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return changedFiles;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if diff contains significant code changes
|
|
111
|
+
* Ignore whitespace-only, comment-only, or formatting changes
|
|
112
|
+
*/
|
|
113
|
+
hasSignificantChanges(diff, filePath) {
|
|
114
|
+
if (!diff || diff.trim().length === 0) {
|
|
115
|
+
// For new/deleted files, consider them significant
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract added/removed lines from diff
|
|
120
|
+
const lines = diff.split('\n');
|
|
121
|
+
const codeChanges = lines.filter(line => {
|
|
122
|
+
// Skip diff headers
|
|
123
|
+
if (line.startsWith('diff') || line.startsWith('index') ||
|
|
124
|
+
line.startsWith('+++') || line.startsWith('---') ||
|
|
125
|
+
line.startsWith('@@')) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for actual code changes (lines starting with + or -)
|
|
130
|
+
if (line.startsWith('+') || line.startsWith('-')) {
|
|
131
|
+
const content = line.substring(1).trim();
|
|
132
|
+
|
|
133
|
+
// Ignore empty lines
|
|
134
|
+
if (content.length === 0) return false;
|
|
135
|
+
|
|
136
|
+
// Ignore comment-only lines (be conservative - keep if unsure)
|
|
137
|
+
const isCommentOnly = content.startsWith('//') ||
|
|
138
|
+
content.startsWith('/*') ||
|
|
139
|
+
content.startsWith('*') ||
|
|
140
|
+
content === '*/';
|
|
141
|
+
|
|
142
|
+
if (isCommentOnly) return false;
|
|
143
|
+
|
|
144
|
+
return true; // This is a significant change
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// If we found any code changes, or if the diff is non-empty, consider it significant
|
|
151
|
+
// Be conservative - if in doubt, include it
|
|
152
|
+
return codeChanges.length > 0 || lines.length > 10;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect changed symbols (functions, classes, methods)
|
|
157
|
+
*/
|
|
158
|
+
detectChangedSymbols(changedFiles) {
|
|
159
|
+
const allChangedSymbols = [];
|
|
160
|
+
|
|
161
|
+
for (const file of changedFiles) {
|
|
162
|
+
// Only analyze source files
|
|
163
|
+
if (file.type !== 'source') continue;
|
|
164
|
+
|
|
165
|
+
// Extract symbols from current and old versions
|
|
166
|
+
const currentSymbols = ASTParser.extractSymbols(file.content, file.path);
|
|
167
|
+
const oldSymbols = file.oldContent
|
|
168
|
+
? ASTParser.extractSymbols(file.oldContent, file.path)
|
|
169
|
+
: [];
|
|
170
|
+
|
|
171
|
+
// Compare and categorize changes
|
|
172
|
+
const changedSymbols = this.compareSymbols(currentSymbols, oldSymbols, file.path);
|
|
173
|
+
allChangedSymbols.push(...changedSymbols);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return allChangedSymbols;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Compare symbols to find added, modified, and deleted
|
|
181
|
+
*/
|
|
182
|
+
compareSymbols(currentSymbols, oldSymbols, filePath) {
|
|
183
|
+
const changes = [];
|
|
184
|
+
|
|
185
|
+
// Create maps for easy lookup
|
|
186
|
+
const oldMap = new Map(oldSymbols.map(s => [s.name, s]));
|
|
187
|
+
const currentMap = new Map(currentSymbols.map(s => [s.name, s]));
|
|
188
|
+
|
|
189
|
+
// Find added and modified symbols
|
|
190
|
+
for (const symbol of currentSymbols) {
|
|
191
|
+
const oldSymbol = oldMap.get(symbol.name);
|
|
192
|
+
|
|
193
|
+
if (! oldSymbol) {
|
|
194
|
+
// New symbol
|
|
195
|
+
changes.push({
|
|
196
|
+
...symbol,
|
|
197
|
+
changeType: 'added',
|
|
198
|
+
file: filePath,
|
|
199
|
+
});
|
|
200
|
+
} else if (this.hasSymbolChanged(symbol, oldSymbol)) {
|
|
201
|
+
// Modified symbol
|
|
202
|
+
changes.push({
|
|
203
|
+
...symbol,
|
|
204
|
+
changeType: 'modified',
|
|
205
|
+
file: filePath,
|
|
206
|
+
oldSignature: oldSymbol.signature,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Find deleted symbols
|
|
212
|
+
for (const symbol of oldSymbols) {
|
|
213
|
+
if (!currentMap.has(symbol.name)) {
|
|
214
|
+
changes.push({
|
|
215
|
+
...symbol,
|
|
216
|
+
changeType: 'deleted',
|
|
217
|
+
file: filePath,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return changes;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if symbol has changed
|
|
227
|
+
*/
|
|
228
|
+
hasSymbolChanged(newSymbol, oldSymbol) {
|
|
229
|
+
// Check signature change
|
|
230
|
+
if (newSymbol.signature !== oldSymbol.signature) return true;
|
|
231
|
+
|
|
232
|
+
// Check line number change (indicates body change)
|
|
233
|
+
if (newSymbol.startLine !== oldSymbol.startLine) return true;
|
|
234
|
+
if (newSymbol.endLine !== oldSymbol.endLine) return true;
|
|
235
|
+
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Determine if file should be analyzed
|
|
241
|
+
*/
|
|
242
|
+
shouldAnalyzeFile(filePath) {
|
|
243
|
+
// Must be a source file
|
|
244
|
+
if (!FileUtils.isSourceFile(filePath)) return false;
|
|
245
|
+
|
|
246
|
+
// Check exclude paths
|
|
247
|
+
if (this.config.excludePaths.some(exclude => filePath.includes(exclude))) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Optionally exclude test files
|
|
252
|
+
if (! this.config.includeTests && FileUtils.isTestFile(filePath)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Impact Detector
|
|
3
|
+
* Detects database changes using layer-aware method tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
export class DatabaseDetector {
|
|
9
|
+
constructor(methodCallGraph, config) {
|
|
10
|
+
this.methodCallGraph = methodCallGraph;
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect database impact from changed files using layer-aware tracking
|
|
16
|
+
*/
|
|
17
|
+
async detect(changedFiles) {
|
|
18
|
+
const databaseChanges = {
|
|
19
|
+
tables: new Set(),
|
|
20
|
+
fields: new Map(), // table -> Set of fields
|
|
21
|
+
operations: new Map(), // table -> Set of operations
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Extract changed methods
|
|
25
|
+
const allChangedMethods = [];
|
|
26
|
+
for (const changedFile of changedFiles) {
|
|
27
|
+
const changedMethods = this.methodCallGraph.getChangedMethods(
|
|
28
|
+
changedFile.diff || '',
|
|
29
|
+
changedFile.absolutePath
|
|
30
|
+
);
|
|
31
|
+
allChangedMethods.push(...changedMethods);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.config.verbose) {
|
|
35
|
+
console.log(`\n đ Analyzing database impact for ${allChangedMethods.length} changed methods`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Find affected repository methods through call graph
|
|
39
|
+
const affectedRepoMethods = this.findAffectedRepositoryMethods(allChangedMethods);
|
|
40
|
+
|
|
41
|
+
if (this.config.verbose) {
|
|
42
|
+
console.log(`\n đ Found ${affectedRepoMethods.length} affected repository methods`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Analyze repository methods for database operations
|
|
46
|
+
for (const repoMethod of affectedRepoMethods) {
|
|
47
|
+
const dbOps = this.extractDatabaseOperationsFromCallGraph(repoMethod);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
for (const op of dbOps) {
|
|
52
|
+
databaseChanges.tables.add(op.table);
|
|
53
|
+
|
|
54
|
+
if (!databaseChanges.operations.has(op.table)) {
|
|
55
|
+
databaseChanges.operations.set(op.table, new Set());
|
|
56
|
+
}
|
|
57
|
+
databaseChanges.operations.get(op.table).add(op.operation);
|
|
58
|
+
|
|
59
|
+
if (op.fields && op.fields.length > 0) {
|
|
60
|
+
if (!databaseChanges.fields.has(op.table)) {
|
|
61
|
+
databaseChanges.fields.set(op.table, new Set());
|
|
62
|
+
}
|
|
63
|
+
op.fields.forEach(field => databaseChanges.fields.get(op.table).add(field));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return this.formatDatabaseImpact(databaseChanges);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
findAffectedRepositoryMethods(changedMethods) {
|
|
72
|
+
const visited = new Set();
|
|
73
|
+
const repoMethods = [];
|
|
74
|
+
const queue = [...changedMethods];
|
|
75
|
+
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const method = queue.shift();
|
|
78
|
+
|
|
79
|
+
if (!method || !method.file) continue;
|
|
80
|
+
|
|
81
|
+
const key = `${method.file}:${method.className}.${method.methodName}`;
|
|
82
|
+
if (visited.has(key)) continue;
|
|
83
|
+
visited.add(key);
|
|
84
|
+
|
|
85
|
+
const isRepository = method.file.includes('repository') ||
|
|
86
|
+
(method.className && method.className.toLowerCase().includes('repository'));
|
|
87
|
+
|
|
88
|
+
if (isRepository) {
|
|
89
|
+
repoMethods.push(method);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const callers = this.methodCallGraph.getCallers(method);
|
|
93
|
+
queue.push(...callers);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return repoMethods;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
extractDatabaseOperationsFromCallGraph(repoMethod) {
|
|
100
|
+
const operations = [];
|
|
101
|
+
|
|
102
|
+
const methodKey = `${repoMethod.className}.${repoMethod.methodName}`;
|
|
103
|
+
const methodCalls = this.methodCallGraph.methodCallsMap?.get(methodKey) || [];
|
|
104
|
+
|
|
105
|
+
if (this.config.verbose) {
|
|
106
|
+
console.log(` đ Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const call of methodCalls) {
|
|
110
|
+
const dbOp = this.detectDatabaseOperationFromCall(call);
|
|
111
|
+
|
|
112
|
+
if (dbOp) {
|
|
113
|
+
operations.push({
|
|
114
|
+
...dbOp,
|
|
115
|
+
method: `${repoMethod.className}.${repoMethod.methodName}`,
|
|
116
|
+
file: repoMethod.file,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return operations;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
detectDatabaseOperationFromCall(call) {
|
|
125
|
+
const typeOrmOps = {
|
|
126
|
+
'insert': 'INSERT',
|
|
127
|
+
'save': 'INSERT/UPDATE',
|
|
128
|
+
'create': 'INSERT',
|
|
129
|
+
'update': 'UPDATE',
|
|
130
|
+
'merge': 'UPDATE',
|
|
131
|
+
'delete': 'DELETE',
|
|
132
|
+
'remove': 'DELETE',
|
|
133
|
+
'softDelete': 'SOFT_DELETE',
|
|
134
|
+
'find': 'SELECT',
|
|
135
|
+
'findOne': 'SELECT',
|
|
136
|
+
'findAndCount': 'SELECT',
|
|
137
|
+
'findBy': 'SELECT',
|
|
138
|
+
'findOneBy': 'SELECT',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const methodName = call.method;
|
|
142
|
+
if (!typeOrmOps[methodName]) return null;
|
|
143
|
+
|
|
144
|
+
const target = call.target;
|
|
145
|
+
const entityMatch = target?.match(/(\w+Repository)/);
|
|
146
|
+
|
|
147
|
+
if (!entityMatch) return null;
|
|
148
|
+
|
|
149
|
+
const entityName = entityMatch[1].replace(/Repository$/, '') + 'Entity';
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
table: this.entityToTableName(entityName),
|
|
153
|
+
operation: typeOrmOps[methodName],
|
|
154
|
+
fields: [],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
entityToTableName(entityName) {
|
|
159
|
+
return entityName
|
|
160
|
+
.replace(/Entity$/, '')
|
|
161
|
+
.replace(/([A-Z])/g, '_$1')
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.replace(/^_/, '');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
formatDatabaseImpact(databaseChanges) {
|
|
167
|
+
const tables = [];
|
|
168
|
+
|
|
169
|
+
for (const tableName of databaseChanges.tables) {
|
|
170
|
+
const operations = Array.from(databaseChanges.operations.get(tableName) || []);
|
|
171
|
+
const fields = Array.from(databaseChanges.fields.get(tableName) || []);
|
|
172
|
+
|
|
173
|
+
tables.push({
|
|
174
|
+
name: tableName,
|
|
175
|
+
operations,
|
|
176
|
+
fields: fields.length > 0 ? fields : undefined,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return tables;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint Impact Detector
|
|
3
|
+
* Detects affected API endpoints using method-level call graph
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class EndpointDetector {
|
|
7
|
+
constructor(methodCallGraph, config) {
|
|
8
|
+
this.methodCallGraph = methodCallGraph;
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect affected endpoints from changed files
|
|
14
|
+
*/
|
|
15
|
+
async detect(changedFiles) {
|
|
16
|
+
const allChangedMethods = [];
|
|
17
|
+
|
|
18
|
+
for (const changedFile of changedFiles) {
|
|
19
|
+
const changedMethods = this.methodCallGraph.getChangedMethods(
|
|
20
|
+
changedFile.diff || '',
|
|
21
|
+
changedFile.absolutePath
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
allChangedMethods.push(...changedMethods);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (allChangedMethods.length === 0) {
|
|
28
|
+
if (this.config.verbose) {
|
|
29
|
+
console.log(' âšī¸ No method-level changes detected');
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.config.verbose) {
|
|
35
|
+
console.log(`\n đ Finding affected endpoints for ${allChangedMethods.length} changed methods...`);
|
|
36
|
+
console.log(` Call graph size: ${this.methodCallGraph.methodCallMap.size} entries`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const affectedEndpoints = this.methodCallGraph.findAffectedEndpoints(allChangedMethods);
|
|
40
|
+
|
|
41
|
+
if (this.config.verbose) {
|
|
42
|
+
for (const endpoint of affectedEndpoints) {
|
|
43
|
+
console.log(`\n đ Analyzing: ${endpoint.affectedBy}`);
|
|
44
|
+
console.log(` â Endpoint: ${endpoint.method} ${endpoint.path}`);
|
|
45
|
+
console.log(` Chain: ${endpoint.callChain.join(' â ')}`);
|
|
46
|
+
console.log(` Layers: ${endpoint.layers.join(' â ')}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return affectedEndpoints;
|
|
51
|
+
}
|
|
52
|
+
}
|