@sun-asterisk/sunlint 1.3.34 → 1.3.36
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/core/architecture-integration.js +16 -7
- package/core/auto-performance-manager.js +1 -1
- package/core/cli-action-handler.js +102 -2
- package/core/cli-program.js +102 -138
- package/core/file-targeting-service.js +62 -4
- package/core/git-utils.js +19 -12
- package/core/github-annotate-service.js +326 -11
- package/core/html-report-generator.js +326 -731
- package/core/impact-integration.js +551 -0
- package/core/output-service.js +293 -21
- package/core/scoring-service.js +3 -2
- package/engines/arch-detect/core/analyzer.js +413 -0
- package/engines/arch-detect/core/index.js +22 -0
- package/engines/arch-detect/engine/hybrid-detector.js +176 -0
- package/engines/arch-detect/engine/index.js +24 -0
- package/engines/arch-detect/engine/rule-executor.js +228 -0
- package/engines/arch-detect/engine/score-calculator.js +214 -0
- package/engines/arch-detect/engine/violation-detector.js +616 -0
- package/engines/arch-detect/index.js +50 -0
- package/engines/arch-detect/rules/base-rule.js +187 -0
- package/engines/arch-detect/rules/index.js +35 -0
- package/engines/arch-detect/rules/layered/index.js +28 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
- package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
- package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
- package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
- package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
- package/engines/arch-detect/rules/modular/index.js +27 -0
- package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
- package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
- package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
- package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
- package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
- package/engines/arch-detect/rules/presentation/index.js +27 -0
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
- package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
- package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
- package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
- package/engines/arch-detect/rules/project-scanner/index.js +31 -0
- package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
- package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
- package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
- package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
- package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
- package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
- package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
- package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
- package/engines/arch-detect/rules/rule-registry.js +111 -0
- package/engines/arch-detect/types/context.types.js +60 -0
- package/engines/arch-detect/types/enums.js +161 -0
- package/engines/arch-detect/types/index.js +25 -0
- package/engines/arch-detect/types/result.types.js +7 -0
- package/engines/arch-detect/types/rule.types.js +7 -0
- package/engines/arch-detect/utils/file-scanner.js +411 -0
- package/engines/arch-detect/utils/index.js +23 -0
- package/engines/arch-detect/utils/pattern-matcher.js +328 -0
- package/engines/impact/cli.js +106 -0
- package/engines/impact/config/default-config.js +54 -0
- package/engines/impact/core/change-detector.js +258 -0
- package/engines/impact/core/detectors/database-detector.js +1317 -0
- package/engines/impact/core/detectors/endpoint-detector.js +55 -0
- package/engines/impact/core/impact-analyzer.js +124 -0
- package/engines/impact/core/report-generator.js +462 -0
- package/engines/impact/core/utils/ast-parser.js +241 -0
- package/engines/impact/core/utils/dependency-graph.js +159 -0
- package/engines/impact/core/utils/file-utils.js +116 -0
- package/engines/impact/core/utils/git-utils.js +203 -0
- package/engines/impact/core/utils/logger.js +13 -0
- package/engines/impact/core/utils/method-call-graph.js +1192 -0
- package/engines/impact/index.js +135 -0
- package/engines/impact/package.json +29 -0
- package/package.json +18 -43
- package/scripts/build-release.sh +0 -0
- package/scripts/copy-impact-analyzer.js +135 -0
- package/scripts/install.sh +0 -0
- package/scripts/manual-release.sh +0 -0
- package/scripts/pre-release-test.sh +0 -0
- package/scripts/prepare-release.sh +0 -0
- package/scripts/quick-performance-test.js +0 -0
- package/scripts/setup-github-registry.sh +0 -0
- package/scripts/trigger-release.sh +0 -0
- package/scripts/verify-install.sh +0 -0
- package/templates/combined-report.html +1418 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Integration for SunLint
|
|
3
|
+
* Wraps impact-analyzer module for seamless integration
|
|
4
|
+
* Following Rule C005: Single responsibility - handle impact analysis integration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
class ImpactIntegration {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.impactModulePath = null;
|
|
16
|
+
this.ChangeDetector = null;
|
|
17
|
+
this.ImpactAnalyzer = null;
|
|
18
|
+
this.ReportGenerator = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load impact analyzer module
|
|
23
|
+
* Note: impact-analyzer is ESM, so we use dynamic import
|
|
24
|
+
*/
|
|
25
|
+
async loadImpactModule() {
|
|
26
|
+
if (this.ChangeDetector && this.ImpactAnalyzer) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Priority 1: Try bundled version (engines/impact)
|
|
31
|
+
const bundledPath = path.join(__dirname, '..', 'engines', 'impact');
|
|
32
|
+
if (fs.existsSync(path.join(bundledPath, 'index.js'))) {
|
|
33
|
+
try {
|
|
34
|
+
this.impactModulePath = bundledPath;
|
|
35
|
+
await this.loadModules(bundledPath);
|
|
36
|
+
if (this.options.verbose) {
|
|
37
|
+
console.log(chalk.gray('📦 Loaded bundled impact-analyzer'));
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (this.options.verbose) {
|
|
42
|
+
console.log(chalk.yellow(`⚠️ Failed to load bundled: ${error.message}`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Priority 2: Try local development path
|
|
48
|
+
const devPaths = [
|
|
49
|
+
path.join(__dirname, '..', '..', '..', '..', 'impact-analyzer'),
|
|
50
|
+
path.join(__dirname, '..', '..', '..', 'impact-analyzer'),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const devPath of devPaths) {
|
|
54
|
+
if (fs.existsSync(path.join(devPath, 'index.js'))) {
|
|
55
|
+
try {
|
|
56
|
+
this.impactModulePath = devPath;
|
|
57
|
+
await this.loadModules(devPath);
|
|
58
|
+
if (this.options.verbose) {
|
|
59
|
+
console.log(chalk.gray(`📦 Loaded impact-analyzer from: ${devPath}`));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (this.options.verbose) {
|
|
64
|
+
console.log(chalk.yellow(`⚠️ Failed to load from ${devPath}: ${error.message}`));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(
|
|
71
|
+
'Impact analyzer module not found. Ensure impact-analyzer exists in the monorepo.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load ESM modules using dynamic import
|
|
77
|
+
*/
|
|
78
|
+
async loadModules(basePath) {
|
|
79
|
+
const changeDetectorPath = path.join(basePath, 'core', 'change-detector.js');
|
|
80
|
+
const impactAnalyzerPath = path.join(basePath, 'core', 'impact-analyzer.js');
|
|
81
|
+
const reportGeneratorPath = path.join(basePath, 'core', 'report-generator.js');
|
|
82
|
+
|
|
83
|
+
// Use dynamic import for ESM modules
|
|
84
|
+
const changeDetectorModule = await import(`file://${changeDetectorPath}`);
|
|
85
|
+
const impactAnalyzerModule = await import(`file://${impactAnalyzerPath}`);
|
|
86
|
+
const reportGeneratorModule = await import(`file://${reportGeneratorPath}`);
|
|
87
|
+
|
|
88
|
+
this.ChangeDetector = changeDetectorModule.ChangeDetector;
|
|
89
|
+
this.ImpactAnalyzer = impactAnalyzerModule.ImpactAnalyzer;
|
|
90
|
+
this.ReportGenerator = reportGeneratorModule.ReportGenerator;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if git repository
|
|
95
|
+
*/
|
|
96
|
+
isGitRepo(projectPath) {
|
|
97
|
+
try {
|
|
98
|
+
execSync('git rev-parse --git-dir', {
|
|
99
|
+
cwd: projectPath,
|
|
100
|
+
stdio: 'pipe'
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if git ref exists
|
|
110
|
+
*/
|
|
111
|
+
refExists(ref, projectPath) {
|
|
112
|
+
try {
|
|
113
|
+
execSync(`git rev-parse ${ref}`, {
|
|
114
|
+
cwd: projectPath,
|
|
115
|
+
stdio: 'pipe'
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build config object for impact analyzer
|
|
125
|
+
*/
|
|
126
|
+
buildConfig(projectPath) {
|
|
127
|
+
return {
|
|
128
|
+
sourceDir: projectPath,
|
|
129
|
+
baseRef: this.options.impactBase || 'HEAD~1',
|
|
130
|
+
headRef: this.options.impactHead || '', // Empty means current working directory
|
|
131
|
+
excludePaths: this.options.exclude
|
|
132
|
+
? this.options.exclude.split(',').map(p => p.trim())
|
|
133
|
+
: ['node_modules', 'dist', 'build', 'specs', 'coverage'],
|
|
134
|
+
maxDepth: this.options.impactMaxDepth || 3,
|
|
135
|
+
includeTests: this.options.impactIncludeTests || false,
|
|
136
|
+
verbose: this.options.verbose || false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run impact analysis on project
|
|
142
|
+
* @param {string} projectPath - Path to analyze
|
|
143
|
+
* @returns {Object} Impact analysis results
|
|
144
|
+
*/
|
|
145
|
+
async analyze(projectPath) {
|
|
146
|
+
await this.loadImpactModule();
|
|
147
|
+
|
|
148
|
+
const absolutePath = path.resolve(projectPath);
|
|
149
|
+
const config = this.buildConfig(absolutePath);
|
|
150
|
+
|
|
151
|
+
// Validation
|
|
152
|
+
if (!fs.existsSync(absolutePath)) {
|
|
153
|
+
throw new Error(`Directory does not exist: ${absolutePath}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!this.isGitRepo(absolutePath)) {
|
|
157
|
+
throw new Error(`Not a git repository: ${absolutePath}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!this.refExists(config.baseRef, absolutePath)) {
|
|
161
|
+
throw new Error(`Git ref does not exist: ${config.baseRef}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (config.headRef && !this.refExists(config.headRef, absolutePath)) {
|
|
165
|
+
throw new Error(`Head git ref does not exist: ${config.headRef}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Ensure tsconfig.json exists (required by ts-morph)
|
|
169
|
+
await this.ensureTsConfig(absolutePath);
|
|
170
|
+
|
|
171
|
+
if (this.options.verbose) {
|
|
172
|
+
console.log(chalk.blue(`\n🔍 Impact Analysis Configuration`));
|
|
173
|
+
console.log(chalk.gray(` Path: ${absolutePath}`));
|
|
174
|
+
console.log(chalk.gray(` Base: ${config.baseRef}`));
|
|
175
|
+
console.log(chalk.gray(` Head: ${config.headRef || 'working directory'}`));
|
|
176
|
+
console.log(chalk.gray(` Max Depth: ${config.maxDepth}`));
|
|
177
|
+
console.log(chalk.gray(` Include Tests: ${config.includeTests}`));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// ============================================
|
|
182
|
+
// Step 1: Detect Changes
|
|
183
|
+
// ============================================
|
|
184
|
+
if (this.options.verbose) {
|
|
185
|
+
console.log(chalk.blue('\n📝 Step 1: Detecting changes...'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const detector = new this.ChangeDetector(config);
|
|
189
|
+
const changedFiles = detector.detectChangedFiles();
|
|
190
|
+
|
|
191
|
+
if (this.options.verbose) {
|
|
192
|
+
console.log(chalk.gray(` ✓ Found ${changedFiles.length} changed files`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const changedSymbols = detector.detectChangedSymbols(changedFiles);
|
|
196
|
+
|
|
197
|
+
if (this.options.verbose) {
|
|
198
|
+
console.log(chalk.gray(` ✓ Found ${changedSymbols.length} changed symbols`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const changes = {
|
|
202
|
+
changedFiles,
|
|
203
|
+
changedSymbols,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ============================================
|
|
207
|
+
// Step 2: Analyze Impact
|
|
208
|
+
// ============================================
|
|
209
|
+
if (this.options.verbose) {
|
|
210
|
+
console.log(chalk.blue('\n🔍 Step 2: Analyzing impact...'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const analyzer = new this.ImpactAnalyzer(config);
|
|
214
|
+
|
|
215
|
+
// Initialize method-level call graph for precise tracking
|
|
216
|
+
await analyzer.initializeMethodCallGraph();
|
|
217
|
+
|
|
218
|
+
const impact = await analyzer.analyzeImpact(changes);
|
|
219
|
+
|
|
220
|
+
if (this.options.verbose) {
|
|
221
|
+
console.log(chalk.gray(` ✓ Impact score: ${impact.impactScore}`));
|
|
222
|
+
console.log(chalk.gray(` ✓ Severity: ${impact.severity.toUpperCase()}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// Step 3: Build Result
|
|
227
|
+
// ============================================
|
|
228
|
+
const result = this.buildResult(changes, impact, config);
|
|
229
|
+
|
|
230
|
+
// Generate markdown report if requested
|
|
231
|
+
if (this.options.impactReport) {
|
|
232
|
+
const reporter = new this.ReportGenerator();
|
|
233
|
+
result.markdownReport = reporter.generateMarkdownReport(changes, impact);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Re-throw with more context
|
|
240
|
+
throw new Error(`Impact analysis failed: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Ensure tsconfig.json exists (required by ts-morph)
|
|
246
|
+
*/
|
|
247
|
+
async ensureTsConfig(projectPath) {
|
|
248
|
+
const tsconfigPath = path.join(projectPath, '..', 'tsconfig.json');
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
251
|
+
return; // Already exists
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Create minimal tsconfig.json for analysis
|
|
255
|
+
const minimalTsConfig = {
|
|
256
|
+
compilerOptions: {
|
|
257
|
+
target: "ES2020",
|
|
258
|
+
module: "commonjs",
|
|
259
|
+
lib: ["ES2020"],
|
|
260
|
+
moduleResolution: "node",
|
|
261
|
+
esModuleInterop: true,
|
|
262
|
+
skipLibCheck: true,
|
|
263
|
+
strict: false,
|
|
264
|
+
resolveJsonModule: true,
|
|
265
|
+
allowSyntheticDefaultImports: true,
|
|
266
|
+
},
|
|
267
|
+
include: ["**/*"],
|
|
268
|
+
exclude: ["node_modules", "dist", "build"]
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (this.options.verbose) {
|
|
272
|
+
console.log(chalk.gray(` Creating minimal tsconfig.json at: ${tsconfigPath}`));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(minimalTsConfig, null, 2));
|
|
276
|
+
|
|
277
|
+
// Mark for cleanup
|
|
278
|
+
this.tempTsConfig = tsconfigPath;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Clean up temporary files
|
|
283
|
+
*/
|
|
284
|
+
cleanup() {
|
|
285
|
+
if (this.tempTsConfig && fs.existsSync(this.tempTsConfig)) {
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync(this.tempTsConfig);
|
|
288
|
+
if (this.options.verbose) {
|
|
289
|
+
console.log(chalk.gray(` Cleaned up temporary tsconfig.json`));
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// Ignore cleanup errors
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Build comprehensive result from impact analysis
|
|
299
|
+
*/
|
|
300
|
+
buildResult(changes, impact, config) {
|
|
301
|
+
// Extract data from impact analysis
|
|
302
|
+
const { affectedEndpoints = [], databaseImpact = {}, logicImpact = {} } = impact;
|
|
303
|
+
|
|
304
|
+
// Calculate category counts from file categorization
|
|
305
|
+
const fileCategories = this.categorizeFiles(changes.changedFiles);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
summary: {
|
|
309
|
+
baseRef: config.baseRef,
|
|
310
|
+
headRef: config.headRef || 'working directory',
|
|
311
|
+
totalChanges: changes.changedFiles.length,
|
|
312
|
+
changedSymbols: changes.changedSymbols.length,
|
|
313
|
+
impactScore: impact.impactScore || 0,
|
|
314
|
+
severity: impact.severity || 'none',
|
|
315
|
+
categories: {
|
|
316
|
+
endpoints: affectedEndpoints.length,
|
|
317
|
+
database: databaseImpact.migrations?.length || 0,
|
|
318
|
+
logic: logicImpact.directCallers?.length || 0,
|
|
319
|
+
files: changes.changedFiles.length,
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
changes: {
|
|
323
|
+
changedFiles: changes.changedFiles,
|
|
324
|
+
changedSymbols: changes.changedSymbols,
|
|
325
|
+
},
|
|
326
|
+
impact: {
|
|
327
|
+
affectedEndpoints: affectedEndpoints.map(ep => ({
|
|
328
|
+
path: ep.path,
|
|
329
|
+
method: ep.method,
|
|
330
|
+
handler: ep.handler,
|
|
331
|
+
impactReason: ep.impactReason || 'Direct or indirect change',
|
|
332
|
+
})),
|
|
333
|
+
databaseImpact: {
|
|
334
|
+
migrations: databaseImpact.migrations || [],
|
|
335
|
+
schemaChanges: databaseImpact.schemaChanges || [],
|
|
336
|
+
riskLevel: databaseImpact.riskLevel || 'none',
|
|
337
|
+
},
|
|
338
|
+
logicImpact: {
|
|
339
|
+
directCallers: logicImpact.directCallers || [],
|
|
340
|
+
indirectCallers: logicImpact.indirectCallers || [],
|
|
341
|
+
riskLevel: logicImpact.riskLevel || 'none',
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
violations: this.generateViolationsFromImpact(impact),
|
|
345
|
+
fileCategories,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Categorize changed files for reporting
|
|
351
|
+
*/
|
|
352
|
+
categorizeFiles(changedFiles) {
|
|
353
|
+
const categories = {
|
|
354
|
+
api: [],
|
|
355
|
+
database: [],
|
|
356
|
+
core: [],
|
|
357
|
+
ui: [],
|
|
358
|
+
tests: [],
|
|
359
|
+
config: [],
|
|
360
|
+
other: [],
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const patterns = {
|
|
364
|
+
api: [/controller/i, /route/i, /endpoint/i, /api\//i, /handler/i],
|
|
365
|
+
database: [/repository/i, /model/i, /schema/i, /migration/i, /entity/i, /dao/i],
|
|
366
|
+
core: [/service/i, /usecase/i, /domain/i, /core\//i, /lib\//i],
|
|
367
|
+
ui: [/component/i, /view/i, /page/i, /screen/i, /widget/i, /\.tsx$/i, /\.vue$/i],
|
|
368
|
+
tests: [/test/i, /spec/i, /__tests__/i],
|
|
369
|
+
config: [/config/i, /\.json$/i, /\.ya?ml$/i, /\.env/i, /dockerfile/i],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
for (const file of changedFiles) {
|
|
373
|
+
const filePath = file.path || file;
|
|
374
|
+
let categorized = false;
|
|
375
|
+
|
|
376
|
+
for (const [category, regexes] of Object.entries(patterns)) {
|
|
377
|
+
if (regexes.some(regex => regex.test(filePath))) {
|
|
378
|
+
categories[category].push(filePath);
|
|
379
|
+
categorized = true;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!categorized) {
|
|
385
|
+
categories.other.push(filePath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return categories;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Generate violations from impact analysis results
|
|
394
|
+
*/
|
|
395
|
+
generateViolationsFromImpact(impact) {
|
|
396
|
+
const violations = [];
|
|
397
|
+
|
|
398
|
+
// Endpoint impact violations
|
|
399
|
+
if (impact.affectedEndpoints && impact.affectedEndpoints.length > 0) {
|
|
400
|
+
violations.push({
|
|
401
|
+
ruleId: 'IMPACT-ENDPOINTS',
|
|
402
|
+
severity: impact.affectedEndpoints.length > 5 ? 'error' : 'warning',
|
|
403
|
+
message: `${impact.affectedEndpoints.length} API endpoint(s) affected by changes`,
|
|
404
|
+
line: 1,
|
|
405
|
+
column: 1,
|
|
406
|
+
category: 'impact',
|
|
407
|
+
source: 'impact-analyzer',
|
|
408
|
+
details: {
|
|
409
|
+
endpoints: impact.affectedEndpoints.slice(0, 10).map(ep =>
|
|
410
|
+
`${ep.method || 'GET'} ${ep.path}`
|
|
411
|
+
),
|
|
412
|
+
total: impact.affectedEndpoints.length,
|
|
413
|
+
recommendation: 'Review API contracts, update documentation, and verify backward compatibility',
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Database impact violations
|
|
419
|
+
if (impact.databaseImpact?.migrations?.length > 0 ||
|
|
420
|
+
impact.databaseImpact?.riskLevel !== 'none') {
|
|
421
|
+
violations.push({
|
|
422
|
+
ruleId: 'IMPACT-DATABASE',
|
|
423
|
+
severity: impact.databaseImpact.riskLevel === 'high' ? 'error' : 'warning',
|
|
424
|
+
message: `Database changes detected (${impact.databaseImpact.riskLevel} risk)`,
|
|
425
|
+
line: 1,
|
|
426
|
+
column: 1,
|
|
427
|
+
category: 'impact',
|
|
428
|
+
source: 'impact-analyzer',
|
|
429
|
+
details: {
|
|
430
|
+
migrations: impact.databaseImpact.migrations || [],
|
|
431
|
+
schemaChanges: impact.databaseImpact.schemaChanges || [],
|
|
432
|
+
riskLevel: impact.databaseImpact.riskLevel,
|
|
433
|
+
recommendation: 'Review migrations, ensure backward compatibility, and plan database rollback strategy',
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Logic impact violations
|
|
439
|
+
if (impact.logicImpact?.directCallers?.length > 5 ||
|
|
440
|
+
impact.logicImpact?.riskLevel === 'high') {
|
|
441
|
+
violations.push({
|
|
442
|
+
ruleId: 'IMPACT-LOGIC',
|
|
443
|
+
severity: impact.logicImpact.riskLevel === 'high' ? 'error' : 'warning',
|
|
444
|
+
message: `High logic impact: ${impact.logicImpact.directCallers?.length || 0} direct callers affected`,
|
|
445
|
+
line: 1,
|
|
446
|
+
column: 1,
|
|
447
|
+
category: 'impact',
|
|
448
|
+
source: 'impact-analyzer',
|
|
449
|
+
details: {
|
|
450
|
+
directCallers: impact.logicImpact.directCallers || [],
|
|
451
|
+
indirectCallers: impact.logicImpact.indirectCallers || [],
|
|
452
|
+
riskLevel: impact.logicImpact.riskLevel,
|
|
453
|
+
recommendation: 'Ensure comprehensive test coverage for affected code paths',
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// High severity overall impact
|
|
459
|
+
if (impact.severity === 'critical' || impact.impactScore > 70) {
|
|
460
|
+
violations.push({
|
|
461
|
+
ruleId: 'IMPACT-CRITICAL',
|
|
462
|
+
severity: 'error',
|
|
463
|
+
message: `Critical impact detected (score: ${impact.impactScore}/100)`,
|
|
464
|
+
line: 1,
|
|
465
|
+
column: 1,
|
|
466
|
+
category: 'impact',
|
|
467
|
+
source: 'impact-analyzer',
|
|
468
|
+
details: {
|
|
469
|
+
impactScore: impact.impactScore,
|
|
470
|
+
severity: impact.severity,
|
|
471
|
+
recommendation: 'This change has wide-reaching impact. Consider phased rollout and comprehensive testing.',
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return violations;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Generate markdown report (legacy - now handled by ReportGenerator)
|
|
481
|
+
*/
|
|
482
|
+
generateMarkdownReport(result) {
|
|
483
|
+
const { summary } = result;
|
|
484
|
+
const severityEmoji = {
|
|
485
|
+
critical: '🔴',
|
|
486
|
+
high: '🟠',
|
|
487
|
+
medium: '🟡',
|
|
488
|
+
low: '🟢',
|
|
489
|
+
none: '⚪',
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
let md = `# Impact Analysis Report\n\n`;
|
|
493
|
+
md += `**Base Reference:** \`${summary.baseRef}\`\n`;
|
|
494
|
+
md += `**Generated:** ${new Date().toISOString()}\n\n`;
|
|
495
|
+
|
|
496
|
+
md += `## Summary\n\n`;
|
|
497
|
+
md += `| Metric | Value |\n`;
|
|
498
|
+
md += `|--------|-------|\n`;
|
|
499
|
+
md += `| Total Changes | ${summary.totalChanges} files |\n`;
|
|
500
|
+
md += `| Impact Score | ${summary.impactScore}/100 |\n`;
|
|
501
|
+
md += `| Severity | ${severityEmoji[summary.severity]} ${summary.severity.toUpperCase()} |\n\n`;
|
|
502
|
+
|
|
503
|
+
md += `## Changes by Category\n\n`;
|
|
504
|
+
md += `| Category | Files |\n`;
|
|
505
|
+
md += `|----------|-------|\n`;
|
|
506
|
+
|
|
507
|
+
for (const [category, count] of Object.entries(summary.categories)) {
|
|
508
|
+
if (count > 0) {
|
|
509
|
+
md += `| ${category.charAt(0).toUpperCase() + category.slice(1)} | ${count} |\n`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
md += `\n## Changed Files\n\n`;
|
|
514
|
+
|
|
515
|
+
for (const [category, files] of Object.entries(changes)) {
|
|
516
|
+
if (files.length > 0) {
|
|
517
|
+
md += `### ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length})\n\n`;
|
|
518
|
+
for (const file of files) {
|
|
519
|
+
md += `- \`${file}\`\n`;
|
|
520
|
+
}
|
|
521
|
+
md += `\n`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
md += `---\n`;
|
|
526
|
+
md += `*Generated by SunLint Impact Analyzer*\n`;
|
|
527
|
+
|
|
528
|
+
return md;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Save markdown report to file
|
|
533
|
+
*/
|
|
534
|
+
async saveReport(markdownContent, outputPath) {
|
|
535
|
+
const resolvedPath = path.resolve(outputPath || 'impact-report.md');
|
|
536
|
+
fs.writeFileSync(resolvedPath, markdownContent, 'utf8');
|
|
537
|
+
return resolvedPath;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Save JSON report to file
|
|
542
|
+
*/
|
|
543
|
+
async saveJsonReport(results, outputPath) {
|
|
544
|
+
const resolvedPath = path.resolve(outputPath);
|
|
545
|
+
const jsonContent = JSON.stringify(results, null, 2);
|
|
546
|
+
fs.writeFileSync(resolvedPath, jsonContent, 'utf8');
|
|
547
|
+
return resolvedPath;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
module.exports = { ImpactIntegration };
|