UNPKG

@sun-asterisk/sunlint

Version:

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

321 lines (270 loc) 9.89 kB
const chalk = require('chalk'); const { table } = require('table'); const path = require('path'); class ReportGenerator { constructor(config, options) { this.config = config; this.options = options; } async generateReport(results, metadata) { const report = { metadata: { ...metadata, timestamp: new Date().toISOString(), tool: '☀️ Sun Lint', company: 'Sun*', format: this.options?.format || 'eslint' }, raw: results, formatted: '', summary: '' }; // Generate formatted output based on format switch (this.options?.format) { case 'eslint': report.formatted = this.generateESLintFormat(results); break; case 'json': report.formatted = JSON.stringify(results, null, 2); break; case 'summary': report.formatted = this.generateSummaryFormat(results); break; case 'table': report.formatted = this.generateTableFormat(results); break; default: report.formatted = this.generateESLintFormat(results); } // Generate summary report.summary = this.generateSummary(results, metadata); return report; } generateESLintFormat(results) { const violations = this.flattenViolations(results); if (violations.length === 0) { return chalk.green('✨ No coding standard violations found!'); } const groupedByFile = this.groupViolationsByFile(violations); let output = ''; for (const [file, fileViolations] of Object.entries(groupedByFile)) { const relativePath = path.relative(process.cwd(), file); output += `\n${chalk.underline(relativePath)}\n`; fileViolations.forEach(violation => { const severityColor = this.getSeverityColor(violation.severity); const line = violation.line || 1; const column = violation.column || 1; output += ` ${line}:${column} `; output += `${severityColor(this.getSeveritySymbol(violation.severity))} `; output += `${violation.message} `; output += `${chalk.gray(violation.ruleId)}\n`; }); } // Add summary line const errorCount = violations.filter(v => v.severity === 'error').length; const warningCount = violations.filter(v => v.severity === 'warning').length; const infoCount = violations.filter(v => v.severity === 'info').length; output += '\n'; output += chalk.red(`✖ ${violations.length} problems `); output += `(${errorCount} errors, ${warningCount} warnings, ${infoCount} infos)\n`; return output; } generateSummaryFormat(results) { const violations = this.flattenViolations(results); let output = ''; // Header output += chalk.blue.bold('🔍 Coding Standards Analysis Report\n'); output += chalk.gray('═'.repeat(50)) + '\n\n'; // Overview output += chalk.white.bold('📊 Overview:\n'); output += ` Files analyzed: ${results.filesAnalyzed || 0}\n`; output += ` Rules executed: ${results.rulesRun || 0}\n`; output += ` Total violations: ${violations.length}\n\n`; // Violations by severity const bySeverity = results.violationsBySeverity || {}; output += chalk.white.bold('🚨 Violations by Severity:\n'); output += ` ${chalk.red('Errors')}: ${bySeverity.error || 0}\n`; output += ` ${chalk.yellow('Warnings')}: ${bySeverity.warning || 0}\n`; output += ` ${chalk.blue('Info')}: ${bySeverity.info || 0}\n\n`; // Top violated rules if (results.violationsByRule && Object.keys(results.violationsByRule).length > 0) { output += chalk.white.bold('📏 Most Violated Rules:\n'); const sortedRules = Object.entries(results.violationsByRule) .sort(([,a], [,b]) => b - a) .slice(0, 5); sortedRules.forEach(([ruleId, count]) => { output += ` ${ruleId}: ${count} violations\n`; }); output += '\n'; } // Top problematic files if (results.violationsByFile && Object.keys(results.violationsByFile).length > 0) { output += chalk.white.bold('📁 Most Problematic Files:\n'); const sortedFiles = Object.entries(results.violationsByFile) .sort(([,a], [,b]) => b - a) .slice(0, 5); sortedFiles.forEach(([file, count]) => { const relativePath = path.relative(process.cwd(), file); output += ` ${relativePath}: ${count} violations\n`; }); output += '\n'; } // Recent violations (first 10) if (violations.length > 0) { output += chalk.white.bold('🔍 Recent Violations:\n'); violations.slice(0, 10).forEach(violation => { const relativePath = path.relative(process.cwd(), violation.file || ''); const severityColor = this.getSeverityColor(violation.severity); output += ` ${severityColor(violation.severity.toUpperCase())}: ${violation.message}\n`; output += ` ${chalk.gray(`at ${relativePath}:${violation.line || 1}:${violation.column || 1} (${violation.ruleId})`)}\n`; }); } return output; } generateTableFormat(results) { const violations = this.flattenViolations(results); if (violations.length === 0) { return chalk.green('✨ No coding standard violations found!'); } // Check if this is an integrated analysis with source information const hasSourceInfo = violations.some(v => v.source); const tableData = hasSourceInfo ? [['File', 'Line', 'Severity', 'Source', 'Rule', 'Message']] : [['File', 'Line', 'Severity', 'Rule', 'Message']]; violations.forEach(violation => { const relativePath = path.relative(process.cwd(), violation.file || ''); const row = [ relativePath, (violation.line || 1).toString(), violation.severity, ]; if (hasSourceInfo) { // Add source information with color coding const sourceDisplay = violation.source === 'sunlint' ? chalk.yellow('SunLint') : chalk.cyan('ESLint'); row.push(sourceDisplay); } row.push( violation.ruleId || 'unknown', violation.message.substring(0, 50) + (violation.message.length > 50 ? '...' : '') ); // Add conflict info if available if (violation.additionalInfo) { row[row.length - 1] += chalk.gray(` (ESLint: ${violation.additionalInfo.eslintRule})`); } tableData.push(row); }); const config = { border: { topBody: '─', topJoin: '┬', topLeft: '┌', topRight: '┐', bottomBody: '─', bottomJoin: '┴', bottomLeft: '└', bottomRight: '┘', bodyLeft: '│', bodyRight: '│', bodyJoin: '│', joinBody: '─', joinLeft: '├', joinRight: '┤', joinJoin: '┼' } }; return table(tableData, config); } generateSummary(results, metadata) { const violations = this.flattenViolations(results); const errorCount = violations.filter(v => v.severity === 'error').length; const warningCount = violations.filter(v => v.severity === 'warning').length; const infoCount = violations.filter(v => v.severity === 'info').length; let summary = ''; summary += `Analysis completed in ${metadata.duration}ms\n`; summary += `Files: ${results.filesAnalyzed || 0} | Rules: ${metadata.rulesRun} | Total: ${violations.length}\n`; if (errorCount > 0) { summary += chalk.red(`Errors: ${errorCount} `); } if (warningCount > 0) { summary += chalk.yellow(`Warnings: ${warningCount} `); } if (infoCount > 0) { summary += chalk.blue(`Info: ${infoCount} `); } return summary.trim(); } flattenViolations(results) { const violations = []; if (results.results) { results.results.forEach(result => { // Handle SunLint format (violations array) if (result.violations) { result.violations.forEach(violation => { violations.push({ ...violation, ruleId: violation.ruleId || result.ruleId, severity: violation.severity || result.severity || 'warning' }); }); } // Handle ESLint format (messages array) if (result.messages) { result.messages.forEach(message => { violations.push({ file: result.filePath || message.file, ruleId: message.ruleId, severity: message.severity === 2 ? 'error' : 'warning', message: message.message, line: message.line, column: message.column, source: message.source || 'eslint' }); }); } }); } return violations; } groupViolationsByFile(violations) { const grouped = {}; violations.forEach(violation => { const file = violation.file || violation.filePath || 'unknown'; if (!grouped[file]) { grouped[file] = []; } grouped[file].push(violation); }); // Sort violations within each file by line number Object.keys(grouped).forEach(file => { grouped[file].sort((a, b) => (a.line || 1) - (b.line || 1)); }); return grouped; } getSeverityColor(severity) { switch (severity) { case 'error': return chalk.red; case 'warning': return chalk.yellow; case 'info': return chalk.blue; default: return chalk.gray; } } getSeveritySymbol(severity) { switch (severity) { case 'error': return '✖'; case 'warning': return '⚠'; case 'info': return 'ℹ'; default: return '•'; } } } module.exports = ReportGenerator;