1use crate::{compute_exit_code, format_error_code, format_warning_code};
18use leo_span::{
19 SESSION_GLOBALS,
20 Span,
21 source_map::{LeoSourceCache, is_color},
22};
23
24pub use ariadne::Color;
25use ariadne::{IndexType, Report};
26use std::fmt;
27
28#[derive(Debug)]
30pub struct Label {
31 msg: String,
32 span: Span,
33 color: Color,
34}
35
36impl Label {
37 pub fn new(span: Span) -> Self {
38 Self { msg: String::new(), span, color: Color::default() }
39 }
40
41 pub fn with_message(mut self, msg: impl fmt::Display) -> Self {
42 self.msg = msg.to_string();
43 self
44 }
45
46 pub fn with_color(mut self, color: Color) -> Self {
47 self.color = color;
48 self
49 }
50
51 pub fn message(&self) -> &str {
56 &self.msg
57 }
58
59 pub fn span(&self) -> Span {
64 self.span
65 }
66}
67
68#[derive(Clone)]
70struct AriadneSpan {
71 file_start_index: u32,
72 span: Span,
73}
74
75impl ariadne::Span for AriadneSpan {
76 type SourceId = u32;
77
78 fn source(&self) -> &Self::SourceId {
79 &self.file_start_index
80 }
81
82 fn start(&self) -> usize {
83 (self.span.lo - self.file_start_index) as usize
84 }
85
86 fn end(&self) -> usize {
87 (self.span.hi - self.file_start_index) as usize
88 }
89}
90
91#[derive(Debug)]
103pub struct Formatted {
104 inner: Box<FormattedInner>,
105}
106
107#[derive(Debug)]
108struct FormattedInner {
109 message: String,
110 help: Option<String>,
111 note: Option<String>,
112 code: i32,
113 type_: String,
114 error: bool,
115 span: Span,
116 labels: Vec<Label>,
117}
118
119impl Formatted {
120 pub fn new_from_span<S>(
122 message: S,
123 help: Option<String>,
124 code: i32,
125 type_: String,
126 error: bool,
127 span: Span,
128 labels: Vec<Label>,
129 ) -> Self
130 where
131 S: ToString,
132 {
133 Self {
134 inner: Box::new(FormattedInner {
135 message: message.to_string(),
136 help,
137 note: None,
138 code,
139 type_,
140 error,
141 span,
142 labels,
143 }),
144 }
145 }
146
147 pub fn error(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
149 Self::new_from_span(message, None, code, code_prefix.to_string(), true, span, vec![])
150 }
151
152 pub fn warning(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
154 Self::new_from_span(message, None, code, code_prefix.to_string(), false, span, vec![])
155 }
156
157 pub fn with_help(mut self, help: impl fmt::Display) -> Self {
158 self.inner.help = Some(help.to_string());
159 self
160 }
161
162 pub fn with_note(mut self, note: impl fmt::Display) -> Self {
163 self.inner.note = Some(note.to_string());
164 self
165 }
166
167 pub fn with_label(mut self, label: Label) -> Self {
168 self.inner.labels.push(label);
169 self
170 }
171
172 pub fn with_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Self {
173 self.inner.labels.extend(labels);
174 self
175 }
176
177 pub fn exit_code(&self) -> i32 {
179 compute_exit_code(37, self.inner.code)
180 }
181
182 pub fn error_code(&self) -> String {
184 format_error_code(&self.inner.type_, 37, self.inner.code)
185 }
186
187 pub fn warning_code(&self) -> String {
189 format_warning_code(&self.inner.type_, 37, self.inner.code)
190 }
191
192 pub fn message(&self) -> &str {
199 &self.inner.message
200 }
201
202 pub fn help(&self) -> Option<&str> {
207 self.inner.help.as_deref()
208 }
209
210 pub fn note(&self) -> Option<&str> {
215 self.inner.note.as_deref()
216 }
217
218 pub fn is_error(&self) -> bool {
224 self.inner.error
225 }
226
227 pub fn span(&self) -> Span {
233 self.inner.span
234 }
235
236 pub fn labels(&self) -> impl Iterator<Item = &Label> {
241 self.inner.labels.iter()
242 }
243
244 pub fn diagnostic_view(&self) -> DiagnosticView<'_> {
251 let code = if self.inner.error { self.error_code() } else { self.warning_code() };
252 let labels = self
253 .inner
254 .labels
255 .iter()
256 .map(|label| DiagnosticLabelView { message: label.message().to_owned(), span: label.span() })
257 .collect();
258 DiagnosticView {
259 message: &self.inner.message,
260 help: self.inner.help.as_deref(),
261 note: self.inner.note.as_deref(),
262 code,
263 is_error: self.inner.error,
264 span: Some(self.inner.span),
265 labels,
266 }
267 }
268
269 fn resolve_span(span: Span, source_map: &leo_span::source_map::SourceMap) -> AriadneSpan {
271 let file_start_index = source_map.find_source_file(span.lo).unwrap().absolute_start;
272 AriadneSpan { file_start_index, span }
273 }
274
275 fn build_report(&self) -> Report<'_, AriadneSpan> {
277 use leo_span::with_session_globals;
278
279 let primary_color = if self.inner.error { Color::Red } else { Color::Yellow };
280
281 with_session_globals(|s| {
282 let primary_span = Self::resolve_span(self.inner.span, &s.source_map);
283
284 let primary_is_multiline = s.source_map.find_source_file(self.inner.span.lo).is_some_and(|f| {
288 let lo = (self.inner.span.lo - f.absolute_start) as usize;
289 let hi = (self.inner.span.hi - f.absolute_start) as usize;
290 f.src.as_bytes().get(lo..hi).is_some_and(|b| b.contains(&b'\n'))
291 });
292 let mut primary = ariadne::Label::new(primary_span.clone()).with_color(primary_color);
293 if primary_is_multiline {
294 primary = primary.with_message("");
295 }
296 let primary_label = std::iter::once(primary);
297
298 let extra_labels: Vec<_> = self
299 .inner
300 .labels
301 .iter()
302 .map(|l| {
303 ariadne::Label::new(Self::resolve_span(l.span, &s.source_map))
304 .with_message(&l.msg)
305 .with_color(l.color)
306 })
307 .collect();
308
309 let mut report = Report::build(
310 if self.inner.error { ariadne::ReportKind::Error } else { ariadne::ReportKind::Warning },
311 primary_span,
312 )
313 .with_config(ariadne::Config::default().with_color(is_color()).with_index_type(IndexType::Byte))
314 .with_message(&self.inner.message)
315 .with_code(if self.inner.error { self.error_code() } else { self.warning_code() })
316 .with_labels(primary_label.chain(extra_labels));
317
318 if let Some(help) = &self.inner.help {
319 report = report.with_help(help);
320 }
321
322 if let Some(note) = &self.inner.note {
323 report = report.with_note(note);
324 }
325
326 report.finish()
327 })
328 }
329}
330
331impl fmt::Display for Formatted {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 if SESSION_GLOBALS.is_set() {
334 let report = self.build_report();
335 let mut cache = LeoSourceCache::new();
336 let mut buf = Vec::new();
337 report.write(&mut cache, &mut buf).map_err(|_| fmt::Error)?;
338 let output = String::from_utf8(buf).map_err(|_| fmt::Error)?;
339 write!(f, "{output}")
340 } else {
341 let (kind, code) =
343 if self.inner.error { ("Error", self.error_code()) } else { ("Warning", self.warning_code()) };
344 write!(f, "{kind} [{code}]: {}", self.inner.message)?;
345 if let Some(help) = &self.inner.help {
346 write!(f, "\n = help: {help}")?;
347 }
348 if let Some(note) = &self.inner.note {
349 write!(f, "\n = note: {note}")?;
350 }
351 Ok(())
352 }
353 }
354}
355
356impl std::error::Error for Formatted {
357 fn description(&self) -> &str {
358 &self.inner.message
359 }
360}
361
362#[derive(Debug, Clone)]
370pub struct DiagnosticView<'a> {
371 pub message: &'a str,
373 pub help: Option<&'a str>,
375 pub note: Option<&'a str>,
377 pub code: String,
379 pub is_error: bool,
381 pub span: Option<Span>,
383 pub labels: Vec<DiagnosticLabelView>,
385}
386
387#[derive(Debug, Clone)]
392pub struct DiagnosticLabelView {
393 pub message: String,
395 pub span: Span,
397}
398
399#[cfg(test)]
400mod tests {
401 use super::{Color, Formatted, Label};
402 use leo_span::{Span, create_session_if_not_set_then};
403
404 #[test]
406 fn diagnostic_view_exposes_primary_fields() {
407 create_session_if_not_set_then(|_| {
408 let span = Span::default();
409 let error = Formatted::error("TST", 1, "boom", span).with_help("try again").with_note("note text");
410
411 let view = error.diagnostic_view();
412 assert_eq!(view.message, "boom");
413 assert_eq!(view.help, Some("try again"));
414 assert_eq!(view.note, Some("note text"));
415 assert_eq!(view.code, error.error_code());
416 assert!(view.is_error);
417 assert_eq!(view.span, Some(span));
418 assert!(view.labels.is_empty());
419 });
420 }
421
422 #[test]
424 fn diagnostic_view_exposes_secondary_labels() {
425 create_session_if_not_set_then(|_| {
426 let primary = Span::new(0, 4);
427 let label_span = Span::new(5, 10);
428 let error = Formatted::error("TST", 2, "boom", primary)
429 .with_label(Label::new(label_span).with_message("see also").with_color(Color::Blue));
430
431 let view = error.diagnostic_view();
432 assert_eq!(view.labels.len(), 1);
433 assert_eq!(view.labels[0].message, "see also");
434 assert_eq!(view.labels[0].span, label_span);
435 });
436 }
437
438 #[test]
440 fn diagnostic_view_marks_warnings() {
441 create_session_if_not_set_then(|_| {
442 let warning = Formatted::warning("TST", 3, "watch out", Span::default());
443 let view = warning.diagnostic_view();
444 assert!(!view.is_error);
445 assert_eq!(view.code, warning.warning_code());
446 });
447 }
448}