@sun-asterisk/sunlint 1.3.32 → 1.3.33
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 +47 -0
- package/core/architecture-integration.js +220 -0
- package/core/cli-action-handler.js +66 -5
- package/core/cli-program.js +13 -1
- package/core/output-service.js +87 -24
- package/core/scoring-service.js +65 -20
- package/core/upload-service.js +43 -9
- package/package.json +6 -5
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +19 -0
- package/scripts/copy-arch-detect.js +78 -0
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Sun Lint is a universal coding standards checker providing comprehensive code qu
|
|
|
19
19
|
- ✅ **Advanced File Targeting**: Include/exclude patterns, language filtering
|
|
20
20
|
- ✅ **Quality Scoring System**: Automated quality score (0-100) with grade (A+ to F)
|
|
21
21
|
- ✅ **Summary Reports**: JSON format for CI/CD dashboards and management reports
|
|
22
|
+
- ✅ **Architecture Detection**: Detect architecture patterns (Layered, Modular, MVVM, VIPER) with health scoring
|
|
22
23
|
|
|
23
24
|
### **🏗️ Architecture**
|
|
24
25
|
|
|
@@ -128,6 +129,52 @@ sunlint --all --input=src --output-summary=quality.json
|
|
|
128
129
|
- Quality gate setup
|
|
129
130
|
- Trending analysis
|
|
130
131
|
|
|
132
|
+
## 🏛️ **Architecture Detection** 🆕
|
|
133
|
+
|
|
134
|
+
Detect and analyze architecture patterns in your codebase with health scoring and violation detection.
|
|
135
|
+
|
|
136
|
+
### **Features**
|
|
137
|
+
- **Pattern Detection**: Layered, Modular, MVVM, VIPER, Clean Architecture
|
|
138
|
+
- **Health Score (0-100)**: Architecture compliance scoring
|
|
139
|
+
- **Violation Detection**: Identify architecture anti-patterns
|
|
140
|
+
- **Markdown Reports**: Detailed analysis reports for documentation
|
|
141
|
+
|
|
142
|
+
### **Quick Usage**
|
|
143
|
+
```bash
|
|
144
|
+
# Architecture analysis only
|
|
145
|
+
sunlint --architecture --input=src
|
|
146
|
+
|
|
147
|
+
# Combined: Code quality + Architecture
|
|
148
|
+
sunlint --all --architecture --input=src
|
|
149
|
+
|
|
150
|
+
# Generate markdown report
|
|
151
|
+
sunlint --architecture --arch-report --input=src
|
|
152
|
+
|
|
153
|
+
# Target specific patterns
|
|
154
|
+
sunlint --architecture --arch-patterns=mvvm,layered --input=src
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### **Example Output**
|
|
158
|
+
```
|
|
159
|
+
🏛️ Architecture Analysis:
|
|
160
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
161
|
+
• Pattern: LAYERED (69% confidence)
|
|
162
|
+
• Health Score: 85/100
|
|
163
|
+
• Violations: 3
|
|
164
|
+
|
|
165
|
+
Top Architecture Violations:
|
|
166
|
+
⚠ Interface/Contract Definitions - score 20% (threshold: 50%)
|
|
167
|
+
⚠ Middleware/Interceptor Layer - score 0% (threshold: 50%)
|
|
168
|
+
... and 1 more
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### **Options**
|
|
172
|
+
| Option | Description |
|
|
173
|
+
|--------|-------------|
|
|
174
|
+
| `--architecture` | Enable architecture pattern detection |
|
|
175
|
+
| `--arch-patterns <patterns>` | Target specific patterns (comma-separated: layered, modular, mvvm, viper) |
|
|
176
|
+
| `--arch-report` | Generate separate markdown report |
|
|
177
|
+
|
|
131
178
|
## 📦 **Installation**
|
|
132
179
|
|
|
133
180
|
### **Global Installation (Recommended)**
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture Integration for SunLint
|
|
3
|
+
* Wraps architecture-detection module for seamless integration
|
|
4
|
+
* Following Rule C005: Single responsibility - handle architecture analysis integration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
class ArchitectureIntegration {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.archModule = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load architecture detection module
|
|
19
|
+
* Tries bundled version first, then falls back to local development path
|
|
20
|
+
*/
|
|
21
|
+
async loadArchitectureModule() {
|
|
22
|
+
if (this.archModule) {
|
|
23
|
+
return this.archModule;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Try bundled version first (engines/arch-detect)
|
|
27
|
+
const bundledPath = path.join(__dirname, '..', 'engines', 'arch-detect', 'index.js');
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(bundledPath)) {
|
|
30
|
+
try {
|
|
31
|
+
this.archModule = require(bundledPath);
|
|
32
|
+
if (this.options.verbose) {
|
|
33
|
+
console.log(chalk.gray('📦 Loaded bundled architecture-detection'));
|
|
34
|
+
}
|
|
35
|
+
return this.archModule;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (this.options.verbose) {
|
|
38
|
+
console.log(chalk.yellow(`⚠️ Failed to load bundled module: ${error.message}`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback: Try local development path
|
|
44
|
+
const devPaths = [
|
|
45
|
+
path.join(__dirname, '..', '..', '..', '..', 'architecture-detection', 'dist', 'index.js'),
|
|
46
|
+
path.join(__dirname, '..', '..', '..', 'architecture-detection', 'dist', 'index.js'),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const devPath of devPaths) {
|
|
50
|
+
if (fs.existsSync(devPath)) {
|
|
51
|
+
try {
|
|
52
|
+
this.archModule = require(devPath);
|
|
53
|
+
if (this.options.verbose) {
|
|
54
|
+
console.log(chalk.gray(`📦 Loaded architecture-detection from: ${devPath}`));
|
|
55
|
+
}
|
|
56
|
+
return this.archModule;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (this.options.verbose) {
|
|
59
|
+
console.log(chalk.yellow(`⚠️ Failed to load from ${devPath}: ${error.message}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Architecture detection module not found. Run "npm run build" to bundle it, ' +
|
|
67
|
+
'or ensure architecture-detection is built in the parent directory.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse architecture patterns from CLI option
|
|
73
|
+
*/
|
|
74
|
+
parsePatterns() {
|
|
75
|
+
if (!this.options.archPatterns) {
|
|
76
|
+
return undefined; // Use default patterns
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const patternMap = {
|
|
80
|
+
'layered': 'LAYERED',
|
|
81
|
+
'modular': 'MODULAR',
|
|
82
|
+
'mvvm': 'MVVM',
|
|
83
|
+
'viper': 'VIPER',
|
|
84
|
+
'presentation': 'PRESENTATION',
|
|
85
|
+
'clean': 'CLEAN_ARCHITECTURE',
|
|
86
|
+
'tdd': 'TDD_CLEAN_ARCHITECTURE',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const patterns = this.options.archPatterns
|
|
90
|
+
.split(',')
|
|
91
|
+
.map(p => p.trim().toLowerCase())
|
|
92
|
+
.map(p => patternMap[p] || p.toUpperCase())
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
|
|
95
|
+
return patterns.length > 0 ? patterns : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run architecture analysis on project
|
|
100
|
+
* @param {string} projectPath - Path to analyze
|
|
101
|
+
* @returns {Object} Architecture analysis results
|
|
102
|
+
*/
|
|
103
|
+
async analyze(projectPath) {
|
|
104
|
+
const archModule = await this.loadArchitectureModule();
|
|
105
|
+
const { ArchitectureAnalyzer } = archModule;
|
|
106
|
+
|
|
107
|
+
if (!ArchitectureAnalyzer) {
|
|
108
|
+
throw new Error('ArchitectureAnalyzer not found in architecture-detection module');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const analyzer = new ArchitectureAnalyzer({
|
|
112
|
+
patterns: this.parsePatterns(),
|
|
113
|
+
respectGitignore: true,
|
|
114
|
+
verbose: this.options.verbose,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (this.options.verbose) {
|
|
118
|
+
console.log(chalk.blue(`🏛️ Analyzing architecture: ${projectPath}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = await analyzer.analyze(projectPath);
|
|
122
|
+
|
|
123
|
+
// Convert to SunLint-compatible format
|
|
124
|
+
return this.convertToSunLintFormat(result, analyzer, projectPath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert architecture results to SunLint format
|
|
129
|
+
*/
|
|
130
|
+
convertToSunLintFormat(result, analyzer, projectPath) {
|
|
131
|
+
const violations = [];
|
|
132
|
+
|
|
133
|
+
// Convert architecture violations to SunLint violation format
|
|
134
|
+
// Check violationAssessment.violations from the new format
|
|
135
|
+
const violationAssessment = result.violationAssessment;
|
|
136
|
+
if (violationAssessment && violationAssessment.violations && violationAssessment.violations.length > 0) {
|
|
137
|
+
for (const violation of violationAssessment.violations) {
|
|
138
|
+
violations.push({
|
|
139
|
+
ruleId: `ARCH-${violation.type || 'VIOLATION'}`,
|
|
140
|
+
severity: this.mapSeverity(violation.impact),
|
|
141
|
+
message: violation.description || violation.ruleName,
|
|
142
|
+
file: violation.affectedFiles?.[0] || projectPath,
|
|
143
|
+
line: 1,
|
|
144
|
+
column: 1,
|
|
145
|
+
category: 'architecture',
|
|
146
|
+
source: 'architecture-detection',
|
|
147
|
+
details: {
|
|
148
|
+
ruleName: violation.ruleName,
|
|
149
|
+
impactReason: violation.impactReason,
|
|
150
|
+
suggestedFix: violation.suggestedFix,
|
|
151
|
+
affectedFiles: violation.affectedFiles,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Generate markdown report if requested
|
|
158
|
+
let markdownReport = null;
|
|
159
|
+
if (this.options.archReport) {
|
|
160
|
+
try {
|
|
161
|
+
markdownReport = analyzer.formatAsMarkdown(result);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (this.options.verbose) {
|
|
164
|
+
console.log(chalk.yellow(`⚠️ Could not generate markdown report: ${error.message}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Map from hybridAnalysis structure
|
|
170
|
+
const hybridAnalysis = result.hybridAnalysis || {};
|
|
171
|
+
const healthScore = violationAssessment?.healthScore || 100;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
summary: {
|
|
175
|
+
primaryPattern: hybridAnalysis.primaryPattern || result.primaryPattern || 'UNKNOWN',
|
|
176
|
+
primaryConfidence: hybridAnalysis.confidence || 0,
|
|
177
|
+
secondaryPatterns: (hybridAnalysis.secondaryPatterns || []).map(p => ({
|
|
178
|
+
pattern: p,
|
|
179
|
+
confidence: 0.5, // Default confidence for secondary patterns
|
|
180
|
+
})),
|
|
181
|
+
healthScore: healthScore,
|
|
182
|
+
violationCount: violations.length,
|
|
183
|
+
analysisTime: result.metadata?.analysisTimeMs || 0,
|
|
184
|
+
isHybrid: hybridAnalysis.isHybrid || false,
|
|
185
|
+
combination: hybridAnalysis.combination || null,
|
|
186
|
+
},
|
|
187
|
+
violations,
|
|
188
|
+
markdownReport,
|
|
189
|
+
raw: result,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Map architecture severity to SunLint severity
|
|
195
|
+
*/
|
|
196
|
+
mapSeverity(severity) {
|
|
197
|
+
const severityMap = {
|
|
198
|
+
'critical': 'error',
|
|
199
|
+
'high': 'error',
|
|
200
|
+
'medium': 'warning',
|
|
201
|
+
'low': 'info',
|
|
202
|
+
};
|
|
203
|
+
return severityMap[severity?.toLowerCase()] || 'warning';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Save markdown report to file
|
|
208
|
+
*/
|
|
209
|
+
async saveReport(markdownContent, projectPath) {
|
|
210
|
+
const projectName = path.basename(projectPath);
|
|
211
|
+
const date = new Date().toISOString().split('T')[0].replace(/-/g, '_');
|
|
212
|
+
const fileName = `sun_arch_report_${projectName}_${date}.md`;
|
|
213
|
+
const outputPath = path.join(process.cwd(), fileName);
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(outputPath, markdownContent, 'utf8');
|
|
216
|
+
return outputPath;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { ArchitectureIntegration };
|
|
@@ -14,6 +14,7 @@ const AnalysisOrchestrator = require('./analysis-orchestrator');
|
|
|
14
14
|
const OutputService = require('./output-service');
|
|
15
15
|
const GitUtils = require('./git-utils');
|
|
16
16
|
const FileTargetingService = require('./file-targeting-service');
|
|
17
|
+
const { ArchitectureIntegration } = require('./architecture-integration');
|
|
17
18
|
|
|
18
19
|
// Legacy orchestrator for fallback
|
|
19
20
|
// const LegacyOrchestrator = require('./legacy-analysis-orchestrator'); // Removed
|
|
@@ -80,11 +81,25 @@ class CliActionHandler {
|
|
|
80
81
|
|
|
81
82
|
// Run analysis with appropriate orchestrator
|
|
82
83
|
const startTime = Date.now();
|
|
83
|
-
|
|
84
|
+
let results = null;
|
|
85
|
+
|
|
86
|
+
// Run code quality analysis (unless --architecture is used alone)
|
|
87
|
+
if (rulesToRun.length > 0 && !this.isArchitectureOnly()) {
|
|
88
|
+
results = await this.runModernAnalysis(rulesToRun, targetingResult.files, config);
|
|
89
|
+
} else {
|
|
90
|
+
results = { results: [], summary: { total: 0, errors: 0, warnings: 0 } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run architecture analysis if requested
|
|
94
|
+
if (this.options.architecture) {
|
|
95
|
+
const architectureResults = await this.runArchitectureAnalysis();
|
|
96
|
+
results.architecture = architectureResults;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
const duration = Date.now() - startTime;
|
|
85
100
|
|
|
86
101
|
// Output results
|
|
87
|
-
await this.outputService.outputResults(results, this.options, {
|
|
102
|
+
await this.outputService.outputResults(results, this.options, {
|
|
88
103
|
duration,
|
|
89
104
|
rulesRun: rulesToRun.length,
|
|
90
105
|
rulesChecked: rulesToRun.length
|
|
@@ -494,18 +509,64 @@ class CliActionHandler {
|
|
|
494
509
|
*/
|
|
495
510
|
handleExit(results) {
|
|
496
511
|
if (this.options.noExit) return;
|
|
497
|
-
|
|
512
|
+
|
|
498
513
|
// Check if any violations were found
|
|
499
|
-
const hasViolations = results.results?.some(result =>
|
|
514
|
+
const hasViolations = results.results?.some(result =>
|
|
500
515
|
result.violations && result.violations.length > 0
|
|
501
516
|
);
|
|
502
|
-
|
|
517
|
+
|
|
503
518
|
if (hasViolations && this.options.failOnViolations !== false) {
|
|
504
519
|
process.exit(1);
|
|
505
520
|
} else {
|
|
506
521
|
process.exit(0);
|
|
507
522
|
}
|
|
508
523
|
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Check if only architecture analysis was requested (no code quality rules)
|
|
527
|
+
* Following Rule C006: Verb-noun naming
|
|
528
|
+
*/
|
|
529
|
+
isArchitectureOnly() {
|
|
530
|
+
return this.options.architecture &&
|
|
531
|
+
!this.options.all &&
|
|
532
|
+
!this.options.rule &&
|
|
533
|
+
!this.options.rules &&
|
|
534
|
+
!this.options.quality &&
|
|
535
|
+
!this.options.security &&
|
|
536
|
+
!this.options.category;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Run architecture analysis using architecture-detection module
|
|
541
|
+
* Following Rule C006: Verb-noun naming
|
|
542
|
+
*/
|
|
543
|
+
async runArchitectureAnalysis() {
|
|
544
|
+
if (!this.options.quiet) {
|
|
545
|
+
console.log(chalk.blue('🏛️ Running architecture analysis...'));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const integration = new ArchitectureIntegration(this.options);
|
|
550
|
+
const projectPath = this.getProjectPath();
|
|
551
|
+
const results = await integration.analyze(projectPath);
|
|
552
|
+
|
|
553
|
+
// Save markdown report if requested
|
|
554
|
+
if (this.options.archReport && results.markdownReport) {
|
|
555
|
+
const reportPath = await integration.saveReport(results.markdownReport, projectPath);
|
|
556
|
+
if (!this.options.quiet) {
|
|
557
|
+
console.log(chalk.green(`📄 Architecture report saved: ${reportPath}`));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return results;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error(chalk.yellow(`⚠️ Architecture analysis failed: ${error.message}`));
|
|
564
|
+
if (this.options.debug) {
|
|
565
|
+
console.error(error.stack);
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
509
570
|
}
|
|
510
571
|
|
|
511
572
|
module.exports = CliActionHandler;
|
package/core/cli-program.js
CHANGED
|
@@ -23,6 +23,12 @@ function createCliProgram() {
|
|
|
23
23
|
.option('--quality', 'Run all code quality rules')
|
|
24
24
|
.option('--security', 'Run all secure coding rules');
|
|
25
25
|
|
|
26
|
+
// Architecture Analysis options
|
|
27
|
+
program
|
|
28
|
+
.option('--architecture', 'Enable architecture pattern detection (layered, modular, mvvm, viper)')
|
|
29
|
+
.option('--arch-patterns <patterns>', 'Target specific architecture patterns (comma-separated)')
|
|
30
|
+
.option('--arch-report', 'Generate separate architecture MD report');
|
|
31
|
+
|
|
26
32
|
// TypeScript specific options (Phase 1 focus)
|
|
27
33
|
program
|
|
28
34
|
.option('--typescript', 'Enable TypeScript-specific analysis')
|
|
@@ -156,11 +162,17 @@ Advanced File Targeting:
|
|
|
156
162
|
|
|
157
163
|
Large Project Optimization:
|
|
158
164
|
$ sunlint --all --input=. --max-semantic-files=500 # Conservative analysis
|
|
159
|
-
$ sunlint --all --input=. --max-semantic-files=2000 # Comprehensive analysis
|
|
165
|
+
$ sunlint --all --input=. --max-semantic-files=2000 # Comprehensive analysis
|
|
160
166
|
$ sunlint --all --input=. --max-semantic-files=-1 # Unlimited (all files)
|
|
161
167
|
$ sunlint --all --input=. --max-semantic-files=0 # Disable semantic analysis
|
|
162
168
|
$ sunlint --all --changed-files --max-semantic-files=300 # Fast CI analysis
|
|
163
169
|
|
|
170
|
+
Architecture Analysis:
|
|
171
|
+
$ sunlint --all --architecture --input=src # Code quality + architecture
|
|
172
|
+
$ sunlint --architecture --input=src # Architecture only
|
|
173
|
+
$ sunlint --architecture --arch-report --input=src # Generate MD report
|
|
174
|
+
$ sunlint --architecture --arch-patterns=mvvm,layered --input=src
|
|
175
|
+
|
|
164
176
|
Sun* Engineering - Coding Standards Made Simple ☀️
|
|
165
177
|
`);
|
|
166
178
|
|
package/core/output-service.js
CHANGED
|
@@ -115,6 +115,74 @@ class OutputService {
|
|
|
115
115
|
if (!options.quiet && options.format !== 'json') {
|
|
116
116
|
console.log(report.summary);
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
// Output architecture results if available
|
|
120
|
+
if (results.architecture && !options.quiet) {
|
|
121
|
+
this.outputArchitectureResults(results.architecture, options);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Output architecture analysis results
|
|
127
|
+
* @param {Object} archResults - Architecture analysis results
|
|
128
|
+
* @param {Object} options - Output options
|
|
129
|
+
*/
|
|
130
|
+
outputArchitectureResults(archResults, options) {
|
|
131
|
+
if (!archResults || !archResults.summary) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const summary = archResults.summary;
|
|
136
|
+
|
|
137
|
+
console.log(chalk.blue('\n🏛️ Architecture Analysis:'));
|
|
138
|
+
console.log('━'.repeat(50));
|
|
139
|
+
|
|
140
|
+
// Primary pattern
|
|
141
|
+
const confidence = Math.round(summary.primaryConfidence * 100);
|
|
142
|
+
console.log(`• Pattern: ${chalk.cyan(summary.primaryPattern)} (${confidence}% confidence)`);
|
|
143
|
+
|
|
144
|
+
// Secondary patterns
|
|
145
|
+
if (summary.secondaryPatterns && summary.secondaryPatterns.length > 0) {
|
|
146
|
+
const secondary = summary.secondaryPatterns
|
|
147
|
+
.map(p => `${p.pattern} (${Math.round(p.confidence * 100)}%)`)
|
|
148
|
+
.join(', ');
|
|
149
|
+
console.log(`• Secondary: ${chalk.gray(secondary)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Health score
|
|
153
|
+
const healthScore = Math.round(summary.healthScore);
|
|
154
|
+
const healthColor = healthScore >= 80 ? chalk.green :
|
|
155
|
+
healthScore >= 60 ? chalk.yellow : chalk.red;
|
|
156
|
+
console.log(`• Health Score: ${healthColor(healthScore + '/100')}`);
|
|
157
|
+
|
|
158
|
+
// Violations
|
|
159
|
+
if (summary.violationCount > 0) {
|
|
160
|
+
console.log(`• Violations: ${chalk.red(summary.violationCount)}`);
|
|
161
|
+
|
|
162
|
+
// Show first 5 violations
|
|
163
|
+
if (archResults.violations && archResults.violations.length > 0) {
|
|
164
|
+
console.log(chalk.gray('\nTop Architecture Violations:'));
|
|
165
|
+
archResults.violations.slice(0, 5).forEach((v, i) => {
|
|
166
|
+
const severity = v.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
|
|
167
|
+
console.log(` ${severity} ${v.message}`);
|
|
168
|
+
if (v.file) {
|
|
169
|
+
console.log(chalk.gray(` → ${v.file}:${v.line || 1}`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (archResults.violations.length > 5) {
|
|
173
|
+
console.log(chalk.gray(` ... and ${archResults.violations.length - 5} more`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`• Violations: ${chalk.green('None')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Report file info
|
|
181
|
+
if (archResults.markdownReport && options.archReport) {
|
|
182
|
+
console.log(chalk.gray('\n📄 Full architecture report saved'));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log();
|
|
118
186
|
}
|
|
119
187
|
|
|
120
188
|
generateAndSaveSummaryReport(violations, results, options, metadata) {
|
|
@@ -463,35 +531,30 @@ class OutputService {
|
|
|
463
531
|
});
|
|
464
532
|
|
|
465
533
|
if (uploadResult.success) {
|
|
466
|
-
if (!options.quiet) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
console.log(chalk.blue(`👤 Submitted by: ${responseData.actor}`));
|
|
485
|
-
}
|
|
486
|
-
} catch (parseError) {
|
|
487
|
-
// If response is not JSON, show raw response
|
|
534
|
+
if (!options.quiet && uploadResult.response) {
|
|
535
|
+
try {
|
|
536
|
+
const responseData = JSON.parse(uploadResult.response);
|
|
537
|
+
if (responseData.message) {
|
|
538
|
+
console.log(chalk.blue(`💬 Server response: ${responseData.message}`));
|
|
539
|
+
}
|
|
540
|
+
if (responseData.report_id) {
|
|
541
|
+
console.log(chalk.blue(`📝 Report ID: ${responseData.report_id}`));
|
|
542
|
+
}
|
|
543
|
+
if (responseData.repository) {
|
|
544
|
+
console.log(chalk.blue(`🏠 Repository: ${responseData.repository}`));
|
|
545
|
+
}
|
|
546
|
+
if (responseData.actor) {
|
|
547
|
+
console.log(chalk.blue(`👤 Submitted by: ${responseData.actor}`));
|
|
548
|
+
}
|
|
549
|
+
} catch (parseError) {
|
|
550
|
+
// If response is not JSON, show raw response if verbose
|
|
551
|
+
if (options.verbose) {
|
|
488
552
|
console.log(chalk.gray(`📄 Response: ${uploadResult.response.substring(0, 200)}...`));
|
|
489
553
|
}
|
|
490
554
|
}
|
|
491
555
|
}
|
|
492
556
|
} else {
|
|
493
|
-
|
|
494
|
-
|
|
557
|
+
// Error already logged in upload-service.js
|
|
495
558
|
if (options.verbose && uploadResult.errorContext) {
|
|
496
559
|
console.warn('Upload error details:', uploadResult.errorContext);
|
|
497
560
|
}
|
package/core/scoring-service.js
CHANGED
|
@@ -10,19 +10,50 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
class ScoringService {
|
|
12
12
|
constructor() {
|
|
13
|
-
// Scoring weights
|
|
13
|
+
// Scoring weights based on violations per KLOC (1000 lines)
|
|
14
|
+
//
|
|
15
|
+
// Calibration targets:
|
|
16
|
+
// - 0 violations/KLOC = 100 (A+)
|
|
17
|
+
// - 1-2 violations/KLOC = 90-95 (A/A+)
|
|
18
|
+
// - 3-4 violations/KLOC = 80-89 (B/B+)
|
|
19
|
+
// - 5-7 violations/KLOC = 70-79 (C/C+)
|
|
20
|
+
// - 8-10 violations/KLOC = 60-69 (D)
|
|
21
|
+
// - >10 violations/KLOC = <60 (F)
|
|
22
|
+
//
|
|
14
23
|
this.weights = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
// Penalty per violation type (per KLOC)
|
|
25
|
+
// Errors are 3x more severe than warnings
|
|
26
|
+
errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
|
|
27
|
+
warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
|
|
28
|
+
|
|
29
|
+
// Absolute penalty thresholds (regardless of LOC)
|
|
30
|
+
// Large projects should still be penalized for raw violation counts
|
|
31
|
+
absoluteErrorThreshold: 20, // Start penalizing if errors > 20
|
|
32
|
+
absoluteErrorPenalty: 0.05, // Each error above threshold reduces score by 0.05 (max 15 pts)
|
|
33
|
+
|
|
34
|
+
absoluteWarningThreshold: 100, // Start penalizing if warnings > 100
|
|
35
|
+
absoluteWarningPenalty: 0.01, // Each warning above threshold reduces score by 0.01 (max 10 pts)
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Thresholds for violations per KLOC (used for grading reference)
|
|
39
|
+
this.thresholds = {
|
|
40
|
+
excellent: 1, // < 1 violation per KLOC = excellent (A+/A)
|
|
41
|
+
good: 3, // < 3 violations per KLOC = good (B+/B)
|
|
42
|
+
acceptable: 5, // < 5 violations per KLOC = acceptable (C+/C)
|
|
43
|
+
poor: 10, // < 10 violations per KLOC = poor (D)
|
|
44
|
+
// >= 10 = very poor (F)
|
|
19
45
|
};
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
/**
|
|
23
49
|
* Calculate quality score
|
|
24
|
-
*
|
|
25
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* New formula (v2):
|
|
52
|
+
* 1. Calculate violations per KLOC (errorsPerKLOC, warningsPerKLOC)
|
|
53
|
+
* 2. Apply penalty based on density: errorsPerKLOC * 2 + warningsPerKLOC * 0.5
|
|
54
|
+
* 3. Apply absolute penalty for projects with too many errors
|
|
55
|
+
* 4. Add small bonus for rules checked
|
|
56
|
+
*
|
|
26
57
|
* @param {Object} params
|
|
27
58
|
* @param {number} params.errorCount - Number of errors found
|
|
28
59
|
* @param {number} params.warningCount - Number of warnings found
|
|
@@ -34,20 +65,34 @@ class ScoringService {
|
|
|
34
65
|
// Base score starts at 100
|
|
35
66
|
let score = 100;
|
|
36
67
|
|
|
37
|
-
// Calculate
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
68
|
+
// Calculate KLOC (thousands of lines of code)
|
|
69
|
+
const kloc = Math.max(loc / 1000, 1); // Minimum 1 KLOC to avoid division issues
|
|
70
|
+
|
|
71
|
+
// Calculate violations per KLOC
|
|
72
|
+
const errorsPerKLOC = errorCount / kloc;
|
|
73
|
+
const warningsPerKLOC = warningCount / kloc;
|
|
74
|
+
const totalViolationsPerKLOC = errorsPerKLOC + warningsPerKLOC;
|
|
75
|
+
|
|
76
|
+
// 1. Density-based penalty (main scoring factor)
|
|
77
|
+
// This penalizes based on how "dense" the violations are
|
|
78
|
+
const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
|
|
79
|
+
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC);
|
|
80
|
+
score -= densityPenalty;
|
|
81
|
+
|
|
82
|
+
// 2. Absolute penalty for projects with too many errors
|
|
83
|
+
// Even large codebases should not have hundreds of errors
|
|
84
|
+
if (errorCount > this.weights.absoluteErrorThreshold) {
|
|
85
|
+
const excessErrors = errorCount - this.weights.absoluteErrorThreshold;
|
|
86
|
+
const absoluteErrorPenalty = excessErrors * this.weights.absoluteErrorPenalty;
|
|
87
|
+
score -= Math.min(absoluteErrorPenalty, 15); // Cap at 15 points
|
|
88
|
+
}
|
|
47
89
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
// 3. Absolute penalty for projects with too many warnings
|
|
91
|
+
if (warningCount > this.weights.absoluteWarningThreshold) {
|
|
92
|
+
const excessWarnings = warningCount - this.weights.absoluteWarningThreshold;
|
|
93
|
+
const absoluteWarningPenalty = excessWarnings * this.weights.absoluteWarningPenalty;
|
|
94
|
+
score -= Math.min(absoluteWarningPenalty, 10); // Cap at 10 points
|
|
95
|
+
}
|
|
51
96
|
|
|
52
97
|
// Ensure score is between 0-100
|
|
53
98
|
score = Math.max(0, Math.min(100, score));
|
package/core/upload-service.js
CHANGED
|
@@ -24,15 +24,49 @@ class UploadService {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const uploadResult = await this.executeUploadCommand(filePath, apiUrl, options);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
|
|
28
|
+
// Check if upload was actually successful based on HTTP status code
|
|
29
|
+
const statusCode = uploadResult.statusCode;
|
|
30
|
+
const isSuccess = statusCode >= 200 && statusCode < 300;
|
|
31
|
+
|
|
32
|
+
if (isSuccess) {
|
|
33
|
+
console.log(chalk.green(`✅ Report uploaded successfully!`));
|
|
34
|
+
console.log(chalk.green(`📡 HTTP Status: ${statusCode}`));
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
url: apiUrl,
|
|
38
|
+
filePath: filePath,
|
|
39
|
+
response: uploadResult.response,
|
|
40
|
+
statusCode: statusCode
|
|
41
|
+
};
|
|
42
|
+
} else {
|
|
43
|
+
// Handle non-success HTTP status codes
|
|
44
|
+
const errorMessages = {
|
|
45
|
+
401: 'Unauthorized - Authentication required. Make sure OIDC token is configured correctly.',
|
|
46
|
+
403: 'Forbidden - Access denied. Check your permissions.',
|
|
47
|
+
404: 'Not Found - API endpoint does not exist.',
|
|
48
|
+
500: 'Internal Server Error - Server-side issue.',
|
|
49
|
+
502: 'Bad Gateway - Server is temporarily unavailable.',
|
|
50
|
+
503: 'Service Unavailable - Server is overloaded or under maintenance.'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const errorMessage = errorMessages[statusCode] || `HTTP Error ${statusCode}`;
|
|
54
|
+
console.error(chalk.red(`❌ Upload failed: ${errorMessage}`));
|
|
55
|
+
console.error(chalk.red(`📡 HTTP Status: ${statusCode}`));
|
|
56
|
+
|
|
57
|
+
if (uploadResult.response) {
|
|
58
|
+
console.error(chalk.yellow(`📄 Response: ${uploadResult.response}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
url: apiUrl,
|
|
64
|
+
filePath: filePath,
|
|
65
|
+
response: uploadResult.response,
|
|
66
|
+
statusCode: statusCode,
|
|
67
|
+
error: errorMessage
|
|
68
|
+
};
|
|
69
|
+
}
|
|
36
70
|
|
|
37
71
|
} catch (error) {
|
|
38
72
|
const errorContext = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sunlint",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.33",
|
|
4
4
|
"description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -41,18 +41,19 @@
|
|
|
41
41
|
"demo:file-targeting": "./demo-file-targeting.sh",
|
|
42
42
|
"lint": "node cli.js --config=.sunlint.json --input=.",
|
|
43
43
|
"lint:eslint-integration": "node cli.js --all --eslint-integration --input=.",
|
|
44
|
-
"build": "npm run copy-rules && npm run generate-registry &&
|
|
44
|
+
"build": "npm run copy-rules && npm run generate-registry && npm run copy-arch-detect && echo 'Build completed'",
|
|
45
45
|
"copy-rules": "node scripts/copy-rules.js",
|
|
46
46
|
"generate-registry": "node scripts/generate-rules-registry.js",
|
|
47
|
-
"
|
|
47
|
+
"copy-arch-detect": "node scripts/copy-arch-detect.js",
|
|
48
|
+
"clean": "rm -rf coverage/ *.log reports/ *.tgz engines/arch-detect",
|
|
48
49
|
"postpack": "echo '📦 Package created successfully! Size: ' && ls -lh *.tgz | awk '{print $5}'",
|
|
49
50
|
"start": "node cli.js --help",
|
|
50
51
|
"version": "node cli.js --version",
|
|
51
|
-
"pack": "npm run
|
|
52
|
+
"pack": "npm run build && npm pack",
|
|
52
53
|
"publish:github": "npm publish --registry=https://npm.pkg.github.com",
|
|
53
54
|
"publish:npmjs": "npm publish --registry=https://registry.npmjs.org",
|
|
54
55
|
"publish:test": "npm publish --dry-run --registry=https://registry.npmjs.org",
|
|
55
|
-
"prepublishOnly": "npm run clean && npm run
|
|
56
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
56
57
|
},
|
|
57
58
|
"keywords": [
|
|
58
59
|
"linting",
|
|
@@ -114,6 +114,25 @@ class C024SymbolBasedAnalyzer {
|
|
|
114
114
|
'commit',
|
|
115
115
|
'useState',
|
|
116
116
|
'useReducer',
|
|
117
|
+
// Logging functions
|
|
118
|
+
'console.log',
|
|
119
|
+
'console.error',
|
|
120
|
+
'console.warn',
|
|
121
|
+
'console.info',
|
|
122
|
+
'console.debug',
|
|
123
|
+
'console.trace',
|
|
124
|
+
'logger.log',
|
|
125
|
+
'logger.error',
|
|
126
|
+
'logger.warn',
|
|
127
|
+
'logger.info',
|
|
128
|
+
'logger.debug',
|
|
129
|
+
'this.logger',
|
|
130
|
+
'log',
|
|
131
|
+
'error',
|
|
132
|
+
'warn',
|
|
133
|
+
'info',
|
|
134
|
+
'debug',
|
|
135
|
+
'trace',
|
|
117
136
|
];
|
|
118
137
|
|
|
119
138
|
// === String patterns that are acceptable (not magic strings) ===
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy architecture-detection dist to engines/arch-detect
|
|
3
|
+
* This script is run during build to bundle architecture detection
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const SOURCE = path.resolve(__dirname, '../../../../architecture-detection/dist');
|
|
10
|
+
const DEST = path.resolve(__dirname, '../engines/arch-detect');
|
|
11
|
+
|
|
12
|
+
function copyDir(src, dest) {
|
|
13
|
+
if (!fs.existsSync(src)) {
|
|
14
|
+
console.log('⚠️ architecture-detection/dist not found. Run "npm run build" in architecture-detection first.');
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Clean destination
|
|
19
|
+
if (fs.existsSync(dest)) {
|
|
20
|
+
fs.rmSync(dest, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
23
|
+
|
|
24
|
+
// Copy files recursively
|
|
25
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const srcPath = path.join(src, entry.name);
|
|
29
|
+
const destPath = path.join(dest, entry.name);
|
|
30
|
+
|
|
31
|
+
// Skip source maps and declaration files
|
|
32
|
+
if (entry.name.endsWith('.map') || entry.name.endsWith('.d.ts')) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip CLI and LLM folders
|
|
37
|
+
if (entry.name === 'cli.js' || entry.name === 'llm' || entry.name === 'llm-primary') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
copyDir(srcPath, destPath);
|
|
43
|
+
} else {
|
|
44
|
+
fs.copyFileSync(srcPath, destPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('📦 Copying architecture-detection...');
|
|
52
|
+
|
|
53
|
+
if (copyDir(SOURCE, DEST)) {
|
|
54
|
+
const size = getTotalSize(DEST);
|
|
55
|
+
console.log(`✅ Copied to engines/arch-detect (${formatSize(size)})`);
|
|
56
|
+
} else {
|
|
57
|
+
console.log('⚠️ Skipped architecture-detection copy');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getTotalSize(dir) {
|
|
61
|
+
let size = 0;
|
|
62
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const fullPath = path.join(dir, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
size += getTotalSize(fullPath);
|
|
67
|
+
} else {
|
|
68
|
+
size += fs.statSync(fullPath).size;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return size;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatSize(bytes) {
|
|
75
|
+
if (bytes < 1024) return bytes + ' B';
|
|
76
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
77
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
78
|
+
}
|