UNPKG

@sun-asterisk/sunlint

Version:

ā˜€ļø SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

346 lines (303 loc) • 11.4 kB
/** * Summary Report Service * Generate summary reports for CI/CD and management dashboards * Following Rule C005: Single responsibility - handle summary report generation */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); class SummaryReportService { constructor() { // Load version from package.json this.version = this._loadVersion(); } /** * Load version from package.json * @returns {string} Package version * @private */ _loadVersion() { try { const packageJsonPath = path.join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); return packageJson.version || '1.3.16'; } catch (error) { return '1.3.16'; // Fallback version } } /** * Get Git repository information * @param {string} cwd - Working directory * @returns {Object} Git info including repository URL, branch, commit hash, and commit details */ getGitInfo(cwd = process.cwd()) { const gitInfo = { repository_url: null, repository_name: null, project_path: null, branch: null, commit_hash: null, commit_message: null, author_email: null, author_name: null, pr_number: null }; try { // Check if it's a git repository execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' }); // Get repository URL (prefer origin) try { const remoteUrl = execSync('git config --get remote.origin.url', { cwd, encoding: 'utf8' }).trim(); // Convert SSH URL to HTTPS URL if needed if (remoteUrl.startsWith('git@')) { gitInfo.repository_url = remoteUrl .replace('git@', 'https://') .replace('.com:', '.com/') .replace('.git', ''); } else { gitInfo.repository_url = remoteUrl.replace('.git', ''); } // Extract repository name from URL const urlMatch = gitInfo.repository_url.match(/\/([^\/]+)$/); if (urlMatch) { gitInfo.repository_name = urlMatch[1]; } } catch (error) { // No remote configured } // Get current branch try { gitInfo.branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf8' }).trim(); } catch (error) { // Can't get branch } // Get current commit hash try { gitInfo.commit_hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim(); } catch (error) { // Can't get commit hash } // Get commit message try { gitInfo.commit_message = execSync('git log -1 --pretty=%B', { cwd, encoding: 'utf8' }).trim(); } catch (error) { // Can't get commit message } // Get author email try { gitInfo.author_email = execSync('git log -1 --pretty=%ae', { cwd, encoding: 'utf8' }).trim(); } catch (error) { // Can't get author email } // Get author name try { gitInfo.author_name = execSync('git log -1 --pretty=%an', { cwd, encoding: 'utf8' }).trim(); } catch (error) { // Can't get author name } // Get project path (relative to repo root for mono-repo) try { const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim(); if (cwd !== repoRoot) { gitInfo.project_path = path.relative(repoRoot, cwd); } } catch (error) { // Can't determine project path } // Try to extract PR number from commit message or branch name try { // Check commit message for PR patterns like "#123" or "pull request #123" const commitMsg = gitInfo.commit_message || ''; const prMatch = commitMsg.match(/#(\d+)/); if (prMatch) { gitInfo.pr_number = parseInt(prMatch[1]); } else { // Check branch name for PR patterns const branchMatch = gitInfo.branch.match(/(?:pr|pull)[/-]?(\d+)/i); if (branchMatch) { gitInfo.pr_number = parseInt(branchMatch[1]); } } } catch (error) { // Can't extract PR number } } catch (error) { // Not a git repository - return nulls } return gitInfo; } /** * Generate summary report from violations * @param {Array} violations - Array of violations * @param {Object} scoringSummary - Scoring summary with score and metrics * @param {Object} options - Additional options * @returns {Object} Summary report in JSON format */ generateSummaryReport(violations, scoringSummary, options = {}) { // Get Git information const gitInfo = this.getGitInfo(options.cwd); // Override with environment variables if available (from CI/CD) const repository_url = process.env.GITHUB_REPOSITORY ? `https://github.com/${process.env.GITHUB_REPOSITORY}` : gitInfo.repository_url; const repository_name = process.env.GITHUB_REPOSITORY ? process.env.GITHUB_REPOSITORY.split('/')[1] : (gitInfo.repository_name || null); const branch = process.env.GITHUB_REF_NAME || gitInfo.branch; const commit_hash = process.env.GITHUB_SHA || gitInfo.commit_hash; // Get commit details from GitHub context or git const commit_message = process.env.GITHUB_EVENT_HEAD_COMMIT_MESSAGE || (process.env.GITHUB_EVENT_PATH ? this._getGitHubEventData('head_commit.message') : null) || gitInfo.commit_message; const author_email = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_EMAIL || (process.env.GITHUB_EVENT_PATH ? this._getGitHubEventData('head_commit.author.email') : null) || gitInfo.author_email; const author_name = process.env.GITHUB_EVENT_HEAD_COMMIT_AUTHOR_NAME || (process.env.GITHUB_EVENT_PATH ? this._getGitHubEventData('head_commit.author.name') : null) || gitInfo.author_name; // Get PR number from GitHub event or git let pr_number = null; if (process.env.GITHUB_EVENT_PATH) { pr_number = this._getGitHubEventData('pull_request.number') || this._getGitHubEventData('number'); } pr_number = pr_number || gitInfo.pr_number; // Get project path (for mono-repo support) const project_path = gitInfo.project_path; // Count violations by rule const violationsByRule = {}; violations.forEach(violation => { const ruleId = violation.ruleId || 'unknown'; if (!violationsByRule[ruleId]) { violationsByRule[ruleId] = { rule_code: ruleId, count: 0, severity: violation.severity || 'warning' }; } violationsByRule[ruleId].count++; }); // Convert to array and sort by count (descending) const violationsSummary = Object.values(violationsByRule) .sort((a, b) => b.count - a.count); // Build the summary report compatible with coding-standards-report API const summaryReport = { // API-compatible fields (flat structure) repository_url, repository_name, project_path, branch, commit_hash, commit_message, author_email, author_name, pr_number, score: scoringSummary.score, total_violations: violations.length, error_count: scoringSummary.metrics.errors, warning_count: scoringSummary.metrics.warnings, info_count: 0, // Reserved for future use lines_of_code: scoringSummary.metrics.linesOfCode, files_analyzed: options.filesAnalyzed || 0, sunlint_version: options.version || this.version, analysis_duration_ms: options.duration || 0, violations: violationsSummary, // Additional metadata for backwards compatibility metadata: { generated_at: new Date().toISOString(), tool: 'SunLint', version: options.version || this.version, analysis_duration_ms: options.duration || 0 }, quality: { score: scoringSummary.score, grade: scoringSummary.grade, metrics: scoringSummary.metrics } }; return summaryReport; } /** * Helper method to extract data from GitHub event JSON * @param {string} path - Dot-notation path to the data (e.g., 'head_commit.message') * @returns {any} Extracted value or null * @private */ _getGitHubEventData(path) { try { const eventPath = process.env.GITHUB_EVENT_PATH; if (!eventPath || !fs.existsSync(eventPath)) { return null; } const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8')); const keys = path.split('.'); let value = eventData; for (const key of keys) { if (value && typeof value === 'object' && key in value) { value = value[key]; } else { return null; } } return value; } catch (error) { return null; } } /** * Generate summary report and save to file * @param {Array} violations - Array of violations * @param {Object} scoringSummary - Scoring summary * @param {string} outputPath - Output file path * @param {Object} options - Additional options */ saveSummaryReport(violations, scoringSummary, outputPath, options = {}) { const summaryReport = this.generateSummaryReport(violations, scoringSummary, options); // Ensure directory exists const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write to file with pretty formatting fs.writeFileSync(outputPath, JSON.stringify(summaryReport, null, 2), 'utf8'); return summaryReport; } /** * Generate a simple text summary for console display * @param {Object} summaryReport - Summary report object * @returns {string} Formatted text summary */ formatTextSummary(summaryReport) { // Extract data from flat structure const score = summaryReport.score; const grade = summaryReport.quality?.grade || 'N/A'; const filesAnalyzed = summaryReport.files_analyzed || 0; const linesOfCode = summaryReport.lines_of_code || 0; const totalViolations = summaryReport.total_violations || 0; const errorCount = summaryReport.error_count || 0; const warningCount = summaryReport.warning_count || 0; const violationsByRule = summaryReport.violations || []; const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0; let output = '\nšŸ“Š Quality Summary Report\n'; output += '━'.repeat(50) + '\n'; output += `šŸ“ˆ Quality Score: ${score} (Grade: ${grade})\n`; output += `šŸ“ Files Analyzed: ${filesAnalyzed}\n`; output += `šŸ“ Lines of Code: ${linesOfCode.toLocaleString()}\n`; output += `āš ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`; output += `šŸ“Š Violations per KLOC: ${violationsPerKLOC}\n`; if (violationsByRule.length > 0) { output += '\nšŸ” Top Violations by Rule:\n'; violationsByRule.slice(0, 10).forEach((item, index) => { output += ` ${index + 1}. ${item.rule_code}: ${item.count} violations (${item.severity})\n`; }); } return output; } } module.exports = SummaryReportService;