1use alloc::string::String;
14use alloc::vec;
15use alloc::vec::Vec;
16use core::fmt::{self, Write as _};
17
18use crate::keywords::{card_for, top_level_forms};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Diagnostic {
24 pub(crate) line: usize,
26 pub(crate) col: usize,
28 pub(crate) width: usize,
30 pub(crate) message: String,
32 pub(crate) keyword: Option<&'static str>,
35 pub(crate) general: bool,
38 pub(crate) line_text: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Diagnostics {
45 pub(crate) file: Option<String>,
48 pub(crate) errors: Vec<Diagnostic>,
50}
51
52#[derive(Clone, Copy, PartialEq, Eq)]
55enum Class {
56 Keyword(&'static str),
58 Statement,
60 Other,
62}
63
64impl Class {
65 fn of(d: &Diagnostic) -> Class {
67 match (d.keyword, d.general) {
68 (Some(kw), _) => Class::Keyword(kw),
69 (None, true) => Class::Statement,
70 (None, false) => Class::Other,
71 }
72 }
73
74 fn name(self) -> &'static str {
76 match self {
77 Class::Keyword(kw) => kw,
78 Class::Statement => "statement",
79 Class::Other => "other",
80 }
81 }
82}
83
84impl Diagnostics {
85 pub fn len(&self) -> usize {
87 self.errors.len()
88 }
89
90 pub fn is_empty(&self) -> bool {
93 self.errors.is_empty()
94 }
95
96 pub fn set_file(&mut self, file: &str) {
99 self.file = Some(String::from(file));
100 }
101
102 pub fn render(&self, max_classes: Option<usize>, max_per_class: Option<usize>) -> String {
110 let groups = self.group();
111 let total = self.errors.len();
112 let total_classes = groups.len();
113 let shown_classes = cap(max_classes, total_classes);
114
115 let noun = if total == 1 { "error" } else { "errors" };
116 let mut out = String::new();
117 match &self.file {
118 Some(f) => {
119 let _ = write!(out, "RESULT: {total} syntax {noun} in {f}");
120 }
121 None => {
122 let _ = write!(out, "RESULT: {total} syntax {noun}");
123 }
124 }
125
126 for (class, items) in groups.iter().take(shown_classes) {
127 out.push_str("\n\n");
128 render_class(&mut out, *class, items, max_per_class);
129 }
130
131 if shown_classes < total_classes {
132 let rest = total_classes - shown_classes;
133 let plural = if rest == 1 { "class" } else { "classes" };
134 let _ = write!(
135 out,
136 "\n\n... and {rest} more {plural} — pass --max-classes 0 for all"
137 );
138 }
139 out
140 }
141
142 fn group(&self) -> Vec<(Class, Vec<&Diagnostic>)> {
145 let mut groups: Vec<(Class, Vec<&Diagnostic>)> = Vec::new();
146 for d in &self.errors {
147 let class = Class::of(d);
148 match groups.iter_mut().find(|(c, _)| *c == class) {
149 Some((_, items)) => items.push(d),
150 None => groups.push((class, vec![d])),
151 }
152 }
153 groups
154 }
155}
156
157impl fmt::Display for Diagnostics {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.write_str(&self.render(None, None))
160 }
161}
162
163fn cap(limit: Option<usize>, total: usize) -> usize {
165 match limit {
166 Some(n) if n > 0 && n < total => n,
167 _ => total,
168 }
169}
170
171const PLACE_GUTTER: &str = " | ";
173
174fn render_class(
176 out: &mut String,
177 class: Class,
178 items: &[&Diagnostic],
179 max_per_class: Option<usize>,
180) {
181 let name = class.name();
182 let n = items.len();
183 let problems = if n == 1 { "problem" } else { "problems" };
184 let _ = writeln!(out, "{name} ({n} {problems})");
185
186 match class {
188 Class::Keyword(kw) => {
189 if let Some(card) = card_for(kw) {
190 label(out, "syntax", card.form);
191 label(out, "example", card.example);
192 }
193 }
194 Class::Statement => {
195 out.push_str(" expected one of these statements:");
196 for k in top_level_forms() {
197 let _ = write!(out, "\n {}", k.card.form);
198 }
199 out.push('\n');
200 }
201 Class::Other => {}
203 }
204
205 let shown = cap(max_per_class, n);
206 for d in items.iter().take(shown) {
207 render_place(out, d);
208 }
209 if shown < n {
210 let rest = n - shown;
211 let p = if rest == 1 { "problem" } else { "problems" };
212 let _ = writeln!(out, " ... and {rest} more {name} {p}");
213 }
214
215 if out.ends_with('\n') {
217 out.pop();
218 }
219}
220
221fn render_place(out: &mut String, d: &Diagnostic) {
223 let _ = writeln!(out, " line {}, col {} - {}", d.line, d.col, d.message);
224 let _ = writeln!(out, "{PLACE_GUTTER}{}", d.line_text);
225 let pad = " ".repeat(d.col.saturating_sub(1));
226 let carets = "^".repeat(d.width.max(1));
227 let _ = writeln!(out, "{PLACE_GUTTER}{pad}{carets}");
228}
229
230fn label(out: &mut String, name: &str, value: &str) {
233 const VALUE_INDENT: &str = " ";
234 let mut lines = value.split('\n');
235 let first = lines.next().unwrap_or("");
236 let _ = writeln!(out, " {name:<7} : {first}");
237 for line in lines {
238 let _ = writeln!(out, "{VALUE_INDENT}{line}");
239 }
240}