use crate::{compute_exit_code, format_error_code, format_warning_code};
use leo_span::{
SESSION_GLOBALS,
Span,
source_map::{LeoSourceCache, is_color},
};
pub use ariadne::Color;
use ariadne::{IndexType, Report};
use std::fmt;
#[derive(Debug)]
pub struct Label {
msg: String,
span: Span,
color: Color,
}
impl Label {
pub fn new(span: Span) -> Self {
Self { msg: String::new(), span, color: Color::default() }
}
pub fn with_message(mut self, msg: impl fmt::Display) -> Self {
self.msg = msg.to_string();
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn message(&self) -> &str {
&self.msg
}
pub fn span(&self) -> Span {
self.span
}
}
#[derive(Clone)]
struct AriadneSpan {
file_start_index: u32,
span: Span,
}
impl ariadne::Span for AriadneSpan {
type SourceId = u32;
fn source(&self) -> &Self::SourceId {
&self.file_start_index
}
fn start(&self) -> usize {
(self.span.lo - self.file_start_index) as usize
}
fn end(&self) -> usize {
(self.span.hi - self.file_start_index) as usize
}
}
#[derive(Debug)]
pub struct Formatted {
inner: Box<FormattedInner>,
}
#[derive(Debug)]
struct FormattedInner {
message: String,
help: Option<String>,
note: Option<String>,
code: i32,
type_: String,
error: bool,
span: Span,
labels: Vec<Label>,
}
impl Formatted {
pub fn new_from_span<S>(
message: S,
help: Option<String>,
code: i32,
type_: String,
error: bool,
span: Span,
labels: Vec<Label>,
) -> Self
where
S: ToString,
{
Self {
inner: Box::new(FormattedInner {
message: message.to_string(),
help,
note: None,
code,
type_,
error,
span,
labels,
}),
}
}
pub fn error(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
Self::new_from_span(message, None, code, code_prefix.to_string(), true, span, vec![])
}
pub fn warning(code_prefix: &str, code: i32, message: impl ToString, span: Span) -> Self {
Self::new_from_span(message, None, code, code_prefix.to_string(), false, span, vec![])
}
pub fn with_help(mut self, help: impl fmt::Display) -> Self {
self.inner.help = Some(help.to_string());
self
}
pub fn with_note(mut self, note: impl fmt::Display) -> Self {
self.inner.note = Some(note.to_string());
self
}
pub fn with_label(mut self, label: Label) -> Self {
self.inner.labels.push(label);
self
}
pub fn with_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Self {
self.inner.labels.extend(labels);
self
}
pub fn exit_code(&self) -> i32 {
compute_exit_code(37, self.inner.code)
}
pub fn error_code(&self) -> String {
format_error_code(&self.inner.type_, 37, self.inner.code)
}
pub fn warning_code(&self) -> String {
format_warning_code(&self.inner.type_, 37, self.inner.code)
}
pub fn message(&self) -> &str {
&self.inner.message
}
pub fn help(&self) -> Option<&str> {
self.inner.help.as_deref()
}
pub fn note(&self) -> Option<&str> {
self.inner.note.as_deref()
}
pub fn is_error(&self) -> bool {
self.inner.error
}
pub fn span(&self) -> Span {
self.inner.span
}
pub fn labels(&self) -> impl Iterator<Item = &Label> {
self.inner.labels.iter()
}
pub fn diagnostic_view(&self) -> DiagnosticView<'_> {
let code = if self.inner.error { self.error_code() } else { self.warning_code() };
let labels = self
.inner
.labels
.iter()
.map(|label| DiagnosticLabelView { message: label.message().to_owned(), span: label.span() })
.collect();
DiagnosticView {
message: &self.inner.message,
help: self.inner.help.as_deref(),
note: self.inner.note.as_deref(),
code,
is_error: self.inner.error,
span: Some(self.inner.span),
labels,
}
}
fn resolve_span(span: Span, source_map: &leo_span::source_map::SourceMap) -> AriadneSpan {
let file_start_index = source_map.find_source_file(span.lo).unwrap().absolute_start;
AriadneSpan { file_start_index, span }
}
fn build_report(&self) -> Report<'_, AriadneSpan> {
use leo_span::with_session_globals;
let primary_color = if self.inner.error { Color::Red } else { Color::Yellow };
with_session_globals(|s| {
let primary_span = Self::resolve_span(self.inner.span, &s.source_map);
let primary_is_multiline = s.source_map.find_source_file(self.inner.span.lo).is_some_and(|f| {
let lo = (self.inner.span.lo - f.absolute_start) as usize;
let hi = (self.inner.span.hi - f.absolute_start) as usize;
f.src.as_bytes().get(lo..hi).is_some_and(|b| b.contains(&b'\n'))
});
let mut primary = ariadne::Label::new(primary_span.clone()).with_color(primary_color);
if primary_is_multiline {
primary = primary.with_message("");
}
let primary_label = std::iter::once(primary);
let extra_labels: Vec<_> = self
.inner
.labels
.iter()
.map(|l| {
ariadne::Label::new(Self::resolve_span(l.span, &s.source_map))
.with_message(&l.msg)
.with_color(l.color)
})
.collect();
let mut report = Report::build(
if self.inner.error { ariadne::ReportKind::Error } else { ariadne::ReportKind::Warning },
primary_span,
)
.with_config(ariadne::Config::default().with_color(is_color()).with_index_type(IndexType::Byte))
.with_message(&self.inner.message)
.with_code(if self.inner.error { self.error_code() } else { self.warning_code() })
.with_labels(primary_label.chain(extra_labels));
if let Some(help) = &self.inner.help {
report = report.with_help(help);
}
if let Some(note) = &self.inner.note {
report = report.with_note(note);
}
report.finish()
})
}
}
impl fmt::Display for Formatted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if SESSION_GLOBALS.is_set() {
let report = self.build_report();
let mut cache = LeoSourceCache::new();
let mut buf = Vec::new();
report.write(&mut cache, &mut buf).map_err(|_| fmt::Error)?;
let output = String::from_utf8(buf).map_err(|_| fmt::Error)?;
write!(f, "{output}")
} else {
let (kind, code) =
if self.inner.error { ("Error", self.error_code()) } else { ("Warning", self.warning_code()) };
write!(f, "{kind} [{code}]: {}", self.inner.message)?;
if let Some(help) = &self.inner.help {
write!(f, "\n = help: {help}")?;
}
if let Some(note) = &self.inner.note {
write!(f, "\n = note: {note}")?;
}
Ok(())
}
}
}
impl std::error::Error for Formatted {
fn description(&self) -> &str {
&self.inner.message
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticView<'a> {
pub message: &'a str,
pub help: Option<&'a str>,
pub note: Option<&'a str>,
pub code: String,
pub is_error: bool,
pub span: Option<Span>,
pub labels: Vec<DiagnosticLabelView>,
}
#[derive(Debug, Clone)]
pub struct DiagnosticLabelView {
pub message: String,
pub span: Span,
}
#[cfg(test)]
mod tests {
use super::{Color, Formatted, Label};
use leo_span::{Span, create_session_if_not_set_then};
#[test]
fn diagnostic_view_exposes_primary_fields() {
create_session_if_not_set_then(|_| {
let span = Span::default();
let error = Formatted::error("TST", 1, "boom", span).with_help("try again").with_note("note text");
let view = error.diagnostic_view();
assert_eq!(view.message, "boom");
assert_eq!(view.help, Some("try again"));
assert_eq!(view.note, Some("note text"));
assert_eq!(view.code, error.error_code());
assert!(view.is_error);
assert_eq!(view.span, Some(span));
assert!(view.labels.is_empty());
});
}
#[test]
fn diagnostic_view_exposes_secondary_labels() {
create_session_if_not_set_then(|_| {
let primary = Span::new(0, 4);
let label_span = Span::new(5, 10);
let error = Formatted::error("TST", 2, "boom", primary)
.with_label(Label::new(label_span).with_message("see also").with_color(Color::Blue));
let view = error.diagnostic_view();
assert_eq!(view.labels.len(), 1);
assert_eq!(view.labels[0].message, "see also");
assert_eq!(view.labels[0].span, label_span);
});
}
#[test]
fn diagnostic_view_marks_warnings() {
create_session_if_not_set_then(|_| {
let warning = Formatted::warning("TST", 3, "watch out", Span::default());
let view = warning.diagnostic_view();
assert!(!view.is_error);
assert_eq!(view.code, warning.warning_code());
});
}
}