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 primary_span_underline: bool,
118}
119
120impl Formatted {
121 pub fn new_from_span<S>(
123 message: S,
124 help: Option<String>,
125 code: i32,
126 type_: String,
127 error: bool,
128 span: Span,
129 labels: Vec<Label>,
130 ) -> Self
131 where
132 S: ToString,
133 {
134 Self {
135 inner: Box::new(FormattedInner {
136 message: message.to_string(),
137 help,
138 note: None,
139 code,
140 type_,
141 error,
142 span,
143 labels,
144 primary_span_underline: false,
145 }),
146 }
147 }
148
149 pub fn error(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
151 Self::new_from_span(message, None, code, code_prefix.to_string(), true, span, vec![])
152 }
153
154 pub fn warning(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
156 Self::new_from_span(message, None, code, code_prefix.to_string(), false, span, vec![])
157 }
158
159 pub fn with_help(mut self, help: impl fmt::Display) -> Self {
160 self.inner.help = Some(help.to_string());
161 self
162 }
163
164 pub fn with_note(mut self, note: impl fmt::Display) -> Self {
165 self.inner.note = Some(note.to_string());
166 self
167 }
168
169 pub fn with_label(mut self, label: Label) -> Self {
170 self.inner.labels.push(label);
171 self
172 }
173
174 pub fn with_primary_span_underline(mut self) -> Self {
177 self.inner.primary_span_underline = true;
178 self
179 }
180
181 pub fn with_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Self {
182 self.inner.labels.extend(labels);
183 self
184 }
185
186 pub fn exit_code(&self) -> i32 {
188 compute_exit_code(37, self.inner.code)
189 }
190
191 pub fn error_code(&self) -> String {
193 format_error_code(&self.inner.type_, 37, self.inner.code)
194 }
195
196 pub fn warning_code(&self) -> String {
198 format_warning_code(&self.inner.type_, 37, self.inner.code)
199 }
200
201 pub fn message(&self) -> &str {
208 &self.inner.message
209 }
210
211 pub fn help(&self) -> Option<&str> {
216 self.inner.help.as_deref()
217 }
218
219 pub fn note(&self) -> Option<&str> {
224 self.inner.note.as_deref()
225 }
226
227 pub fn is_error(&self) -> bool {
233 self.inner.error
234 }
235
236 pub fn span(&self) -> Span {
242 self.inner.span
243 }
244
245 pub fn labels(&self) -> impl Iterator<Item = &Label> {
250 self.inner.labels.iter()
251 }
252
253 pub fn diagnostic_view(&self) -> DiagnosticView<'_> {
260 let code = if self.inner.error { self.error_code() } else { self.warning_code() };
261 let labels = self
262 .inner
263 .labels
264 .iter()
265 .map(|label| DiagnosticLabelView { message: label.message().to_owned(), span: label.span() })
266 .collect();
267 DiagnosticView {
268 message: &self.inner.message,
269 help: self.inner.help.as_deref(),
270 note: self.inner.note.as_deref(),
271 code,
272 is_error: self.inner.error,
273 span: Some(self.inner.span),
274 labels,
275 }
276 }
277
278 fn resolve_span(span: Span, source_map: &leo_span::source_map::SourceMap) -> AriadneSpan {
280 let file_start_index = source_map.find_source_file(span.lo).unwrap().absolute_start;
281 AriadneSpan { file_start_index, span }
282 }
283
284 fn build_report(&self) -> Report<'_, AriadneSpan> {
286 use leo_span::with_session_globals;
287
288 let primary_color = if self.inner.error { Color::Red } else { Color::Yellow };
289
290 with_session_globals(|s| {
291 let primary_span = Self::resolve_span(self.inner.span, &s.source_map);
292
293 let primary_is_multiline = s.source_map.find_source_file(self.inner.span.lo).is_some_and(|f| {
297 let lo = (self.inner.span.lo - f.absolute_start) as usize;
298 let hi = (self.inner.span.hi - f.absolute_start) as usize;
299 f.src.as_bytes().get(lo..hi).is_some_and(|b| b.contains(&b'\n'))
300 });
301 let mut primary = ariadne::Label::new(primary_span.clone()).with_color(primary_color);
302 if primary_is_multiline {
303 primary = primary.with_message("here");
304 } else if self.inner.primary_span_underline {
305 primary = primary.with_message("");
306 }
307 let primary_label = std::iter::once(primary);
308
309 let extra_labels: Vec<_> = self
310 .inner
311 .labels
312 .iter()
313 .map(|l| {
314 ariadne::Label::new(Self::resolve_span(l.span, &s.source_map))
315 .with_message(&l.msg)
316 .with_color(l.color)
317 })
318 .collect();
319
320 let mut report = Report::build(
321 if self.inner.error { ariadne::ReportKind::Error } else { ariadne::ReportKind::Warning },
322 primary_span,
323 )
324 .with_config(ariadne::Config::default().with_color(is_color()).with_index_type(IndexType::Byte))
325 .with_message(&self.inner.message)
326 .with_code(if self.inner.error { self.error_code() } else { self.warning_code() })
327 .with_labels(primary_label.chain(extra_labels));
328
329 if let Some(help) = &self.inner.help {
330 report = report.with_help(help);
331 }
332
333 if let Some(note) = &self.inner.note {
334 report = report.with_note(note);
335 }
336
337 report.finish()
338 })
339 }
340}
341
342impl fmt::Display for Formatted {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 if SESSION_GLOBALS.is_set() {
345 let report = self.build_report();
346 let mut cache = LeoSourceCache::new();
347 let mut buf = Vec::new();
348 report.write(&mut cache, &mut buf).map_err(|_| fmt::Error)?;
349 let output = String::from_utf8(buf).map_err(|_| fmt::Error)?;
350 let output = if self.inner.primary_span_underline {
351 normalize_empty_primary_label_underline(&output)
352 } else {
353 output
354 };
355 write!(f, "{output}")
356 } else {
357 let (kind, code) =
359 if self.inner.error { ("Error", self.error_code()) } else { ("Warning", self.warning_code()) };
360 write!(f, "{kind} [{code}]: {}", self.inner.message)?;
361 if let Some(help) = &self.inner.help {
362 write!(f, "\n = help: {help}")?;
363 }
364 if let Some(note) = &self.inner.note {
365 write!(f, "\n = note: {note}")?;
366 }
367 Ok(())
368 }
369 }
370}
371
372fn normalize_empty_primary_label_underline(output: &str) -> String {
373 let mut normalized = Vec::new();
374 let mut lines = output.lines();
375
376 while let Some(line) = lines.next() {
377 if line.contains('┬') && line.contains('─') {
381 normalized.push(line.replace('┬', "─").trim_end().to_owned());
382 if let Some(next) = lines.next()
383 && !next.contains('╰')
384 {
385 normalized.push(next.to_owned());
386 }
387 } else {
388 normalized.push(line.to_owned());
389 }
390 }
391
392 let mut output = normalized.join("\n");
393 if output.is_empty() || output.ends_with('\n') {
394 output
395 } else {
396 output.push('\n');
397 output
398 }
399}
400
401impl std::error::Error for Formatted {
402 fn description(&self) -> &str {
403 &self.inner.message
404 }
405}
406
407#[derive(Debug, Clone)]
415pub struct DiagnosticView<'a> {
416 pub message: &'a str,
418 pub help: Option<&'a str>,
420 pub note: Option<&'a str>,
422 pub code: String,
424 pub is_error: bool,
426 pub span: Option<Span>,
428 pub labels: Vec<DiagnosticLabelView>,
430}
431
432#[derive(Debug, Clone)]
437pub struct DiagnosticLabelView {
438 pub message: String,
440 pub span: Span,
442}
443
444#[cfg(test)]
445mod tests {
446 use super::{Color, Formatted, Label};
447 use leo_span::{Span, create_session_if_not_set_then, source_map::FileName};
448
449 #[test]
451 fn diagnostic_view_exposes_primary_fields() {
452 create_session_if_not_set_then(|_| {
453 let span = Span::default();
454 let error = Formatted::error("TST", 1, "boom", span).with_help("try again").with_note("note text");
455
456 let view = error.diagnostic_view();
457 assert_eq!(view.message, "boom");
458 assert_eq!(view.help, Some("try again"));
459 assert_eq!(view.note, Some("note text"));
460 assert_eq!(view.code, error.error_code());
461 assert!(view.is_error);
462 assert_eq!(view.span, Some(span));
463 assert!(view.labels.is_empty());
464 });
465 }
466
467 #[test]
469 fn diagnostic_view_exposes_secondary_labels() {
470 create_session_if_not_set_then(|_| {
471 let primary = Span::new(0, 4);
472 let label_span = Span::new(5, 10);
473 let error = Formatted::error("TST", 2, "boom", primary)
474 .with_label(Label::new(label_span).with_message("see also").with_color(Color::Blue));
475
476 let view = error.diagnostic_view();
477 assert_eq!(view.labels.len(), 1);
478 assert_eq!(view.labels[0].message, "see also");
479 assert_eq!(view.labels[0].span, label_span);
480 });
481 }
482
483 #[test]
485 fn diagnostic_view_marks_warnings() {
486 create_session_if_not_set_then(|_| {
487 let warning = Formatted::warning("TST", 3, "watch out", Span::default());
488 let view = warning.diagnostic_view();
489 assert!(!view.is_error);
490 assert_eq!(view.code, warning.warning_code());
491 });
492 }
493
494 #[test]
496 fn multi_line_primary_span_uses_default_label() {
497 create_session_if_not_set_then(|s| {
498 let source = "program test.aleo {\n @custom\n constructor() {}\n}\n";
499 let file = s.source_map.new_source(source, FileName::Custom("test.leo".into()));
500 let lo = file.absolute_start + source.find("@custom").unwrap() as u32;
501 let hi = file.absolute_start + source.find(" {}\n").unwrap() as u32 + 3;
502 let rendered = Formatted::error("TST", 4, "boom", Span::new(lo, hi)).to_string();
503
504 assert!(rendered.contains("here"));
505
506 let lo = file.absolute_start + source.find("test").unwrap() as u32;
507 let hi = lo + "test".len() as u32;
508 let rendered = Formatted::error("TST", 5, "boom", Span::new(lo, hi)).to_string();
509
510 assert!(!rendered.contains("here"));
511 });
512 }
513
514 #[test]
517 fn primary_span_underline_omits_empty_pointer_connector() {
518 create_session_if_not_set_then(|s| {
519 let source = "program test.aleo {\n}\n";
520 let file = s.source_map.new_source(source, FileName::Custom("test.leo".into()));
521 let lo = file.absolute_start + source.find("test.aleo").unwrap() as u32;
522 let hi = lo + "test.aleo".len() as u32;
523 let rendered =
524 Formatted::error("TST", 6, "boom", Span::new(lo, hi)).with_primary_span_underline().to_string();
525
526 assert!(rendered.chars().filter(|c| *c == '─').count() >= 13);
527 assert!(!rendered.contains('┬'));
528 assert!(!rendered.contains('╰'));
529 });
530 }
531}