@felixgeelhaar/cclint
Version:
Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.
170 lines • 5.97 kB
JavaScript
import { LintingResult } from '../../domain/LintingResult.js';
import { Severity } from '../../domain/Severity.js';
import { formatSarifResult } from './sarifFormatter.js';
const RULE_GROUP_THRESHOLD = 4;
export function formatResult(result, format = 'text', options = {}) {
if (format === 'json') {
return formatJsonResult(result);
}
if (format === 'sarif') {
return formatSarifResult(result);
}
return formatTextResult(result, options);
}
function shouldUseEmoji(plainOpt) {
if (plainOpt === true)
return false;
if (process.env['NO_EMOJI'] === '1' || process.env['NO_COLOR'] === '1') {
return false;
}
if (process.env['CI'] === 'true')
return false;
return Boolean(process.stdout.isTTY);
}
function formatTextResult(result, options) {
const lines = [];
const emoji = shouldUseEmoji(options.plain);
if (!result.hasViolations()) {
lines.push(emoji
? `✅ No issues found in ${result.file.path}`
: `[OK] No issues found in ${result.file.path}`);
return lines.join('\n');
}
lines.push(emoji
? `📝 Linting results for ${result.file.path}:`
: `Linting results for ${result.file.path}:`);
lines.push('');
if (options.summary) {
lines.push(...formatSummaryGroups(result, emoji));
}
else {
lines.push(...formatViolationList(result, emoji));
}
lines.push('');
lines.push(formatSummaryLine(result, emoji));
const footer = formatFooter(result, options);
if (footer) {
lines.push(footer);
}
return lines.join('\n');
}
function formatViolationList(result, emoji) {
const out = [];
const ruleCounts = countByRule(result);
// Track which rules we've already collapsed
const collapsedRules = new Set();
for (const violation of result.violations) {
const count = ruleCounts.get(violation.ruleId) ?? 0;
// Collapse rules firing ≥THRESHOLD times to first item + summary
if (count >= RULE_GROUP_THRESHOLD) {
if (collapsedRules.has(violation.ruleId)) {
continue;
}
collapsedRules.add(violation.ruleId);
const prefix = getSeverityPrefix(violation.severity, emoji);
out.push(`${prefix} ${violation.toString()}`);
out.push(` … and ${count - 1} more from rule [${violation.ruleId}]. Run \`cclint lint <file> --summary\` for grouped view.`);
}
else {
const prefix = getSeverityPrefix(violation.severity, emoji);
out.push(`${prefix} ${violation.toString()}`);
}
}
return out;
}
function formatSummaryGroups(result, emoji) {
const out = [];
const groups = new Map();
for (const violation of result.violations) {
const existing = groups.get(violation.ruleId);
if (existing) {
existing.count += 1;
}
else {
groups.set(violation.ruleId, {
severity: violation.severity,
count: 1,
firstLine: violation.location.line,
});
}
}
// Sort: errors first, warnings, then info; within severity by descending count
const sorted = [...groups.entries()].sort((a, b) => {
const sevDiff = b[1].severity.compareTo(a[1].severity);
if (sevDiff !== 0)
return sevDiff;
return b[1].count - a[1].count;
});
for (const [ruleId, info] of sorted) {
const prefix = getSeverityPrefix(info.severity, emoji);
const plural = info.count === 1 ? '' : 's';
out.push(`${prefix} ${ruleId} (${info.count} occurrence${plural}, first at line ${info.firstLine})`);
}
return out;
}
function countByRule(result) {
const counts = new Map();
for (const v of result.violations) {
counts.set(v.ruleId, (counts.get(v.ruleId) ?? 0) + 1);
}
return counts;
}
function formatSummaryLine(result, emoji) {
const errors = result.getErrorCount();
const warnings = result.getWarningCount();
const infos = result.getInfoCount();
const parts = [];
if (errors > 0) {
parts.push(emoji
? `❌ ${errors} error${errors === 1 ? '' : 's'}`
: `${errors} error${errors === 1 ? '' : 's'}`);
}
if (warnings > 0) {
parts.push(emoji
? `⚠️ ${warnings} warning${warnings === 1 ? '' : 's'}`
: `${warnings} warning${warnings === 1 ? '' : 's'}`);
}
if (infos > 0) {
parts.push(emoji ? `ℹ️ ${infos} info` : `${infos} info`);
}
return `Summary: ${parts.length > 0 ? parts.join(', ') : '0 issues'}`;
}
function formatFooter(result, options) {
const fixable = options.fixableCount ?? 0;
if (fixable > 0) {
return `→ ${fixable} of ${result.violations.length} ${fixable === 1 ? 'issue is' : 'issues are'} auto-fixable. Run with --fix.`;
}
return null;
}
function formatJsonResult(result) {
const jsonResult = {
file: result.file.path,
violations: result.violations.map(violation => ({
ruleId: violation.ruleId,
message: violation.message,
severity: violation.severity.name,
location: {
line: violation.location.line,
column: violation.location.column,
},
})),
summary: {
errors: result.getErrorCount(),
warnings: result.getWarningCount(),
infos: result.getInfoCount(),
},
};
return JSON.stringify(jsonResult, null, 2);
}
function getSeverityPrefix(severity, emoji) {
if (severity === Severity.ERROR) {
return emoji ? '❌ [ERROR]' : '[ERROR]';
}
else if (severity === Severity.WARNING) {
return emoji ? '⚠️ [WARN]' : '[WARN]';
}
else {
return emoji ? 'ℹ️ [INFO]' : '[INFO]';
}
}
//# sourceMappingURL=textFormatter.js.map