1use std::fmt;
2
3use colored::{Color, Colorize};
4
5use crate::{InputSrc, Inputs, Span};
6
7#[derive(Debug, Clone)]
9pub struct Diagnostic {
10 pub span: Span,
12 pub message: String,
14 pub kind: DiagnosticKind,
16 pub inputs: Inputs,
18}
19
20impl PartialEq for Diagnostic {
21 fn eq(&self, other: &Self) -> bool {
22 self.span == other.span && self.message == other.message
23 }
24}
25
26impl Eq for Diagnostic {}
27
28impl PartialOrd for Diagnostic {
29 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
30 Some(self.cmp(other))
31 }
32}
33
34impl Ord for Diagnostic {
35 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
36 self.span.cmp(&other.span)
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum DiagnosticKind {
43 Info,
45 Style,
47 Advice,
49 Warning,
51}
52
53impl fmt::Display for Diagnostic {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 self.message.fmt(f)
56 }
57}
58
59impl Diagnostic {
60 pub fn new(
62 message: String,
63 span: impl Into<Span>,
64 kind: DiagnosticKind,
65 inputs: Inputs,
66 ) -> Self {
67 Self {
68 message,
69 span: span.into(),
70 kind,
71 inputs,
72 }
73 }
74 pub fn report(&self) -> Report {
76 Report::new_multi(
77 ReportKind::Diagnostic(self.kind),
78 &self.inputs,
79 [(&self.message, self.span.clone())],
80 )
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ReportKind {
87 Error,
89 Diagnostic(DiagnosticKind),
91}
92
93impl From<DiagnosticKind> for ReportKind {
94 fn from(kind: DiagnosticKind) -> Self {
95 ReportKind::Diagnostic(kind)
96 }
97}
98
99impl ReportKind {
100 pub fn str(&self) -> &'static str {
102 match self {
103 ReportKind::Error => "Error",
104 ReportKind::Diagnostic(DiagnosticKind::Warning) => "Warning",
105 ReportKind::Diagnostic(DiagnosticKind::Advice) => "Advice",
106 ReportKind::Diagnostic(DiagnosticKind::Style) => "Style",
107 ReportKind::Diagnostic(DiagnosticKind::Info) => "Info",
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum ReportFragment {
115 Plain(String),
117 Colored(String, ReportKind),
119 Faint(String),
121 Fainter(String),
123 Newline,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Report {
130 pub fragments: Vec<ReportFragment>,
132 pub color: bool,
136}
137
138impl Report {
139 pub fn color(mut self, color: bool) -> Self {
143 self.color = color;
144 self
145 }
146 pub fn new(kind: ReportKind, message: impl Into<String>) -> Self {
148 let message = message.into();
149 let mut fragments = vec![
150 ReportFragment::Colored(kind.str().into(), kind),
151 ReportFragment::Plain(": ".into()),
152 ];
153 if message.lines().count() > 1 {
154 fragments.push(ReportFragment::Newline);
155 }
156 fragments.push(ReportFragment::Plain(message));
157 Self {
158 fragments,
159 color: true,
160 }
161 }
162 pub fn new_multi<I, T>(kind: ReportKind, inputs: &Inputs, errors: I) -> Self
164 where
165 I: IntoIterator<Item = (T, Span)>,
166 T: fmt::Display,
167 {
168 let mut fragments = Vec::new();
169 for (i, (message, span)) in errors.into_iter().enumerate() {
170 if i > 0 {
171 fragments.push(ReportFragment::Newline);
172 }
173 fragments.push(ReportFragment::Colored(kind.str().into(), kind));
174 fragments.push(ReportFragment::Plain(": ".into()));
175 let message = message.to_string();
176 for (i, line) in message.lines().enumerate() {
177 if i > 0 || message.lines().count() > 1 {
178 fragments.push(ReportFragment::Newline);
179 fragments.push(ReportFragment::Plain(" ".into()));
180 }
181 fragments.push(ReportFragment::Plain(line.into()));
182 }
183 if let Span::Code(mut span) = span {
184 while let InputSrc::Macro(inner) = span.src {
185 span = *inner;
186 }
187 fragments.push(ReportFragment::Newline);
188 fragments.push(ReportFragment::Fainter(" at ".into()));
189 if let InputSrc::File(path) = &span.src {
190 fragments.push(ReportFragment::Fainter(format!("{}:", path.display())));
191 }
192 fragments.push(ReportFragment::Fainter(format!(
193 "{}:{}",
194 span.start.line, span.start.col
195 )));
196 fragments.push(ReportFragment::Newline);
197 let line_prefix = format!("{} | ", span.start.line);
198 fragments.push(ReportFragment::Plain(line_prefix.clone()));
199 let input = inputs.get(&span.src);
200 let line = input
201 .lines()
202 .nth(span.start.line as usize - 1)
203 .unwrap_or("");
204 let start_char_pos = span.start.col - 1;
205 let end_char_pos = if span.start.line == span.end.line {
206 span.end.col - 1
207 } else {
208 line.chars().count() as u16
209 };
210 let pre_color: String = line.chars().take(start_char_pos as usize).collect();
211 let color: String = line
212 .chars()
213 .skip(start_char_pos as usize)
214 .take(end_char_pos.saturating_sub(start_char_pos).max(1) as usize)
215 .collect();
216 let post_color: String = line.chars().skip(end_char_pos as usize).collect();
217 fragments.push(ReportFragment::Faint(pre_color));
218 fragments.push(ReportFragment::Colored(color, kind));
219 fragments.push(ReportFragment::Faint(post_color));
220 fragments.push(ReportFragment::Newline);
221 fragments.push(ReportFragment::Plain(
222 " ".repeat(line_prefix.chars().count()),
223 ));
224 fragments.push(ReportFragment::Plain(" ".repeat(start_char_pos as usize)));
225 fragments.push(ReportFragment::Colored(
226 "─".repeat(end_char_pos.saturating_sub(start_char_pos).max(1) as usize),
227 kind,
228 ));
229 }
230 }
231 Self {
232 fragments,
233 color: true,
234 }
235 }
236 pub fn tests(successes: usize, failures: usize, not_run: usize) -> Self {
238 let mut fragments = if successes == 0 && not_run == 0 {
239 if failures == 0 {
240 vec![]
241 } else {
242 vec![ReportFragment::Colored(
243 match failures {
244 1 => "Test failed".into(),
245 2 => "Both tests failed".into(),
246 _ => format!("All {failures} tests failed"),
247 },
248 ReportKind::Error,
249 )]
250 }
251 } else {
252 let mut fragments = vec![ReportFragment::Colored(
253 match (successes, failures) {
254 (1, 0) if not_run == 0 => "Test passed".into(),
255 (2, 0) if not_run == 0 => "Both tests passed".into(),
256 (suc, 0) if not_run == 0 => format!("All {suc} tests passed"),
257 (suc, _) => {
258 format!("{suc} test{} passed", if suc == 1 { "" } else { "s" })
259 }
260 },
261 DiagnosticKind::Info.into(),
262 )];
263 if failures > 0 {
264 fragments.extend([
265 ReportFragment::Plain(", ".into()),
266 ReportFragment::Colored(format!("{failures} failed"), ReportKind::Error),
267 ])
268 }
269 fragments
270 };
271 if not_run > 0 {
272 if fragments.is_empty() {
273 fragments.push(ReportFragment::Colored(
274 format!("0 of {not_run} tests ran"),
275 ReportKind::Error,
276 ));
277 } else {
278 fragments.push(ReportFragment::Plain(", ".into()));
279 fragments.push(ReportFragment::Colored(
280 format!("{not_run} didn't run"),
281 DiagnosticKind::Warning.into(),
282 ));
283 }
284 }
285 Report {
286 fragments,
287 color: true,
288 }
289 }
290}
291
292impl fmt::Display for Report {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 for frag in &self.fragments {
295 match frag {
296 ReportFragment::Plain(s)
297 | ReportFragment::Faint(s)
298 | ReportFragment::Fainter(s) => write!(f, "{s}")?,
299 ReportFragment::Colored(s, kind) => {
300 if self.color {
301 let s = s.color(match kind {
302 ReportKind::Error => Color::Red,
303 ReportKind::Diagnostic(DiagnosticKind::Warning) => Color::Yellow,
304 ReportKind::Diagnostic(DiagnosticKind::Style) => Color::Green,
305 ReportKind::Diagnostic(DiagnosticKind::Advice) => Color::TrueColor {
306 r: 50,
307 g: 150,
308 b: 255,
309 },
310 ReportKind::Diagnostic(DiagnosticKind::Info) => Color::BrightCyan,
311 });
312 write!(f, "{s}")?
313 } else {
314 write!(f, "{s}")?
315 }
316 }
317 ReportFragment::Newline => writeln!(f)?,
318 }
319 }
320 Ok(())
321 }
322}