@sun-asterisk/sunlint 1.3.9 → 1.3.11
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/CHANGELOG.md +91 -1
- package/README.md +62 -0
- package/config/rules/enhanced-rules-registry.json +18 -0
- package/core/cli-action-handler.js +30 -1
- package/core/cli-program.js +5 -1
- package/core/output-service.js +160 -3
- package/core/scoring-service.js +169 -0
- package/core/semantic-engine.js +4 -2
- package/core/summary-report-service.js +189 -0
- package/core/upload-service.js +282 -0
- package/docs/QUALITY_SCORING_GUIDE.md +397 -0
- package/package.json +1 -1
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +116 -10
- package/rules/common/C060_no_override_superclass/analyzer.js +180 -0
- package/rules/common/C060_no_override_superclass/config.json +50 -0
- package/rules/common/C060_no_override_superclass/symbol-based-analyzer.js +220 -0
- package/rules/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## � **v1.4.0 - Quality Scoring & Summary Reports (October 7, 2025)**
|
|
6
|
+
|
|
7
|
+
**Release Date**: October 7, 2025
|
|
8
|
+
**Type**: Major Feature Release
|
|
9
|
+
**Branch**: `fix.sunlint.cli_options`
|
|
10
|
+
|
|
11
|
+
### ✨ **Major Features**
|
|
12
|
+
|
|
13
|
+
#### **Quality Scoring System** 🎯
|
|
14
|
+
- **NEW**: Automated quality score calculation (0-100) based on violations, LOC, and rules checked
|
|
15
|
+
- **NEW**: Grade system (A+ to F) for easy quality interpretation
|
|
16
|
+
- **Formula**: `Score = 100 - (errors × 5 + warnings × 1) × (1000 / LOC) + (rules × 0.5)`
|
|
17
|
+
- **Normalization**: Violations normalized per 1000 lines of code (KLOC)
|
|
18
|
+
- **Bonus System**: Up to 10 points bonus for comprehensive rule coverage
|
|
19
|
+
|
|
20
|
+
#### **Summary Report Generation** 📊
|
|
21
|
+
- **NEW**: `--output-summary <file>` option for JSON summary reports
|
|
22
|
+
- **Format**: CI/CD-ready JSON with quality score, violations, and metrics
|
|
23
|
+
- **Git Integration**: Auto-detects repository URL, branch, and commit hash
|
|
24
|
+
- **Environment Variables**: Supports GitHub Actions env vars (GITHUB_REPOSITORY, GITHUB_SHA, etc.)
|
|
25
|
+
- **Violation Summary**: Aggregated violations by rule with count and severity
|
|
26
|
+
- **Metrics**: LOC, files analyzed, violations per KLOC, errors/warnings breakdown
|
|
27
|
+
|
|
28
|
+
#### **New Services** 🛠️
|
|
29
|
+
- **ScoringService**: Calculate quality scores with customizable weights
|
|
30
|
+
- Error penalty: -5 points
|
|
31
|
+
- Warning penalty: -1 point
|
|
32
|
+
- Rule bonus: +0.5 points (max 10)
|
|
33
|
+
- LOC normalization factor: 0.001
|
|
34
|
+
- **SummaryReportService**: Generate and save summary reports
|
|
35
|
+
- Git information extraction
|
|
36
|
+
- Violation aggregation by rule
|
|
37
|
+
- Text summary formatting for console
|
|
38
|
+
|
|
39
|
+
### 🔧 **CLI Enhancements**
|
|
40
|
+
- **FIXED**: Default semantic analysis now enabled for heuristic engine
|
|
41
|
+
- Removed default `'0'` from `--max-semantic-files` option
|
|
42
|
+
- ts-morph now works by default without explicit `--max-semantic-files=-1`
|
|
43
|
+
- **ENHANCED**: Display message now shows "TypeScript/JavaScript files (TS/TSX/JS/JSX)"
|
|
44
|
+
- Previously only showed "TS/JS files" causing confusion about TSX support
|
|
45
|
+
- **IMPROVED**: Semantic engine disabled message now more informative
|
|
46
|
+
- Clarifies when analysis is explicitly disabled vs. using default
|
|
47
|
+
|
|
48
|
+
### 📝 **Documentation**
|
|
49
|
+
- **NEW**: `docs/QUALITY_SCORING_GUIDE.md` - Comprehensive scoring guide
|
|
50
|
+
- Scoring formula explanation
|
|
51
|
+
- Grade scale interpretation
|
|
52
|
+
- CI/CD integration examples (GitHub Actions, GitLab CI)
|
|
53
|
+
- Dashboard integration patterns
|
|
54
|
+
- Quality gate setup
|
|
55
|
+
- Trending analysis examples
|
|
56
|
+
- **NEW**: `examples/github-actions-quality-check.yml` - GitHub Actions workflow template
|
|
57
|
+
- **UPDATED**: README.md with Quality Scoring section
|
|
58
|
+
|
|
59
|
+
### 🎯 **Use Cases**
|
|
60
|
+
- **CI/CD Integration**: Automated quality gates in pipelines
|
|
61
|
+
- **Management Dashboards**: Summary reports for project quality tracking
|
|
62
|
+
- **Quality Trending**: Track quality metrics over time
|
|
63
|
+
- **PR Comments**: Automated quality feedback on pull requests
|
|
64
|
+
- **Slack Notifications**: Quality score alerts to team channels
|
|
65
|
+
|
|
66
|
+
### 📊 **Example Output**
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"quality": {
|
|
70
|
+
"score": 92.6,
|
|
71
|
+
"grade": "A",
|
|
72
|
+
"metrics": {
|
|
73
|
+
"errors": 0,
|
|
74
|
+
"warnings": 39,
|
|
75
|
+
"linesOfCode": 4954,
|
|
76
|
+
"violationsPerKLOC": 7.9
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"violations": {
|
|
80
|
+
"total": 39,
|
|
81
|
+
"by_rule": [
|
|
82
|
+
{ "rule_code": "C065", "count": 39, "severity": "warning" }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 🔗 **Related Issues**
|
|
89
|
+
- Fixed semantic engine requiring explicit CLI options for ts-morph
|
|
90
|
+
- Enhanced TSX file visibility in console output
|
|
91
|
+
- Improved default configuration for heuristic engine
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## �🔧 **v1.3.9 - File Targeting Regression Fix (October 2, 2025)**
|
|
6
96
|
|
|
7
97
|
**Release Date**: October 2, 2025
|
|
8
98
|
**Type**: Bug Fix
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Sun Lint is a universal coding standards checker providing comprehensive code qu
|
|
|
17
17
|
- ✅ **Zero Config**: Works immediately after `npm install`
|
|
18
18
|
- ✅ **CI/CD Ready**: Baseline comparison, fail-on-new-violations, timeout protection
|
|
19
19
|
- ✅ **Advanced File Targeting**: Include/exclude patterns, language filtering
|
|
20
|
+
- ✅ **Quality Scoring System**: Automated quality score (0-100) with grade (A+ to F)
|
|
21
|
+
- ✅ **Summary Reports**: JSON format for CI/CD dashboards and management reports
|
|
20
22
|
|
|
21
23
|
### **🏗️ Architecture**
|
|
22
24
|
|
|
@@ -64,8 +66,68 @@ sunlint --rules=C010,C006 --eslint-integration --input=src
|
|
|
64
66
|
|
|
65
67
|
# Git integration
|
|
66
68
|
sunlint --all --changed-files
|
|
69
|
+
|
|
70
|
+
# Quality scoring and summary report (for CI/CD)
|
|
71
|
+
sunlint --all --input=src --output-summary=quality-report.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 📊 **Quality Scoring & Summary Reports** 🆕
|
|
75
|
+
|
|
76
|
+
Generate comprehensive quality reports with automated scoring for CI/CD integration and management dashboards.
|
|
77
|
+
|
|
78
|
+
### **Features**
|
|
79
|
+
- **Quality Score (0-100)**: Calculated based on violations, LOC, and rules checked
|
|
80
|
+
- **Grade System**: A+ to F grade for easy interpretation
|
|
81
|
+
- **JSON Format**: Ready for CI/CD pipelines and dashboard integration
|
|
82
|
+
- **Git Integration**: Automatically includes repository info, branch, and commit hash
|
|
83
|
+
- **Metrics**: Violations per KLOC, errors/warnings count, LOC analysis
|
|
84
|
+
|
|
85
|
+
### **Quick Usage**
|
|
86
|
+
```bash
|
|
87
|
+
# Generate summary report
|
|
88
|
+
sunlint --all --input=src --output-summary=quality.json
|
|
89
|
+
|
|
90
|
+
# Both detailed and summary reports
|
|
91
|
+
sunlint --all --input=src --output=detailed.txt --output-summary=quality.json
|
|
92
|
+
|
|
93
|
+
# In GitHub Actions (auto-detects git info)
|
|
94
|
+
sunlint --all --input=src --output-summary=quality.json
|
|
67
95
|
```
|
|
68
96
|
|
|
97
|
+
### **Example Output**
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"quality": {
|
|
101
|
+
"score": 92.6,
|
|
102
|
+
"grade": "A",
|
|
103
|
+
"metrics": {
|
|
104
|
+
"errors": 0,
|
|
105
|
+
"warnings": 39,
|
|
106
|
+
"linesOfCode": 4954,
|
|
107
|
+
"violationsPerKLOC": 7.9
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"violations": {
|
|
111
|
+
"total": 39,
|
|
112
|
+
"by_rule": [
|
|
113
|
+
{ "rule_code": "C065", "count": 39 }
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"repository": {
|
|
117
|
+
"repository_url": "https://github.com/org/repo",
|
|
118
|
+
"branch": "main",
|
|
119
|
+
"commit_hash": "abc123"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**📚 Full Documentation**: See [docs/QUALITY_SCORING_GUIDE.md](docs/QUALITY_SCORING_GUIDE.md) for:
|
|
125
|
+
- Scoring formula and interpretation
|
|
126
|
+
- CI/CD integration examples (GitHub Actions, GitLab CI)
|
|
127
|
+
- Dashboard integration
|
|
128
|
+
- Quality gate setup
|
|
129
|
+
- Trending analysis
|
|
130
|
+
|
|
69
131
|
## 📦 **Installation**
|
|
70
132
|
|
|
71
133
|
### **Global Installation (Recommended)**
|
|
@@ -375,6 +375,24 @@
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
},
|
|
378
|
+
"C060": {
|
|
379
|
+
"name": "Do not override superclass methods and ignore critical logic",
|
|
380
|
+
"description": "Preserve important behavior or lifecycle logic defined in the superclass to ensure correctness and prevent silent errors.",
|
|
381
|
+
"category": "logging",
|
|
382
|
+
"severity": "warning",
|
|
383
|
+
"languages": ["typescript", "javascript", "dart"],
|
|
384
|
+
"analyzer": "./rules/common/C060_no_override_superclass/analyzer.js",
|
|
385
|
+
"version": "1.0.0",
|
|
386
|
+
"status": "stable",
|
|
387
|
+
"tags": ["logging", "production", "debugging", "console"],
|
|
388
|
+
"strategy": {
|
|
389
|
+
"preferred": "regex",
|
|
390
|
+
"fallbacks": ["regex"],
|
|
391
|
+
"accuracy": {
|
|
392
|
+
"regex": 90
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
378
396
|
"S001": {
|
|
379
397
|
"name": "Fail Securely",
|
|
380
398
|
"description": "Verify that if there is an error in access control, the system fails securely",
|
|
@@ -85,7 +85,8 @@ class CliActionHandler {
|
|
|
85
85
|
// Output results
|
|
86
86
|
await this.outputService.outputResults(results, this.options, {
|
|
87
87
|
duration,
|
|
88
|
-
rulesRun: rulesToRun.length
|
|
88
|
+
rulesRun: rulesToRun.length,
|
|
89
|
+
rulesChecked: rulesToRun.length
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
// Exit with appropriate code
|
|
@@ -295,6 +296,34 @@ class CliActionHandler {
|
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// Validate upload-report option - requires output-summary to be set
|
|
300
|
+
if (this.options.uploadReport !== undefined) {
|
|
301
|
+
if (!this.options.outputSummary) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
chalk.red(`❌ --upload-report requires --output-summary to be specified\n`) +
|
|
304
|
+
chalk.gray('Example: sunlint --all --output-summary=report.json --upload-report --input=src')
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Set default URL if no URL provided (when uploadReport is true)
|
|
309
|
+
if (typeof this.options.uploadReport === 'boolean' || !this.options.uploadReport) {
|
|
310
|
+
this.options.uploadReport = 'https://coding-standards-report.sun-asterisk.vn/api/reports';
|
|
311
|
+
if (this.options.verbose) {
|
|
312
|
+
console.log(chalk.gray(`ℹ️ Using default upload URL: ${this.options.uploadReport}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Basic URL validation
|
|
317
|
+
try {
|
|
318
|
+
new URL(this.options.uploadReport);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
chalk.red(`❌ Invalid upload URL format: ${this.options.uploadReport}\n`) +
|
|
322
|
+
chalk.gray('Please provide a valid HTTP/HTTPS URL')
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
298
327
|
// Priority 1: CLI --input parameter (highest priority)
|
|
299
328
|
if (this.options.input) {
|
|
300
329
|
// Validate CLI input path exists
|
package/core/cli-program.js
CHANGED
|
@@ -34,6 +34,8 @@ function createCliProgram() {
|
|
|
34
34
|
.option('-i, --input <path>', 'Input file or directory to analyze (REQUIRED)')
|
|
35
35
|
.option('-f, --format <format>', 'Output format (eslint,json,summary,table)', 'eslint')
|
|
36
36
|
.option('-o, --output <file>', 'Output file path')
|
|
37
|
+
.option('--output-summary <file>', 'Output summary report file path (JSON format for CI/CD)')
|
|
38
|
+
.option('--upload-report [url]', 'Upload summary report to API endpoint after analysis (default: Sun* Coding Standards API)')
|
|
37
39
|
.option('--config <file>', 'Configuration file path (default: auto-discover)');
|
|
38
40
|
|
|
39
41
|
// File targeting options
|
|
@@ -71,7 +73,7 @@ function createCliProgram() {
|
|
|
71
73
|
.option('--debug', 'Enable debug mode')
|
|
72
74
|
.option('--ai', 'Enable AI-powered analysis')
|
|
73
75
|
.option('--no-ai', 'Force disable AI analysis')
|
|
74
|
-
.option('--max-semantic-files <number>', 'Symbol table file limit for TypeScript analysis (default:
|
|
76
|
+
.option('--max-semantic-files <number>', 'Symbol table file limit for TypeScript analysis (default: 1000, -1 for unlimited)')
|
|
75
77
|
.option('--list-engines', 'List available analysis engines');
|
|
76
78
|
|
|
77
79
|
// ESLint Integration options
|
|
@@ -129,6 +131,8 @@ CI/CD Integration:
|
|
|
129
131
|
$ sunlint --all --changed-files --diff-base=origin/main --fail-on-new-violations
|
|
130
132
|
$ sunlint --all --staged-files --format=summary
|
|
131
133
|
$ sunlint --all --pr-mode --diff-base=origin/main
|
|
134
|
+
$ sunlint --all --output-summary=report.json --upload-report
|
|
135
|
+
$ sunlint --all --output-summary=report.json --upload-report=https://custom-api.com/reports
|
|
132
136
|
|
|
133
137
|
ESLint Integration:
|
|
134
138
|
$ sunlint --typescript --eslint-integration --input=src
|
package/core/output-service.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Output Service
|
|
3
|
-
* Following Rule C005: Single responsibility - handle output operations
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const fs = require('fs');
|
|
7
6
|
const path = require('path');
|
|
8
7
|
const chalk = require('chalk');
|
|
8
|
+
const ScoringService = require('./scoring-service');
|
|
9
|
+
const SummaryReportService = require('./summary-report-service');
|
|
10
|
+
const UploadService = require('./upload-service');
|
|
9
11
|
|
|
10
12
|
class OutputService {
|
|
11
|
-
constructor() {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.scoringService = new ScoringService();
|
|
15
|
+
this.summaryReportService = new SummaryReportService();
|
|
16
|
+
this.uploadService = new UploadService();
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
async outputResults(results, options, metadata = {}) {
|
|
14
20
|
// Generate report based on format
|
|
@@ -27,12 +33,91 @@ class OutputService {
|
|
|
27
33
|
console.log(chalk.green(`📄 Report saved to: ${options.output}`));
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
// Summary report output (new feature for CI/CD)
|
|
37
|
+
if (options.outputSummary) {
|
|
38
|
+
const summaryReport = this.generateAndSaveSummaryReport(
|
|
39
|
+
report.violations,
|
|
40
|
+
results,
|
|
41
|
+
options,
|
|
42
|
+
metadata
|
|
43
|
+
);
|
|
44
|
+
console.log(chalk.green(`📊 Summary report saved to: ${options.outputSummary}`));
|
|
45
|
+
|
|
46
|
+
// Display summary in console
|
|
47
|
+
if (!options.quiet) {
|
|
48
|
+
console.log(this.summaryReportService.formatTextSummary(summaryReport));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Upload report if upload URL is provided
|
|
52
|
+
if (options.uploadReport) {
|
|
53
|
+
await this.handleUploadReport(options.outputSummary, options.uploadReport, options);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
// Summary (skip for JSON format)
|
|
31
58
|
if (!options.quiet && options.format !== 'json') {
|
|
32
59
|
console.log(report.summary);
|
|
33
60
|
}
|
|
34
61
|
}
|
|
35
62
|
|
|
63
|
+
generateAndSaveSummaryReport(violations, results, options, metadata) {
|
|
64
|
+
const totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
|
|
65
|
+
|
|
66
|
+
// Calculate LOC
|
|
67
|
+
let loc = 0;
|
|
68
|
+
if (options.input) {
|
|
69
|
+
const inputPaths = Array.isArray(options.input) ? options.input : [options.input];
|
|
70
|
+
for (const inputPath of inputPaths) {
|
|
71
|
+
if (fs.existsSync(inputPath)) {
|
|
72
|
+
const stat = fs.statSync(inputPath);
|
|
73
|
+
if (stat.isDirectory()) {
|
|
74
|
+
loc += this.scoringService.calculateDirectoryLOC(inputPath);
|
|
75
|
+
} else {
|
|
76
|
+
loc += this.scoringService.calculateLOC([inputPath]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Count violations
|
|
83
|
+
const errorCount = violations.filter(v => v.severity === 'error').length;
|
|
84
|
+
const warningCount = violations.filter(v => v.severity === 'warning').length;
|
|
85
|
+
|
|
86
|
+
// Get number of rules checked - use metadata first, then parse from options
|
|
87
|
+
let rulesChecked = metadata.rulesChecked;
|
|
88
|
+
if (!rulesChecked && options.rule) {
|
|
89
|
+
rulesChecked = typeof options.rule === 'string'
|
|
90
|
+
? options.rule.split(',').filter(r => r.trim()).length
|
|
91
|
+
: Array.isArray(options.rule)
|
|
92
|
+
? options.rule.length
|
|
93
|
+
: 1;
|
|
94
|
+
}
|
|
95
|
+
rulesChecked = rulesChecked || 1;
|
|
96
|
+
|
|
97
|
+
// Calculate score
|
|
98
|
+
const scoringSummary = this.scoringService.generateScoringSummary({
|
|
99
|
+
errorCount,
|
|
100
|
+
warningCount,
|
|
101
|
+
rulesChecked,
|
|
102
|
+
loc
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Generate and save summary report
|
|
106
|
+
const summaryReport = this.summaryReportService.saveSummaryReport(
|
|
107
|
+
violations,
|
|
108
|
+
scoringSummary,
|
|
109
|
+
options.outputSummary,
|
|
110
|
+
{
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
filesAnalyzed: totalFiles,
|
|
113
|
+
duration: metadata.duration,
|
|
114
|
+
version: metadata.version || '1.3.9'
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return summaryReport;
|
|
119
|
+
}
|
|
120
|
+
|
|
36
121
|
generateReport(results, metadata, options = {}) {
|
|
37
122
|
const allViolations = [];
|
|
38
123
|
let totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
|
|
@@ -98,7 +183,8 @@ class OutputService {
|
|
|
98
183
|
return {
|
|
99
184
|
formatted,
|
|
100
185
|
summary,
|
|
101
|
-
raw
|
|
186
|
+
raw,
|
|
187
|
+
violations: allViolations // Add violations for summary report
|
|
102
188
|
};
|
|
103
189
|
}
|
|
104
190
|
|
|
@@ -244,6 +330,77 @@ class OutputService {
|
|
|
244
330
|
|
|
245
331
|
return jsonResults;
|
|
246
332
|
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handle uploading report to API endpoint
|
|
336
|
+
*/
|
|
337
|
+
async handleUploadReport(filePath, apiUrl, options = {}) {
|
|
338
|
+
try {
|
|
339
|
+
if (!filePath) {
|
|
340
|
+
throw new Error('Summary report file path is required for upload');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!apiUrl) {
|
|
344
|
+
throw new Error('API URL is required for upload');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check if curl is available
|
|
348
|
+
if (!this.uploadService.checkCurlAvailability()) {
|
|
349
|
+
console.warn(chalk.yellow('⚠️ curl is not available. Skipping report upload.'));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Validate API endpoint if not in quiet mode
|
|
354
|
+
if (!options.quiet) {
|
|
355
|
+
console.log(chalk.blue('🔍 Checking API endpoint accessibility...'));
|
|
356
|
+
const endpointCheck = await this.uploadService.validateApiEndpoint(apiUrl, {
|
|
357
|
+
timeout: options.uploadTimeout || 10
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!endpointCheck.accessible) {
|
|
361
|
+
console.warn(chalk.yellow(`⚠️ API endpoint may not be accessible: ${endpointCheck.error}`));
|
|
362
|
+
console.warn(chalk.yellow('Proceeding with upload attempt...'));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Upload the report
|
|
367
|
+
const uploadResult = await this.uploadService.uploadReportToApi(filePath, apiUrl, {
|
|
368
|
+
timeout: options.uploadTimeout || 30,
|
|
369
|
+
verbose: options.verbose,
|
|
370
|
+
debug: options.debug
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (uploadResult.success) {
|
|
374
|
+
if (!options.quiet) {
|
|
375
|
+
console.log(chalk.green(`✅ Report successfully uploaded to: ${apiUrl}`));
|
|
376
|
+
if (uploadResult.statusCode) {
|
|
377
|
+
console.log(chalk.green(`📡 HTTP Status: ${uploadResult.statusCode}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
console.warn(chalk.yellow(`⚠️ Failed to upload report: ${uploadResult.error}`));
|
|
382
|
+
|
|
383
|
+
if (options.verbose && uploadResult.errorContext) {
|
|
384
|
+
console.warn('Upload error details:', uploadResult.errorContext);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return uploadResult;
|
|
389
|
+
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error(chalk.red('❌ Upload process failed:'), error.message);
|
|
392
|
+
|
|
393
|
+
if (options.debug) {
|
|
394
|
+
console.error('Upload error stack:', error.stack);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Don't throw error to prevent interrupting the main analysis flow
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
error: error.message
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
247
404
|
}
|
|
248
405
|
|
|
249
406
|
module.exports = OutputService;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring Service
|
|
3
|
+
* Calculate quality score based on violations, rules, and LOC
|
|
4
|
+
* Following Rule C005: Single responsibility - handle scoring operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
class ScoringService {
|
|
12
|
+
constructor() {
|
|
13
|
+
// Scoring weights
|
|
14
|
+
this.weights = {
|
|
15
|
+
errorPenalty: 5, // Each error reduces score by 5 points
|
|
16
|
+
warningPenalty: 1, // Each warning reduces score by 1 point
|
|
17
|
+
ruleBonus: 0.5, // Bonus for each rule checked
|
|
18
|
+
locFactor: 0.001 // Factor for normalizing by LOC
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate quality score
|
|
24
|
+
* Score formula: 100 - (errorCount * 5 + warningCount * 1) * (1000 / LOC) + (rulesChecked * 0.5)
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} params
|
|
27
|
+
* @param {number} params.errorCount - Number of errors found
|
|
28
|
+
* @param {number} params.warningCount - Number of warnings found
|
|
29
|
+
* @param {number} params.rulesChecked - Number of rules checked
|
|
30
|
+
* @param {number} params.loc - Total lines of code
|
|
31
|
+
* @returns {number} Score between 0-100
|
|
32
|
+
*/
|
|
33
|
+
calculateScore({ errorCount, warningCount, rulesChecked, loc }) {
|
|
34
|
+
// Base score starts at 100
|
|
35
|
+
let score = 100;
|
|
36
|
+
|
|
37
|
+
// Calculate violations penalty
|
|
38
|
+
const totalPenalty = (errorCount * this.weights.errorPenalty) +
|
|
39
|
+
(warningCount * this.weights.warningPenalty);
|
|
40
|
+
|
|
41
|
+
// Normalize penalty by LOC (per 1000 lines)
|
|
42
|
+
const locNormalization = loc > 0 ? 1000 / loc : 1;
|
|
43
|
+
const normalizedPenalty = totalPenalty * locNormalization;
|
|
44
|
+
|
|
45
|
+
// Apply penalty
|
|
46
|
+
score -= normalizedPenalty;
|
|
47
|
+
|
|
48
|
+
// Add bonus for rules checked (more rules = more thorough check)
|
|
49
|
+
const ruleBonus = Math.min(rulesChecked * this.weights.ruleBonus, 10); // Max 10 points bonus
|
|
50
|
+
score += ruleBonus;
|
|
51
|
+
|
|
52
|
+
// Ensure score is between 0-100
|
|
53
|
+
score = Math.max(0, Math.min(100, score));
|
|
54
|
+
|
|
55
|
+
// Round to 1 decimal place
|
|
56
|
+
return Math.round(score * 10) / 10;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate Lines of Code (LOC) for given files
|
|
61
|
+
* @param {string[]} files - Array of file paths
|
|
62
|
+
* @returns {number} Total lines of code
|
|
63
|
+
*/
|
|
64
|
+
calculateLOC(files) {
|
|
65
|
+
let totalLines = 0;
|
|
66
|
+
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(file)) {
|
|
70
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
71
|
+
const lines = content.split('\n').length;
|
|
72
|
+
totalLines += lines;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Ignore files that can't be read
|
|
76
|
+
console.warn(`Warning: Could not read file ${file}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return totalLines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Calculate LOC for a directory
|
|
85
|
+
* @param {string} directory - Directory path
|
|
86
|
+
* @param {string[]} extensions - File extensions to include (e.g., ['.ts', '.tsx', '.js', '.jsx'])
|
|
87
|
+
* @returns {number} Total lines of code
|
|
88
|
+
*/
|
|
89
|
+
calculateDirectoryLOC(directory, extensions = ['.ts', '.tsx', '.js', '.jsx']) {
|
|
90
|
+
let totalLines = 0;
|
|
91
|
+
|
|
92
|
+
const processDirectory = (dir) => {
|
|
93
|
+
try {
|
|
94
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const fullPath = path.join(dir, entry.name);
|
|
98
|
+
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
// Skip common directories
|
|
101
|
+
if (!['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
|
|
102
|
+
processDirectory(fullPath);
|
|
103
|
+
}
|
|
104
|
+
} else if (entry.isFile()) {
|
|
105
|
+
const ext = path.extname(entry.name);
|
|
106
|
+
if (extensions.includes(ext)) {
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
109
|
+
totalLines += content.split('\n').length;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Ignore files that can't be read
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Ignore directories that can't be read
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(directory)) {
|
|
122
|
+
processDirectory(directory);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return totalLines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get score grade based on score value
|
|
130
|
+
* @param {number} score - Score value (0-100)
|
|
131
|
+
* @returns {string} Grade (A+, A, B+, B, C+, C, D, F)
|
|
132
|
+
*/
|
|
133
|
+
getGrade(score) {
|
|
134
|
+
if (score >= 95) return 'A+';
|
|
135
|
+
if (score >= 90) return 'A';
|
|
136
|
+
if (score >= 85) return 'B+';
|
|
137
|
+
if (score >= 80) return 'B';
|
|
138
|
+
if (score >= 75) return 'C+';
|
|
139
|
+
if (score >= 70) return 'C';
|
|
140
|
+
if (score >= 60) return 'D';
|
|
141
|
+
return 'F';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate scoring summary
|
|
146
|
+
* @param {Object} params
|
|
147
|
+
* @returns {Object} Scoring summary with score, grade, and metrics
|
|
148
|
+
*/
|
|
149
|
+
generateScoringSummary(params) {
|
|
150
|
+
const score = this.calculateScore(params);
|
|
151
|
+
const grade = this.getGrade(score);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
score,
|
|
155
|
+
grade,
|
|
156
|
+
metrics: {
|
|
157
|
+
errors: params.errorCount,
|
|
158
|
+
warnings: params.warningCount,
|
|
159
|
+
rulesChecked: params.rulesChecked,
|
|
160
|
+
linesOfCode: params.loc,
|
|
161
|
+
violationsPerKLOC: params.loc > 0
|
|
162
|
+
? Math.round(((params.errorCount + params.warningCount) / params.loc * 1000) * 10) / 10
|
|
163
|
+
: 0
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = ScoringService;
|
package/core/semantic-engine.js
CHANGED
|
@@ -102,7 +102,7 @@ class SemanticEngine {
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
if (targetFiles) {
|
|
105
|
-
console.log(`🎯 Targeted files received: ${targetFiles.length} total, ${semanticFiles.length} TS/JS
|
|
105
|
+
console.log(`🎯 Targeted files received: ${targetFiles.length} total, ${semanticFiles.length} TypeScript/JavaScript files (TS/TSX/JS/JSX)`);
|
|
106
106
|
if (semanticFiles.length < 10) {
|
|
107
107
|
console.log(` Files: ${semanticFiles.map(f => path.basename(f)).join(', ')}`);
|
|
108
108
|
}
|
|
@@ -119,7 +119,9 @@ class SemanticEngine {
|
|
|
119
119
|
} else if (userMaxFiles === 0) {
|
|
120
120
|
// Disable semantic analysis
|
|
121
121
|
maxFiles = 0;
|
|
122
|
-
console.log(`🔧 Semantic Engine config: DISABLED semantic analysis (heuristic only)`);
|
|
122
|
+
console.log(`🔧 Semantic Engine config: DISABLED semantic analysis (heuristic only mode)`);
|
|
123
|
+
console.log(` 💡 Semantic analysis explicitly disabled with --max-semantic-files=0`);
|
|
124
|
+
console.log(` 💡 To enable: omit the option (default: 1000) or use --max-semantic-files=1000 (or higher)`);
|
|
123
125
|
} else if (userMaxFiles > 0) {
|
|
124
126
|
// User-specified limit
|
|
125
127
|
maxFiles = Math.min(userMaxFiles, semanticFiles.length);
|