use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt::{self, Write as _};
use crate::keywords::{card_for, top_level_forms};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub(crate) line: usize,
pub(crate) col: usize,
pub(crate) width: usize,
pub(crate) message: String,
pub(crate) keyword: Option<&'static str>,
pub(crate) general: bool,
pub(crate) line_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostics {
pub(crate) file: Option<String>,
pub(crate) errors: Vec<Diagnostic>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Class {
Keyword(&'static str),
Statement,
Other,
}
impl Class {
fn of(d: &Diagnostic) -> Class {
match (d.keyword, d.general) {
(Some(kw), _) => Class::Keyword(kw),
(None, true) => Class::Statement,
(None, false) => Class::Other,
}
}
fn name(self) -> &'static str {
match self {
Class::Keyword(kw) => kw,
Class::Statement => "statement",
Class::Other => "other",
}
}
}
impl Diagnostics {
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn set_file(&mut self, file: &str) {
self.file = Some(String::from(file));
}
pub fn render(&self, max_classes: Option<usize>, max_per_class: Option<usize>) -> String {
let groups = self.group();
let total = self.errors.len();
let total_classes = groups.len();
let shown_classes = cap(max_classes, total_classes);
let noun = if total == 1 { "error" } else { "errors" };
let mut out = String::new();
match &self.file {
Some(f) => {
let _ = write!(out, "RESULT: {total} syntax {noun} in {f}");
}
None => {
let _ = write!(out, "RESULT: {total} syntax {noun}");
}
}
for (class, items) in groups.iter().take(shown_classes) {
out.push_str("\n\n");
render_class(&mut out, *class, items, max_per_class);
}
if shown_classes < total_classes {
let rest = total_classes - shown_classes;
let plural = if rest == 1 { "class" } else { "classes" };
let _ = write!(
out,
"\n\n... and {rest} more {plural} — pass --max-classes 0 for all"
);
}
out
}
fn group(&self) -> Vec<(Class, Vec<&Diagnostic>)> {
let mut groups: Vec<(Class, Vec<&Diagnostic>)> = Vec::new();
for d in &self.errors {
let class = Class::of(d);
match groups.iter_mut().find(|(c, _)| *c == class) {
Some((_, items)) => items.push(d),
None => groups.push((class, vec![d])),
}
}
groups
}
}
impl fmt::Display for Diagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.render(None, None))
}
}
fn cap(limit: Option<usize>, total: usize) -> usize {
match limit {
Some(n) if n > 0 && n < total => n,
_ => total,
}
}
const PLACE_GUTTER: &str = " | ";
fn render_class(
out: &mut String,
class: Class,
items: &[&Diagnostic],
max_per_class: Option<usize>,
) {
let name = class.name();
let n = items.len();
let problems = if n == 1 { "problem" } else { "problems" };
let _ = writeln!(out, "{name} ({n} {problems})");
match class {
Class::Keyword(kw) => {
if let Some(card) = card_for(kw) {
label(out, "syntax", card.form);
label(out, "example", card.example);
}
}
Class::Statement => {
out.push_str(" expected one of these statements:");
for k in top_level_forms() {
let _ = write!(out, "\n {}", k.card.form);
}
out.push('\n');
}
Class::Other => {}
}
let shown = cap(max_per_class, n);
for d in items.iter().take(shown) {
render_place(out, d);
}
if shown < n {
let rest = n - shown;
let p = if rest == 1 { "problem" } else { "problems" };
let _ = writeln!(out, " ... and {rest} more {name} {p}");
}
if out.ends_with('\n') {
out.pop();
}
}
fn render_place(out: &mut String, d: &Diagnostic) {
let _ = writeln!(out, " line {}, col {} - {}", d.line, d.col, d.message);
let _ = writeln!(out, "{PLACE_GUTTER}{}", d.line_text);
let pad = " ".repeat(d.col.saturating_sub(1));
let carets = "^".repeat(d.width.max(1));
let _ = writeln!(out, "{PLACE_GUTTER}{pad}{carets}");
}
fn label(out: &mut String, name: &str, value: &str) {
const VALUE_INDENT: &str = " ";
let mut lines = value.split('\n');
let first = lines.next().unwrap_or("");
let _ = writeln!(out, " {name:<7} : {first}");
for line in lines {
let _ = writeln!(out, "{VALUE_INDENT}{line}");
}
}