1use crate::span::{LineCol, SourceFile, Span};
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Severity {
10 Error,
11 Warning,
12}
13
14#[derive(Debug, Clone, Serialize)]
17pub struct Label {
18 pub span: Span,
19 pub message: String,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct Diagnostic {
24 pub severity: Severity,
25 pub code: &'static str,
27 pub message: String,
28 pub labels: Vec<Label>,
29 pub help: Option<String>,
30 pub notes: Vec<String>,
31}
32
33impl Diagnostic {
34 pub fn error(code: &'static str, message: impl Into<String>) -> Self {
35 Diagnostic {
36 severity: Severity::Error,
37 code,
38 message: message.into(),
39 labels: Vec::new(),
40 help: None,
41 notes: Vec::new(),
42 }
43 }
44
45 pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
46 Diagnostic {
47 severity: Severity::Warning,
48 ..Self::error(code, message)
49 }
50 }
51
52 pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
53 self.labels.push(Label {
54 span,
55 message: message.into(),
56 });
57 self
58 }
59
60 pub fn with_help(mut self, help: impl Into<String>) -> Self {
61 self.help = Some(help.into());
62 self
63 }
64
65 pub fn with_note(mut self, note: impl Into<String>) -> Self {
66 self.notes.push(note.into());
67 self
68 }
69
70 pub fn primary_span(&self) -> Option<Span> {
71 self.labels.first().map(|l| l.span)
72 }
73}
74
75#[derive(Debug, Serialize)]
77pub struct ResolvedDiagnostic<'a> {
78 pub severity: Severity,
79 pub code: &'static str,
80 pub message: &'a str,
81 pub file: &'a str,
82 pub labels: Vec<ResolvedLabel<'a>>,
83 pub help: Option<&'a str>,
84 pub notes: &'a [String],
85}
86
87#[derive(Debug, Serialize)]
88pub struct ResolvedLabel<'a> {
89 pub message: &'a str,
90 pub span: Span,
91 pub start: LineCol,
92 pub end: LineCol,
93 pub text: &'a str,
95}
96
97pub fn resolve<'a>(d: &'a Diagnostic, src: &'a SourceFile) -> ResolvedDiagnostic<'a> {
98 ResolvedDiagnostic {
99 severity: d.severity,
100 code: d.code,
101 message: &d.message,
102 file: &src.name,
103 labels: d
104 .labels
105 .iter()
106 .map(|l| ResolvedLabel {
107 message: &l.message,
108 span: l.span,
109 start: src.line_col(l.span.start),
110 end: src.line_col(l.span.end),
111 text: src.snippet(l.span),
112 })
113 .collect(),
114 help: d.help.as_deref(),
115 notes: &d.notes,
116 }
117}
118
119pub struct Styles {
122 bold: &'static str,
123 red: &'static str,
124 yellow: &'static str,
125 blue: &'static str,
126 green: &'static str,
127 reset: &'static str,
128}
129
130impl Styles {
131 pub fn colored() -> Self {
132 Styles {
133 bold: "\x1b[1m",
134 red: "\x1b[1;31m",
135 yellow: "\x1b[1;33m",
136 blue: "\x1b[1;34m",
137 green: "\x1b[1;32m",
138 reset: "\x1b[0m",
139 }
140 }
141
142 pub fn plain() -> Self {
143 Styles {
144 bold: "",
145 red: "",
146 yellow: "",
147 blue: "",
148 green: "",
149 reset: "",
150 }
151 }
152}
153
154pub fn render(d: &Diagnostic, src: &SourceFile, st: &Styles) -> String {
166 let mut out = String::new();
167 let (sev_color, sev_name) = match d.severity {
168 Severity::Error => (st.red, "error"),
169 Severity::Warning => (st.yellow, "warning"),
170 };
171 out.push_str(&format!(
172 "{sev_color}{sev_name}[{code}]{reset}{bold}: {msg}{reset}\n",
173 code = d.code,
174 msg = d.message,
175 sev_color = sev_color,
176 reset = st.reset,
177 bold = st.bold,
178 ));
179
180 if let Some(primary) = d.primary_span() {
181 let lc = src.line_col(primary.start);
182 let max_line = d
184 .labels
185 .iter()
186 .map(|l| src.line_col(l.span.start).line)
187 .max()
188 .unwrap_or(lc.line);
189 let gw = max_line.to_string().len();
190 out.push_str(&format!(
191 "{:gw$}{blue}-->{reset} {}:{}:{}\n",
192 "",
193 src.name,
194 lc.line,
195 lc.col,
196 gw = gw + 1,
197 blue = st.blue,
198 reset = st.reset,
199 ));
200 out.push_str(&format!(
201 "{:gw$} {blue}|{reset}\n",
202 "",
203 gw = gw,
204 blue = st.blue,
205 reset = st.reset
206 ));
207
208 for (i, label) in d.labels.iter().enumerate() {
209 let is_primary = i == 0;
210 let l_start = src.line_col(label.span.start);
211 let l_end = src.line_col(label.span.end);
212 let line_text = src.line_text(l_start.line);
213 out.push_str(&format!(
214 "{blue}{:>gw$} |{reset} {}\n",
215 l_start.line,
216 line_text,
217 gw = gw,
218 blue = st.blue,
219 reset = st.reset,
220 ));
221 let pad = l_start.col - 1;
223 let width = if l_end.line == l_start.line {
224 (l_end.col - l_start.col).max(1)
225 } else {
226 line_text.chars().count().saturating_sub(pad).max(1)
227 };
228 let (mark, color) = if is_primary {
229 ("^", sev_color)
230 } else {
231 ("-", st.blue)
232 };
233 out.push_str(&format!(
234 "{blue}{:gw$} |{reset} {:pad$}{color}{marks} {msg}{reset}\n",
235 "",
236 "",
237 gw = gw,
238 pad = pad,
239 color = color,
240 marks = mark.repeat(width),
241 msg = label.message,
242 blue = st.blue,
243 reset = st.reset,
244 ));
245 }
246 out.push_str(&format!(
247 "{:gw$} {blue}|{reset}\n",
248 "",
249 gw = gw,
250 blue = st.blue,
251 reset = st.reset
252 ));
253 }
254
255 if let Some(help) = &d.help {
256 out.push_str(&format!(
257 "{green}help{reset}{bold}:{reset} {help}\n",
258 green = st.green,
259 reset = st.reset,
260 bold = st.bold,
261 help = help
262 ));
263 }
264 for note in &d.notes {
265 out.push_str(&format!(
266 "{blue}note{reset}{bold}:{reset} {note}\n",
267 blue = st.blue,
268 reset = st.reset,
269 bold = st.bold,
270 note = note
271 ));
272 }
273 out
274}
275
276pub fn render_all(diags: &[Diagnostic], src: &SourceFile, st: &Styles) -> String {
279 let mut out = String::new();
280 for d in diags {
281 out.push_str(&render(d, src, st));
282 out.push('\n');
283 }
284 let errors = diags
285 .iter()
286 .filter(|d| d.severity == Severity::Error)
287 .count();
288 let warnings = diags
289 .iter()
290 .filter(|d| d.severity == Severity::Warning)
291 .count();
292 if errors > 0 || warnings > 0 {
293 let mut parts = Vec::new();
294 if errors > 0 {
295 parts.push(format!(
296 "{} error{}",
297 errors,
298 if errors == 1 { "" } else { "s" }
299 ));
300 }
301 if warnings > 0 {
302 parts.push(format!(
303 "{} warning{}",
304 warnings,
305 if warnings == 1 { "" } else { "s" }
306 ));
307 }
308 let (color, word) = if errors > 0 {
309 (st.red, "error")
310 } else {
311 (st.yellow, "warning")
312 };
313 out.push_str(&format!(
314 "{color}{word}{reset}{bold}: `{file}` emitted {summary}{reset}\n",
315 color = color,
316 word = word,
317 file = src.name,
318 summary = parts.join(", "),
319 reset = st.reset,
320 bold = st.bold,
321 ));
322 }
323 out
324}
325
326pub fn edit_distance(a: &str, b: &str) -> usize {
328 let a: Vec<char> = a.chars().collect();
329 let b: Vec<char> = b.chars().collect();
330 let mut prev: Vec<usize> = (0..=b.len()).collect();
331 let mut cur = vec![0; b.len() + 1];
332 for i in 1..=a.len() {
333 cur[0] = i;
334 for j in 1..=b.len() {
335 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
336 cur[j] = (prev[j] + 1).min(cur[j - 1] + 1).min(prev[j - 1] + cost);
337 }
338 std::mem::swap(&mut prev, &mut cur);
339 }
340 prev[b.len()]
341}
342
343pub fn suggest<'a>(input: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
345 let max = (input.chars().count() / 3).max(1) + 1;
346 candidates
347 .into_iter()
348 .map(|c| (edit_distance(input, c), c))
349 .filter(|(d, _)| *d <= max)
350 .min_by_key(|(d, _)| *d)
351 .map(|(_, c)| c)
352}