@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
215 lines (188 loc) • 7.38 kB
JavaScript
/**
* Scoring Service
* Calculate quality score based on violations, rules, and LOC
* Following Rule C005: Single responsibility - handle scoring operations
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
class ScoringService {
constructor() {
// Scoring weights based on violations per KLOC (1000 lines)
//
// Calibration targets:
// - 0 violations/KLOC = 100 (A+)
// - 1-2 violations/KLOC = 90-95 (A/A+)
// - 3-4 violations/KLOC = 80-89 (B/B+)
// - 5-7 violations/KLOC = 70-79 (C/C+)
// - 8-10 violations/KLOC = 60-69 (D)
// - >10 violations/KLOC = <60 (F)
//
this.weights = {
// Penalty per violation type (per KLOC)
// Errors are 3x more severe than warnings
errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
// Absolute penalty thresholds (regardless of LOC)
// Large projects should still be penalized for raw violation counts
absoluteErrorThreshold: 20, // Start penalizing if errors > 20
absoluteErrorPenalty: 0.05, // Each error above threshold reduces score by 0.05 (max 15 pts)
absoluteWarningThreshold: 100, // Start penalizing if warnings > 100
absoluteWarningPenalty: 0.01, // Each warning above threshold reduces score by 0.01 (max 10 pts)
};
// Thresholds for violations per KLOC (used for grading reference)
this.thresholds = {
excellent: 1, // < 1 violation per KLOC = excellent (A+/A)
good: 3, // < 3 violations per KLOC = good (B+/B)
acceptable: 5, // < 5 violations per KLOC = acceptable (C+/C)
poor: 10, // < 10 violations per KLOC = poor (D)
// >= 10 = very poor (F)
};
}
/**
* Calculate quality score
*
* New formula (v2):
* 1. Calculate violations per KLOC (errorsPerKLOC, warningsPerKLOC)
* 2. Apply penalty based on density: errorsPerKLOC * 2 + warningsPerKLOC * 0.5
* 3. Apply absolute penalty for projects with too many errors
* 4. Add small bonus for rules checked
*
* @param {Object} params
* @param {number} params.errorCount - Number of errors found
* @param {number} params.warningCount - Number of warnings found
* @param {number} params.rulesChecked - Number of rules checked
* @param {number} params.loc - Total lines of code
* @returns {number} Score between 0-100
*/
calculateScore({ errorCount, warningCount, rulesChecked, loc }) {
// Base score starts at 100
let score = 100;
// Calculate KLOC (thousands of lines of code)
const kloc = Math.max(loc / 1000, 1); // Minimum 1 KLOC to avoid division issues
// Calculate violations per KLOC
const errorsPerKLOC = errorCount / kloc;
const warningsPerKLOC = warningCount / kloc;
const totalViolationsPerKLOC = errorsPerKLOC + warningsPerKLOC;
// 1. Density-based penalty (main scoring factor)
// This penalizes based on how "dense" the violations are
const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC);
score -= densityPenalty;
// 2. Absolute penalty for projects with too many errors
// Even large codebases should not have hundreds of errors
if (errorCount > this.weights.absoluteErrorThreshold) {
const excessErrors = errorCount - this.weights.absoluteErrorThreshold;
const absoluteErrorPenalty = excessErrors * this.weights.absoluteErrorPenalty;
score -= Math.min(absoluteErrorPenalty, 15); // Cap at 15 points
}
// 3. Absolute penalty for projects with too many warnings
if (warningCount > this.weights.absoluteWarningThreshold) {
const excessWarnings = warningCount - this.weights.absoluteWarningThreshold;
const absoluteWarningPenalty = excessWarnings * this.weights.absoluteWarningPenalty;
score -= Math.min(absoluteWarningPenalty, 10); // Cap at 10 points
}
// Ensure score is between 0-100
score = Math.max(0, Math.min(100, score));
// Round to 1 decimal place
return Math.round(score * 10) / 10;
}
/**
* Calculate Lines of Code (LOC) for given files
* @param {string[]} files - Array of file paths
* @returns {number} Total lines of code
*/
calculateLOC(files) {
let totalLines = 0;
for (const file of files) {
try {
if (fs.existsSync(file)) {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n').length;
totalLines += lines;
}
} catch (error) {
// Ignore files that can't be read
console.warn(`Warning: Could not read file ${file}`);
}
}
return totalLines;
}
/**
* Calculate LOC for a directory
* @param {string} directory - Directory path
* @param {string[]} extensions - File extensions to include (e.g., ['.ts', '.tsx', '.js', '.jsx'])
* @returns {number} Total lines of code
*/
calculateDirectoryLOC(directory, extensions = ['.ts', '.tsx', '.js', '.jsx']) {
let totalLines = 0;
const processDirectory = (dir) => {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip common directories
if (!['node_modules', '.git', 'dist', 'build', 'coverage'].includes(entry.name)) {
processDirectory(fullPath);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (extensions.includes(ext)) {
try {
const content = fs.readFileSync(fullPath, 'utf8');
totalLines += content.split('\n').length;
} catch (error) {
// Ignore files that can't be read
}
}
}
}
} catch (error) {
// Ignore directories that can't be read
}
};
if (fs.existsSync(directory)) {
processDirectory(directory);
}
return totalLines;
}
/**
* Get score grade based on score value
* @param {number} score - Score value (0-100)
* @returns {string} Grade (A+, A, B+, B, C+, C, D, F)
*/
getGrade(score) {
if (score >= 95) return 'A+';
if (score >= 90) return 'A';
if (score >= 85) return 'B+';
if (score >= 80) return 'B';
if (score >= 75) return 'C+';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
/**
* Generate scoring summary
* @param {Object} params
* @returns {Object} Scoring summary with score, grade, and metrics
*/
generateScoringSummary(params) {
const score = this.calculateScore(params);
const grade = this.getGrade(score);
return {
score,
grade,
metrics: {
errors: params.errorCount,
warnings: params.warningCount,
rulesChecked: params.rulesChecked,
linesOfCode: params.loc,
violationsPerKLOC: params.loc > 0
? Math.round(((params.errorCount + params.warningCount) / params.loc * 1000) * 10) / 10
: 0
}
};
}
}
module.exports = ScoringService;