UNPKG

@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
/** * 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;