UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

170 lines (147 loc) 5.09 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 this.weights = { errorPenalty: 5, // Each error reduces score by 5 points warningPenalty: 1, // Each warning reduces score by 1 point ruleBonus: 0.5, // Bonus for each rule checked locFactor: 0.001 // Factor for normalizing by LOC }; } /** * Calculate quality score * Score formula: 100 - (errorCount * 5 + warningCount * 1) * (1000 / LOC) + (rulesChecked * 0.5) * * @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 violations penalty const totalPenalty = (errorCount * this.weights.errorPenalty) + (warningCount * this.weights.warningPenalty); // Normalize penalty by LOC (per 1000 lines) const locNormalization = loc > 0 ? 1000 / loc : 1; const normalizedPenalty = totalPenalty * locNormalization; // Apply penalty score -= normalizedPenalty; // Add bonus for rules checked (more rules = more thorough check) const ruleBonus = Math.min(rulesChecked * this.weights.ruleBonus, 10); // Max 10 points bonus score += ruleBonus; // 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;