121 lines
3.1 KiB
TypeScript
121 lines
3.1 KiB
TypeScript
/**
|
||
* Human-readable reporter output.
|
||
*
|
||
* Turns diagnostics, traces, and shrink results into plain text
|
||
* suitable for terminal reading.
|
||
*/
|
||
|
||
import type { Diagnostic } from './diagnostics.js';
|
||
import type { TraceEvent } from './traces.js';
|
||
import type { ShrinkResult } from './shrink.js';
|
||
|
||
/**
|
||
* Options for human formatting.
|
||
* Injected so callers control colors, verbosity, etc.
|
||
*/
|
||
export interface HumanFormatOptions {
|
||
// show trace events after each diagnostic
|
||
showTraces?: boolean;
|
||
// show shrink summary when available
|
||
showShrink?: boolean;
|
||
// max related facts to print
|
||
maxRelated?: number;
|
||
}
|
||
|
||
/**
|
||
* Render a list of diagnostics into a human-readable string.
|
||
*/
|
||
export function renderHumanReport(
|
||
diagnostics: Diagnostic[],
|
||
traces: readonly TraceEvent[],
|
||
shrinkResults: Map<string, ShrinkResult>,
|
||
opts: HumanFormatOptions = {},
|
||
): string {
|
||
const lines: string[] = [];
|
||
|
||
for (const d of diagnostics) {
|
||
lines.push(renderDiagnostic(d, opts));
|
||
|
||
if (opts.showShrink && d.clauseId && shrinkResults.has(d.clauseId)) {
|
||
const shrink = shrinkResults.get(d.clauseId)!;
|
||
lines.push(renderShrink(shrink));
|
||
}
|
||
|
||
if (opts.showTraces && d.traceRef) {
|
||
const relevant = traces.filter(
|
||
(t) => t.traceEventId === d.traceRef || t.refs.diagnosticId === d.diagnosticId,
|
||
);
|
||
if (relevant.length > 0) {
|
||
lines.push(' trace:');
|
||
for (const t of relevant) {
|
||
lines.push(` ${t.phase} at ${t.at}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
/**
|
||
* Render a single diagnostic in human form.
|
||
*/
|
||
export function renderDiagnostic(
|
||
d: Diagnostic,
|
||
opts: HumanFormatOptions = {},
|
||
): string {
|
||
const lines: string[] = [];
|
||
const prefix = d.severity === 'error' ? '✖' : d.severity === 'warning' ? '⚠' : 'ℹ';
|
||
lines.push(`${prefix} ${d.message}`);
|
||
lines.push(` ${d.code}`);
|
||
|
||
if (d.position) {
|
||
lines.push(
|
||
` at line ${d.position.start.line}, column ${d.position.start.column}`,
|
||
);
|
||
}
|
||
|
||
const maxRelated = opts.maxRelated ?? 5;
|
||
if (d.related.length > 0) {
|
||
lines.push(' related:');
|
||
for (const r of d.related.slice(0, maxRelated)) {
|
||
lines.push(` • ${r.message}`);
|
||
}
|
||
if (d.related.length > maxRelated) {
|
||
lines.push(` … and ${d.related.length - maxRelated} more`);
|
||
}
|
||
}
|
||
|
||
if (d.fixHints.length > 0) {
|
||
lines.push(' hints:');
|
||
for (const h of d.fixHints) {
|
||
lines.push(` → ${h}`);
|
||
}
|
||
}
|
||
|
||
if (d.suggestedFix) {
|
||
lines.push(' suggested fix:');
|
||
lines.push(` action: ${d.suggestedFix.action}`);
|
||
lines.push(` target: ${d.suggestedFix.target}`);
|
||
lines.push(` value: ${d.suggestedFix.value}`);
|
||
lines.push(` rationale: ${d.suggestedFix.rationale}`);
|
||
}
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
/**
|
||
* Render a shrink result summary.
|
||
*/
|
||
export function renderShrink(result: ShrinkResult): string {
|
||
const lines: string[] = [];
|
||
lines.push(' shrink:');
|
||
if (result.reduced) {
|
||
lines.push(` reduced across: ${result.axes.join(', ')}`);
|
||
lines.push(` steps: ${result.steps}`);
|
||
} else {
|
||
lines.push(' no reduction possible');
|
||
}
|
||
return lines.join('\n');
|
||
}
|