## Error reporting and formatting for Validatrix.
##
## Provides consistent error formatting across all language validators,
## including human-readable and machine-readable (JSON) output.
import std/[strformat, json, tables]
import ../core/types
# ---------------------------------------------------------------------------
# Error formatter
# ---------------------------------------------------------------------------
type
ErrorFormatter* = object
## Formats validation errors consistently.
includeSourceContext*: bool
includeColorHint*: bool
maxErrors*: int
proc newErrorFormatter*(
srcCtx: bool = true,
colorHint: bool = false,
maxErr: int = 0
): ErrorFormatter =
## Create a new error formatter.
result.includeSourceContext = srcCtx
result.includeColorHint = colorHint
result.maxErrors = maxErr
# ---------------------------------------------------------------------------
# Format single error as human-readable string
# ---------------------------------------------------------------------------
proc formatError*(self: ErrorFormatter, err: ValidationError, source: string = ""): string =
## Format a single validation error as a human-readable string.
let posStr = &"L{err.position.line}:{err.position.column}"
let codeStr = &"[{err.code}]"
let sevStr = &"({$err.severity})"
let msgStr = err.message
result = &" {codeStr} {sevStr} {posStr}: {msgStr}"
if self.includeSourceContext and source.len > 0 and err.position.line > 0:
let lines = source.splitLines()
let lineIdx = err.position.line - 1
if lineIdx >= 0 and lineIdx < lines.len:
let sourceLine = lines[lineIdx]
result.add("\n" & " | " & sourceLine)
if err.position.column > 0:
let caretCol = max(err.position.column - 1, 0)
let caret = " ".repeat(caretCol) & "^"
result.add("\n" & " | " & caret)
if self.includeColorHint and err.hint.len > 0:
result.add("\n" & " hint: " & err.hint)
# ---------------------------------------------------------------------------
# Format all errors
# ---------------------------------------------------------------------------
proc formatAllErrors*(self: ErrorFormatter, errors: seq[ValidationError], source: string = ""): string =
## Format all errors as a human-readable multi-line string.
if errors.len == 0:
return " No errors found."
var parts: seq[string] = @[]
let count = if self.maxErrors > 0: min(errors.len, self.maxErrors) else: errors.len
for i in 0..<count:
parts.add(self.formatError(errors[i], source))
result = parts.join("\n")
if self.maxErrors > 0 and errors.len > self.maxErrors:
result.add(&"\n ... and {errors.len - self.maxErrors} more errors (use maxErrors=0 to show all)")
# ---------------------------------------------------------------------------
# Format validation result as human-readable report
# ---------------------------------------------------------------------------
proc formatReport*(vr: ValidationResult, source: string = ""): string =
## Format the complete validation result as a human-readable report.
let formatter = newErrorFormatter(srcCtx = true, colorHint = true)
var lines: seq[string] = @[]
lines.add(&"=== Validation Report ===")
lines.add(&" Flavor: {$vr.flavor}")
lines.add(&" Detection: {$vr.detectionMethod}")
lines.add(&" Valid: {vr.valid}")
lines.add(&" Errors: {vr.errors.len}")
lines.add(&" Warnings: {vr.warnings}")
lines.add(&" Duration: {vr.durationMs:.2f}ms")
lines.add(&" Lines of code: {vr.moduleInfo.linesOfCode}")
lines.add(&" Tokens: {vr.moduleInfo.totalTokens}")
lines.add("")
if vr.errors.len > 0:
lines.add(" --- Findings ---")
for err in vr.errors:
lines.add(formatter.formatError(err, source))
else:
lines.add(" No issues found.")
lines.add("")
lines.add(" --- Summary ---")
if vr.valid:
lines.add(" VALID - source passed all checks.")
else:
lines.add(" INVALID - source has issues that need attention.")
if vr.debugOutput.len > 0:
lines.add("")
lines.add(" --- Debug Output ---")
for line in vr.debugOutput.splitLines():
lines.add(" " & line)
result = lines.join("\n")
# ---------------------------------------------------------------------------
# JSON formatting (comprehensive)
# ---------------------------------------------------------------------------
proc toJsonReport*(vr: ValidationResult): JsonNode =
## Full validation result as JSON (same as result.toJson()).
vr.toJson()
proc toJsonReportString*(vr: ValidationResult, pretty: bool = true): string =
## Validation result as JSON string.
let j = vr.toJson()
if pretty:
result = j.pretty()
else:
result = $j
# ---------------------------------------------------------------------------
# Error counting / categorization
# ---------------------------------------------------------------------------
proc countBySeverity*(errors: seq[ValidationError]): Table[string, int] =
## Count errors by severity.
result = initTable[string, int]()
for err in errors:
let key = $err.severity
result.mgetOrPut(key, 0).inc
proc countByCode*(errors: seq[ValidationError]): Table[string, int] =
## Count errors by error code.
result = initTable[string, int]()
for err in errors:
result.mgetOrPut(err.code, 0).inc
proc filterBySeverity*(errors: seq[ValidationError], minSeverity: ErrorSeverity): seq[ValidationError] =
## Filter errors by minimum severity level.
let severities: array[4, ErrorSeverity] = [esInfo, esWarning, esError, esCritical]
var minIdx = 0
for i, s in severities:
if s == minSeverity:
minIdx = i
break
for err in errors:
var errIdx = 0
for i, s in severities:
if s == err.severity:
errIdx = i
break
if errIdx >= minIdx:
result.add(err)
# ---------------------------------------------------------------------------
# Error severity ordering for filtering
# ---------------------------------------------------------------------------
proc `>`*(a, b: ErrorSeverity): bool =
## Compare severities: critical > error > warning > info.
const order: array[4, int] = [0, 1, 2, 3]
order[ord(a)] > order[ord(b)]
proc `<`*(a, b: ErrorSeverity): bool =
const order: array[4, int] = [0, 1, 2, 3]
order[ord(a)] < order[ord(b)]
proc `<=`*(a, b: ErrorSeverity): bool =
a < b or a == b
proc `>=`*(a, b: ErrorSeverity): bool =
a > b or a == b