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