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