@sun-asterisk/sunlint 1.3.47 ā 1.3.48
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/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/output-service.js +12 -3
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
|
@@ -9,8 +9,9 @@ const fs = require('fs');
|
|
|
9
9
|
const chalk = require('chalk');
|
|
10
10
|
|
|
11
11
|
class ArchitectureIntegration {
|
|
12
|
-
constructor(options = {}) {
|
|
12
|
+
constructor(options = {}, config = {}) {
|
|
13
13
|
this.options = options;
|
|
14
|
+
this.config = config; // Store merged config
|
|
14
15
|
this.archModule = null;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -78,13 +79,25 @@ class ArchitectureIntegration {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* Parse architecture patterns from CLI option
|
|
82
|
+
* Parse architecture patterns from CLI option or config
|
|
83
|
+
* Priority: CLI --arch-patterns > Config file patterns > Default (undefined)
|
|
82
84
|
*/
|
|
83
85
|
parsePatterns() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// Priority 1: CLI --arch-patterns flag
|
|
87
|
+
if (this.options.archPatterns) {
|
|
88
|
+
return this.parsePatternString(this.options.archPatterns);
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// Priority 2: Config file patterns array
|
|
92
|
+
if (this.config?.architecture?.patterns?.length > 0) {
|
|
93
|
+
return this.normalizePatterns(this.config.architecture.patterns);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Priority 3: Default (undefined = all patterns)
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
normalizePatterns(patterns) {
|
|
88
101
|
const patternMap = {
|
|
89
102
|
'layered': 'LAYERED',
|
|
90
103
|
'modular': 'MODULAR',
|
|
@@ -95,13 +108,46 @@ class ArchitectureIntegration {
|
|
|
95
108
|
'tdd': 'TDD_CLEAN_ARCHITECTURE',
|
|
96
109
|
};
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
.map(p => p.trim().toLowerCase())
|
|
111
|
+
return patterns
|
|
112
|
+
.map(p => typeof p === 'string' ? p.trim().toLowerCase() : p)
|
|
101
113
|
.map(p => patternMap[p] || p.toUpperCase())
|
|
102
114
|
.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
parsePatternString(patternString) {
|
|
118
|
+
const patterns = patternString.split(',').map(p => p.trim());
|
|
119
|
+
return this.normalizePatterns(patterns);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Determine if markdown report should be generated
|
|
124
|
+
* Priority: CLI flag > Config file > Default (false)
|
|
125
|
+
*/
|
|
126
|
+
shouldGenerateReport() {
|
|
127
|
+
if (this.options.archReport !== undefined) {
|
|
128
|
+
return this.options.archReport;
|
|
129
|
+
}
|
|
103
130
|
|
|
104
|
-
|
|
131
|
+
if (this.config?.architecture?.generateReport !== undefined) {
|
|
132
|
+
return this.config.architecture.generateReport;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get report filename from config or auto-generate
|
|
140
|
+
*/
|
|
141
|
+
getReportFilename(projectPath) {
|
|
142
|
+
// Use config reportOutput if provided
|
|
143
|
+
if (this.config?.architecture?.reportOutput) {
|
|
144
|
+
return this.config.architecture.reportOutput;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Auto-generate filename
|
|
148
|
+
const projectName = path.basename(projectPath);
|
|
149
|
+
const date = new Date().toISOString().split('T')[0];
|
|
150
|
+
return `sun_arch_report_${projectName}_${date}.md`;
|
|
105
151
|
}
|
|
106
152
|
|
|
107
153
|
/**
|
|
@@ -259,7 +305,7 @@ class ArchitectureIntegration {
|
|
|
259
305
|
|
|
260
306
|
// Generate markdown report if requested
|
|
261
307
|
let markdownReport = null;
|
|
262
|
-
if (this.
|
|
308
|
+
if (this.shouldGenerateReport()) {
|
|
263
309
|
try {
|
|
264
310
|
markdownReport = analyzer.formatAsMarkdown(result);
|
|
265
311
|
} catch (error) {
|
|
@@ -313,12 +359,8 @@ class ArchitectureIntegration {
|
|
|
313
359
|
/**
|
|
314
360
|
* Save markdown report to file
|
|
315
361
|
*/
|
|
316
|
-
async saveReport(markdownContent,
|
|
317
|
-
const
|
|
318
|
-
const date = new Date().toISOString().split('T')[0].replace(/-/g, '_');
|
|
319
|
-
const fileName = `sun_arch_report_${projectName}_${date}.md`;
|
|
320
|
-
const outputPath = path.join(process.cwd(), fileName);
|
|
321
|
-
|
|
362
|
+
async saveReport(markdownContent, reportFilename) {
|
|
363
|
+
const outputPath = path.join(process.cwd(), reportFilename);
|
|
322
364
|
fs.writeFileSync(outputPath, markdownContent, 'utf8');
|
|
323
365
|
return outputPath;
|
|
324
366
|
}
|
|
@@ -25,13 +25,13 @@ class CliActionHandler {
|
|
|
25
25
|
this.options = options;
|
|
26
26
|
this.configManager = null;
|
|
27
27
|
this.ruleSelectionService = new RuleSelectionService();
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// Use new orchestrator by default, fallback to legacy if needed
|
|
30
30
|
this.orchestrator = new AnalysisOrchestrator();
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
this.outputService = new OutputService(options);
|
|
33
33
|
this.fileTargetingService = new FileTargetingService();
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
this.isModernMode = !options.useLegacy;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -43,10 +43,11 @@ class CliActionHandler {
|
|
|
43
43
|
try {
|
|
44
44
|
this.displayModernBanner();
|
|
45
45
|
this.handleShortcuts();
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
// Load configuration
|
|
48
48
|
const config = await this.loadConfiguration();
|
|
49
|
-
|
|
49
|
+
this.loadedConfig = config; // Store for architecture integration
|
|
50
|
+
|
|
50
51
|
// Validate input with priority system
|
|
51
52
|
this.validateInput(config);
|
|
52
53
|
|
|
@@ -60,7 +61,7 @@ class CliActionHandler {
|
|
|
60
61
|
|
|
61
62
|
// Select rules to run
|
|
62
63
|
const rulesToRun = await this.ruleSelectionService.selectRules(config, this.options);
|
|
63
|
-
|
|
64
|
+
|
|
64
65
|
if (rulesToRun.length === 0) {
|
|
65
66
|
console.log(chalk.yellow('ā ļø No rules to run'));
|
|
66
67
|
return;
|
|
@@ -68,13 +69,20 @@ class CliActionHandler {
|
|
|
68
69
|
|
|
69
70
|
// Apply enhanced file targeting
|
|
70
71
|
const targetingResult = await this.applyFileTargeting(config);
|
|
71
|
-
|
|
72
|
+
|
|
73
|
+
// Determine if we should proceed based on requested analyses
|
|
74
|
+
const hasSourceFiles = targetingResult.files.length > 0;
|
|
75
|
+
const willRunCodeQuality = rulesToRun.length > 0 && !this.isArchitectureOnly() && !this.isImpactOnly();
|
|
76
|
+
const willRunArchitecture = !!config.architecture?.enabled;
|
|
77
|
+
const willRunImpact = !!(this.options.impact || config.impact?.enabled);
|
|
78
|
+
|
|
79
|
+
if (!hasSourceFiles && !willRunArchitecture && !willRunImpact) {
|
|
72
80
|
console.log(chalk.yellow('ā ļø No files to analyze after applying filters'));
|
|
73
81
|
this.displayTargetingStats(targetingResult.stats);
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
// Update options with filtered files
|
|
85
|
+
// Update options with filtered files (even if empty, for engines that handle it)
|
|
78
86
|
this.options.targetFiles = targetingResult.files;
|
|
79
87
|
|
|
80
88
|
// Display analysis info
|
|
@@ -91,8 +99,8 @@ class CliActionHandler {
|
|
|
91
99
|
results = { results: [], summary: { total: 0, errors: 0, warnings: 0 } };
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
// Run architecture analysis if requested
|
|
95
|
-
if (
|
|
102
|
+
// Run architecture analysis if requested (via CLI or Config)
|
|
103
|
+
if (config.architecture?.enabled) {
|
|
96
104
|
const architectureResults = await this.runArchitectureAnalysis();
|
|
97
105
|
results.architecture = architectureResults;
|
|
98
106
|
}
|
|
@@ -114,10 +122,10 @@ class CliActionHandler {
|
|
|
114
122
|
|
|
115
123
|
// Exit with appropriate code
|
|
116
124
|
this.handleExit(results);
|
|
117
|
-
|
|
125
|
+
|
|
118
126
|
} catch (error) {
|
|
119
127
|
console.error(chalk.red('ā Sun Lint Error:'), error.message);
|
|
120
|
-
|
|
128
|
+
|
|
121
129
|
// Following Rule C035: Log complete error context
|
|
122
130
|
if (this.options.debug) {
|
|
123
131
|
console.error('Full error context:', {
|
|
@@ -127,7 +135,7 @@ class CliActionHandler {
|
|
|
127
135
|
mode: this.isModernMode ? 'modern' : 'legacy'
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
|
-
|
|
138
|
+
|
|
131
139
|
process.exit(1);
|
|
132
140
|
}
|
|
133
141
|
}
|
|
@@ -150,7 +158,7 @@ class CliActionHandler {
|
|
|
150
158
|
verbose: this.options.verbose // Pass verbose for debugging
|
|
151
159
|
}
|
|
152
160
|
});
|
|
153
|
-
|
|
161
|
+
|
|
154
162
|
if (this.options.verbose) {
|
|
155
163
|
console.log(`š§ Debug: maxSemanticFiles option = ${this.options.maxSemanticFiles}`);
|
|
156
164
|
console.log(`š§ Debug: parsed maxSemanticFiles = ${this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : Infinity}`);
|
|
@@ -192,15 +200,15 @@ class CliActionHandler {
|
|
|
192
200
|
if (this.options.engine === 'auto') {
|
|
193
201
|
// Auto-select best engines: default to heuristic for compatibility
|
|
194
202
|
const autoEngines = ['heuristic'];
|
|
195
|
-
|
|
203
|
+
|
|
196
204
|
// Add ESLint for JS/TS files if available
|
|
197
205
|
if (this.hasJavaScriptTypeScriptFiles() || config.eslint?.enabled !== false) {
|
|
198
206
|
autoEngines.push('eslint');
|
|
199
207
|
}
|
|
200
|
-
|
|
208
|
+
|
|
201
209
|
return autoEngines;
|
|
202
210
|
}
|
|
203
|
-
|
|
211
|
+
|
|
204
212
|
// Return specific engine as requested
|
|
205
213
|
return [this.options.engine];
|
|
206
214
|
}
|
|
@@ -234,7 +242,7 @@ class CliActionHandler {
|
|
|
234
242
|
*/
|
|
235
243
|
validateAIConfiguration(config) {
|
|
236
244
|
const aiConfig = config.ai || {};
|
|
237
|
-
|
|
245
|
+
|
|
238
246
|
// Check for API key
|
|
239
247
|
if (!aiConfig.apiKey && !process.env.OPENAI_API_KEY) {
|
|
240
248
|
console.warn(chalk.yellow('ā ļø No OpenAI API key found in config or environment'));
|
|
@@ -256,7 +264,7 @@ class CliActionHandler {
|
|
|
256
264
|
*/
|
|
257
265
|
hasJavaScriptTypeScriptFiles() {
|
|
258
266
|
if (!this.options.targetFiles) return false;
|
|
259
|
-
|
|
267
|
+
|
|
260
268
|
return this.options.targetFiles.some(file => {
|
|
261
269
|
const ext = require('path').extname(file).toLowerCase();
|
|
262
270
|
return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext);
|
|
@@ -272,7 +280,7 @@ class CliActionHandler {
|
|
|
272
280
|
if (this.options.quiet || this.options.format === 'json') {
|
|
273
281
|
return;
|
|
274
282
|
}
|
|
275
|
-
|
|
283
|
+
|
|
276
284
|
const { version } = require('../package.json');
|
|
277
285
|
|
|
278
286
|
console.log();
|
|
@@ -284,7 +292,7 @@ class CliActionHandler {
|
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
// Delegate methods to base functionality (same as original CliActionHandler)
|
|
287
|
-
|
|
295
|
+
|
|
288
296
|
/**
|
|
289
297
|
* Load configuration using existing config manager
|
|
290
298
|
* Following Rule C006: Verb-noun naming
|
|
@@ -325,7 +333,7 @@ class CliActionHandler {
|
|
|
325
333
|
chalk.gray('Example: sunlint --all --output-summary=report.json --upload-report --input=src')
|
|
326
334
|
);
|
|
327
335
|
}
|
|
328
|
-
|
|
336
|
+
|
|
329
337
|
// Set default URL if no URL provided (when uploadReport is true)
|
|
330
338
|
if (typeof this.options.uploadReport === 'boolean' || !this.options.uploadReport) {
|
|
331
339
|
this.options.uploadReport = 'https://coding-standards-report.sun-asterisk.vn/api/reports';
|
|
@@ -333,7 +341,7 @@ class CliActionHandler {
|
|
|
333
341
|
console.log(chalk.gray(`ā¹ļø Using default upload URL: ${this.options.uploadReport}`));
|
|
334
342
|
}
|
|
335
343
|
}
|
|
336
|
-
|
|
344
|
+
|
|
337
345
|
// Basic URL validation
|
|
338
346
|
try {
|
|
339
347
|
new URL(this.options.uploadReport);
|
|
@@ -361,8 +369,8 @@ class CliActionHandler {
|
|
|
361
369
|
if (config && config.include && Array.isArray(config.include) && config.include.length > 0) {
|
|
362
370
|
// Config provides include patterns, use current directory as base
|
|
363
371
|
// Let FileTargetingService handle the include patterns from config
|
|
364
|
-
this.options.input = '.';
|
|
365
|
-
|
|
372
|
+
this.options.input = '.';
|
|
373
|
+
|
|
366
374
|
if (this.options.verbose) {
|
|
367
375
|
console.log(chalk.gray(`ā¹ļø Using config include patterns: ${config.include.join(', ')}`));
|
|
368
376
|
}
|
|
@@ -373,7 +381,7 @@ class CliActionHandler {
|
|
|
373
381
|
if (!this.options.input && (!config || !config.include)) {
|
|
374
382
|
// Set default input directory instead of glob patterns
|
|
375
383
|
this.options.input = '.'; // Current directory, let FileTargetingService handle patterns
|
|
376
|
-
|
|
384
|
+
|
|
377
385
|
if (this.options.verbose) {
|
|
378
386
|
console.log(chalk.gray('ā¹ļø Using default input: current directory with JS/TS file patterns'));
|
|
379
387
|
}
|
|
@@ -422,14 +430,14 @@ class CliActionHandler {
|
|
|
422
430
|
async showDryRunPreview(config, rulesToRun = null) {
|
|
423
431
|
console.log(chalk.blue('š Dry Run Preview'));
|
|
424
432
|
console.log(chalk.gray('This would analyze the following configuration:'));
|
|
425
|
-
|
|
433
|
+
|
|
426
434
|
let rulesInfo;
|
|
427
435
|
if (rulesToRun) {
|
|
428
436
|
rulesInfo = `${rulesToRun.length} rules (${this.getPresetName()})`;
|
|
429
437
|
} else {
|
|
430
438
|
rulesInfo = this.options.rules || 'config-based';
|
|
431
439
|
}
|
|
432
|
-
|
|
440
|
+
|
|
433
441
|
console.log(JSON.stringify({
|
|
434
442
|
rules: rulesInfo,
|
|
435
443
|
files: this.options.targetFiles?.length || 'auto-detected',
|
|
@@ -492,7 +500,7 @@ class CliActionHandler {
|
|
|
492
500
|
*/
|
|
493
501
|
displayTargetingStats(stats) {
|
|
494
502
|
if (this.options.quiet) return;
|
|
495
|
-
|
|
503
|
+
|
|
496
504
|
console.log(chalk.gray('Targeting Stats:'));
|
|
497
505
|
Object.entries(stats).forEach(([key, value]) => {
|
|
498
506
|
console.log(`⢠${key}: ${value}`);
|
|
@@ -523,8 +531,11 @@ class CliActionHandler {
|
|
|
523
531
|
* Following Rule C006: Verb-noun naming
|
|
524
532
|
*/
|
|
525
533
|
isArchitectureOnly() {
|
|
526
|
-
|
|
527
|
-
|
|
534
|
+
const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
|
|
535
|
+
const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
|
|
536
|
+
|
|
537
|
+
return isArchEnabled &&
|
|
538
|
+
!isImpactEnabled &&
|
|
528
539
|
!this.options.all &&
|
|
529
540
|
!this.options.specific &&
|
|
530
541
|
!this.options.rule &&
|
|
@@ -539,8 +550,11 @@ class CliActionHandler {
|
|
|
539
550
|
* Following Rule C006: Verb-noun naming
|
|
540
551
|
*/
|
|
541
552
|
isImpactOnly() {
|
|
542
|
-
|
|
543
|
-
|
|
553
|
+
const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
|
|
554
|
+
const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
|
|
555
|
+
|
|
556
|
+
return isImpactEnabled &&
|
|
557
|
+
!isArchEnabled &&
|
|
544
558
|
!this.options.all &&
|
|
545
559
|
!this.options.specific &&
|
|
546
560
|
!this.options.rule &&
|
|
@@ -560,13 +574,14 @@ class CliActionHandler {
|
|
|
560
574
|
}
|
|
561
575
|
|
|
562
576
|
try {
|
|
563
|
-
const integration = new ArchitectureIntegration(this.options);
|
|
577
|
+
const integration = new ArchitectureIntegration(this.options, this.loadedConfig);
|
|
564
578
|
const projectPath = this.getProjectPath();
|
|
565
579
|
const results = await integration.analyze(projectPath);
|
|
566
580
|
|
|
567
581
|
// Save markdown report if requested
|
|
568
|
-
if (
|
|
569
|
-
const
|
|
582
|
+
if (integration.shouldGenerateReport() && results.markdownReport) {
|
|
583
|
+
const reportFilename = integration.getReportFilename(projectPath);
|
|
584
|
+
const reportPath = await integration.saveReport(results.markdownReport, reportFilename);
|
|
570
585
|
if (!this.options.quiet) {
|
|
571
586
|
console.log(chalk.green(`š Architecture report saved: ${reportPath}`));
|
|
572
587
|
}
|
package/core/config-manager.js
CHANGED
package/core/config-merger.js
CHANGED
|
@@ -92,6 +92,12 @@ class ConfigMerger {
|
|
|
92
92
|
// Performance overrides
|
|
93
93
|
overrides.performance = this.applyPerformanceOverrides(overrides.performance, options);
|
|
94
94
|
|
|
95
|
+
// Architecture overrides
|
|
96
|
+
overrides.architecture = this.applyArchitectureOverrides(
|
|
97
|
+
overrides.architecture,
|
|
98
|
+
options
|
|
99
|
+
);
|
|
100
|
+
|
|
95
101
|
// Auto-expand include patterns if input doesn't match any existing patterns
|
|
96
102
|
const expandedOverrides = this.autoExpandIncludePatterns(overrides, options);
|
|
97
103
|
// Copy expanded properties back to overrides
|
|
@@ -139,6 +145,33 @@ class ConfigMerger {
|
|
|
139
145
|
return performance;
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Apply CLI architecture overrides to config
|
|
150
|
+
* Rule C006: verb-noun naming convention
|
|
151
|
+
* Priority: CLI flags > Config file > Defaults
|
|
152
|
+
*/
|
|
153
|
+
applyArchitectureOverrides(architectureConfig, options) {
|
|
154
|
+
const architecture = { ...architectureConfig };
|
|
155
|
+
|
|
156
|
+
// CLI --architecture overrides config enabled
|
|
157
|
+
if (options.architecture !== undefined) {
|
|
158
|
+
architecture.enabled = options.architecture;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// CLI --arch-patterns overrides config patterns
|
|
162
|
+
if (options.archPatterns) {
|
|
163
|
+
const patterns = options.archPatterns.split(',').map(p => p.trim());
|
|
164
|
+
architecture.patterns = patterns;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// CLI --arch-report overrides config generateReport
|
|
168
|
+
if (options.archReport !== undefined) {
|
|
169
|
+
architecture.generateReport = options.archReport;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return architecture;
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
/**
|
|
143
176
|
* Rule C006: applyEnvironmentVariables - verb-noun naming
|
|
144
177
|
*/
|
package/core/config-validator.js
CHANGED
|
@@ -11,12 +11,13 @@ class ConfigValidator {
|
|
|
11
11
|
this.validRuleValues = ['error', 'warning', 'info', 'warn', 'off', true, false, 0, 1, 2];
|
|
12
12
|
this.ruleValueMapping = {
|
|
13
13
|
0: 'off',
|
|
14
|
-
1: 'warning',
|
|
14
|
+
1: 'warning',
|
|
15
15
|
2: 'error',
|
|
16
16
|
'warn': 'warning',
|
|
17
17
|
true: 'warning',
|
|
18
18
|
false: 'off'
|
|
19
19
|
};
|
|
20
|
+
this.validArchitecturePatterns = ['layered', 'modular', 'mvvm', 'viper', 'presentation', 'clean', 'tdd'];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -29,6 +30,7 @@ class ConfigValidator {
|
|
|
29
30
|
this.validateIncludeExcludePatterns(config.include, config.exclude);
|
|
30
31
|
this.validateOutputFormat(config.output);
|
|
31
32
|
this.validateRuleValues(config.rules);
|
|
33
|
+
this.validateArchitectureConfig(config);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -109,7 +111,7 @@ class ConfigValidator {
|
|
|
109
111
|
|
|
110
112
|
// Check category configuration
|
|
111
113
|
const rule = rulesRegistry.rules[ruleId];
|
|
112
|
-
|
|
114
|
+
|
|
113
115
|
if (rule && config.categories && config.categories[rule.category] !== undefined) {
|
|
114
116
|
return this.normalizeRuleValue(config.categories[rule.category]);
|
|
115
117
|
}
|
|
@@ -121,6 +123,39 @@ class ConfigValidator {
|
|
|
121
123
|
|
|
122
124
|
return 'off';
|
|
123
125
|
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Rule C006: validateArchitectureConfig - verb-noun naming
|
|
129
|
+
* Rule C031: Specific validation logic for architecture configuration
|
|
130
|
+
*/
|
|
131
|
+
validateArchitectureConfig(config) {
|
|
132
|
+
if (!config.architecture) return;
|
|
133
|
+
|
|
134
|
+
const chalk = require('chalk');
|
|
135
|
+
const arch = config.architecture;
|
|
136
|
+
|
|
137
|
+
if (arch.enabled !== undefined && typeof arch.enabled !== 'boolean') {
|
|
138
|
+
console.warn(chalk.yellow('ā ļø architecture.enabled must be boolean'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (arch.patterns !== undefined && !Array.isArray(arch.patterns)) {
|
|
142
|
+
console.warn(chalk.yellow('ā ļø architecture.patterns must be an array'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (arch.patterns && Array.isArray(arch.patterns)) {
|
|
146
|
+
const invalid = arch.patterns.filter(p =>
|
|
147
|
+
!this.validArchitecturePatterns.includes(String(p).toLowerCase())
|
|
148
|
+
);
|
|
149
|
+
if (invalid.length > 0) {
|
|
150
|
+
console.warn(chalk.yellow(`ā ļø Unknown patterns: ${invalid.join(', ')}`));
|
|
151
|
+
console.warn(chalk.gray(` Valid: ${this.validArchitecturePatterns.join(', ')}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (arch.generateReport !== undefined && typeof arch.generateReport !== 'boolean') {
|
|
156
|
+
console.warn(chalk.yellow('ā ļø architecture.generateReport must be boolean'));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
124
159
|
}
|
|
125
160
|
|
|
126
161
|
module.exports = ConfigValidator;
|
package/core/output-service.js
CHANGED
|
@@ -195,8 +195,11 @@ class OutputService {
|
|
|
195
195
|
const totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
|
|
196
196
|
|
|
197
197
|
// Calculate LOC
|
|
198
|
+
// In changed-files mode (PR), use only the analyzed files for accurate scoring
|
|
198
199
|
let loc = 0;
|
|
199
|
-
if (options.
|
|
200
|
+
if (options.changedFiles && options.targetFiles && options.targetFiles.length > 0) {
|
|
201
|
+
loc = this.scoringService.calculateLOC(options.targetFiles);
|
|
202
|
+
} else if (options.input) {
|
|
200
203
|
const inputPaths = Array.isArray(options.input) ? options.input : [options.input];
|
|
201
204
|
for (const inputPath of inputPaths) {
|
|
202
205
|
if (fs.existsSync(inputPath)) {
|
|
@@ -210,9 +213,10 @@ class OutputService {
|
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
// Count violations
|
|
216
|
+
// Count violations by severity
|
|
214
217
|
const errorCount = violations.filter(v => v.severity === 'error').length;
|
|
215
218
|
const warningCount = violations.filter(v => v.severity === 'warning').length;
|
|
219
|
+
const infoCount = violations.filter(v => v.severity === 'info').length;
|
|
216
220
|
|
|
217
221
|
// Get number of rules checked - use metadata first, then parse from options
|
|
218
222
|
let rulesChecked = metadata.rulesChecked;
|
|
@@ -225,10 +229,14 @@ class OutputService {
|
|
|
225
229
|
}
|
|
226
230
|
rulesChecked = rulesChecked || 1;
|
|
227
231
|
|
|
232
|
+
// Determine scoring mode
|
|
233
|
+
const scoringMode = options.changedFiles ? 'pr' : 'project';
|
|
234
|
+
|
|
228
235
|
// Calculate score
|
|
229
236
|
const scoringSummary = this.scoringService.generateScoringSummary({
|
|
230
237
|
errorCount,
|
|
231
238
|
warningCount,
|
|
239
|
+
infoCount,
|
|
232
240
|
rulesChecked,
|
|
233
241
|
loc
|
|
234
242
|
});
|
|
@@ -243,7 +251,8 @@ class OutputService {
|
|
|
243
251
|
filesAnalyzed: totalFiles,
|
|
244
252
|
duration: metadata.duration,
|
|
245
253
|
version: metadata.version || this.version,
|
|
246
|
-
architecture: results.architecture || null
|
|
254
|
+
architecture: results.architecture || null,
|
|
255
|
+
scoringMode
|
|
247
256
|
}
|
|
248
257
|
);
|
|
249
258
|
|
package/core/scoring-service.js
CHANGED
|
@@ -22,9 +22,10 @@ class ScoringService {
|
|
|
22
22
|
//
|
|
23
23
|
this.weights = {
|
|
24
24
|
// Penalty per violation type (per KLOC)
|
|
25
|
-
//
|
|
25
|
+
// Severity ratio: error(6) : warning(2) : info(0.5) = 12:4:1
|
|
26
26
|
errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
|
|
27
27
|
warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
|
|
28
|
+
infoPenaltyPerKLOC: 0.5, // Each info per KLOC reduces score by 0.5 points
|
|
28
29
|
|
|
29
30
|
// Absolute penalty thresholds (regardless of LOC)
|
|
30
31
|
// Large projects should still be penalized for raw violation counts
|
|
@@ -57,11 +58,12 @@ class ScoringService {
|
|
|
57
58
|
* @param {Object} params
|
|
58
59
|
* @param {number} params.errorCount - Number of errors found
|
|
59
60
|
* @param {number} params.warningCount - Number of warnings found
|
|
61
|
+
* @param {number} params.infoCount - Number of info violations found
|
|
60
62
|
* @param {number} params.rulesChecked - Number of rules checked
|
|
61
63
|
* @param {number} params.loc - Total lines of code
|
|
62
64
|
* @returns {number} Score between 0-100
|
|
63
65
|
*/
|
|
64
|
-
calculateScore({ errorCount = 0, warningCount = 0, rulesChecked = 0, loc = 0 }) {
|
|
66
|
+
calculateScore({ errorCount = 0, warningCount = 0, infoCount = 0, rulesChecked = 0, loc = 0 }) {
|
|
65
67
|
// Base score starts at 100
|
|
66
68
|
let score = 100;
|
|
67
69
|
|
|
@@ -72,12 +74,13 @@ class ScoringService {
|
|
|
72
74
|
// Calculate violations per KLOC
|
|
73
75
|
const errorsPerKLOC = errorCount / kloc;
|
|
74
76
|
const warningsPerKLOC = warningCount / kloc;
|
|
75
|
-
const
|
|
77
|
+
const infosPerKLOC = infoCount / kloc;
|
|
76
78
|
|
|
77
79
|
// 1. Density-based penalty (main scoring factor)
|
|
78
80
|
// This penalizes based on how "dense" the violations are
|
|
79
81
|
const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
|
|
80
|
-
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC)
|
|
82
|
+
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC) +
|
|
83
|
+
(infosPerKLOC * this.weights.infoPenaltyPerKLOC);
|
|
81
84
|
score -= densityPenalty;
|
|
82
85
|
|
|
83
86
|
// 2. Absolute penalty for projects with too many errors
|
|
@@ -195,6 +198,8 @@ class ScoringService {
|
|
|
195
198
|
generateScoringSummary(params) {
|
|
196
199
|
const score = this.calculateScore(params);
|
|
197
200
|
const grade = this.getGrade(score);
|
|
201
|
+
const infoCount = params.infoCount || 0;
|
|
202
|
+
const totalViolations = params.errorCount + params.warningCount + infoCount;
|
|
198
203
|
|
|
199
204
|
return {
|
|
200
205
|
score,
|
|
@@ -202,10 +207,11 @@ class ScoringService {
|
|
|
202
207
|
metrics: {
|
|
203
208
|
errors: params.errorCount,
|
|
204
209
|
warnings: params.warningCount,
|
|
210
|
+
infos: infoCount,
|
|
205
211
|
rulesChecked: params.rulesChecked,
|
|
206
212
|
linesOfCode: params.loc,
|
|
207
|
-
violationsPerKLOC: params.loc > 0
|
|
208
|
-
? Math.round((
|
|
213
|
+
violationsPerKLOC: params.loc > 0
|
|
214
|
+
? Math.round((totalViolations / params.loc * 1000) * 10) / 10
|
|
209
215
|
: 0
|
|
210
216
|
}
|
|
211
217
|
};
|
|
@@ -247,11 +247,12 @@ class SummaryReportService {
|
|
|
247
247
|
total_violations: violations.length,
|
|
248
248
|
error_count: scoringSummary.metrics.errors,
|
|
249
249
|
warning_count: scoringSummary.metrics.warnings,
|
|
250
|
-
info_count: 0,
|
|
250
|
+
info_count: scoringSummary.metrics.infos || 0,
|
|
251
251
|
lines_of_code: scoringSummary.metrics.linesOfCode,
|
|
252
252
|
files_analyzed: options.filesAnalyzed || 0,
|
|
253
253
|
sunlint_version: options.version || this.version,
|
|
254
254
|
analysis_duration_ms: options.duration || 0,
|
|
255
|
+
scoring_mode: options.scoringMode || 'project',
|
|
255
256
|
violations: violationsSummary,
|
|
256
257
|
|
|
257
258
|
// Additional metadata for backwards compatibility
|
|
@@ -259,7 +260,8 @@ class SummaryReportService {
|
|
|
259
260
|
generated_at: new Date().toISOString(),
|
|
260
261
|
tool: 'SunLint',
|
|
261
262
|
version: options.version || this.version,
|
|
262
|
-
analysis_duration_ms: options.duration || 0
|
|
263
|
+
analysis_duration_ms: options.duration || 0,
|
|
264
|
+
scoring_mode: options.scoringMode || 'project'
|
|
263
265
|
},
|
|
264
266
|
quality: {
|
|
265
267
|
score: scoringSummary.score,
|
|
@@ -384,15 +386,18 @@ class SummaryReportService {
|
|
|
384
386
|
const totalViolations = summaryReport.total_violations || 0;
|
|
385
387
|
const errorCount = summaryReport.error_count || 0;
|
|
386
388
|
const warningCount = summaryReport.warning_count || 0;
|
|
389
|
+
const infoCount = summaryReport.info_count || 0;
|
|
387
390
|
const violationsByRule = summaryReport.violations || [];
|
|
388
391
|
const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
|
|
392
|
+
const scoringMode = summaryReport.scoring_mode || 'project';
|
|
389
393
|
|
|
390
394
|
let output = '\nš Quality Summary Report\n';
|
|
391
395
|
output += 'ā'.repeat(50) + '\n';
|
|
392
|
-
output += `š Quality Score: ${score} (Grade: ${grade})
|
|
396
|
+
output += `š Quality Score: ${score} (Grade: ${grade})`;
|
|
397
|
+
output += scoringMode === 'pr' ? ' [PR mode]\n' : '\n';
|
|
393
398
|
output += `š Files Analyzed: ${filesAnalyzed}\n`;
|
|
394
399
|
output += `š Lines of Code: ${linesOfCode.toLocaleString()}\n`;
|
|
395
|
-
output += `ā ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
|
|
400
|
+
output += `ā ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings, ${infoCount} info)\n`;
|
|
396
401
|
output += `š Violations per KLOC: ${violationsPerKLOC}\n`;
|
|
397
402
|
|
|
398
403
|
if (violationsByRule.length > 0) {
|