use crate::syntax::{SourceMap, Span};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub span: Option<Span>,
pub message: String,
pub help: Option<String>,
}
impl Diagnostic {
pub fn new(span: Span, message: impl Into<String>) -> Diagnostic {
Diagnostic { span: Some(span), message: message.into(), help: None }
}
pub fn bare(message: impl Into<String>) -> Diagnostic {
Diagnostic { span: None, message: message.into(), help: None }
}
pub fn help(mut self, help: impl Into<String>) -> Diagnostic {
self.help = Some(help.into());
self
}
pub fn render(&self, sm: &SourceMap) -> String {
match self.span {
Some(sp) => format!("[{}] {}", sm.location(sp), self.message),
None => self.message.clone(),
}
}
}
pub fn render_snippet(sm: &SourceMap, span: Option<Span>, message: &str, help: Option<&str>) -> String {
render_coded(sm, None, span, message, help)
}
pub fn render_coded(
sm: &SourceMap,
code: Option<&str>,
span: Option<Span>,
message: &str,
help: Option<&str>,
) -> String {
render_with_level(sm, "error", code, span, message, help)
}
pub fn render_with_level(
sm: &SourceMap,
level: &str,
code: Option<&str>,
span: Option<Span>,
message: &str,
help: Option<&str>,
) -> String {
let mut out = match code {
Some(c) => format!("{}[{}]: {}\n", level, c, message),
None => format!("{}: {}\n", level, message),
};
let span = span.filter(|s| !s.is_dummy());
let Some(span) = span else {
if let Some(h) = help {
out.push_str(&format!(" = help: {}\n", h));
}
return out;
};
let f = sm.file(span.file);
let line = f.line_col(span.start).line;
let first = line.saturating_sub(2).max(1);
let last = (line + 1).min(f.line_count());
let width = last.to_string().len();
let pad = " ".repeat(width);
out.push_str(&format!("{} --> {}\n", pad, sm.location(span)));
out.push_str(&format!("{} |\n", pad));
for n in first..=last {
let text = f.line_text(n);
out.push_str(format!("{:>w$} | {}", n, text, w = width).trim_end());
out.push('\n');
if n == line {
let from = (text_offset(f, span.start) as usize).min(text.len());
let to = (from + span.end.saturating_sub(span.start) as usize).min(text.len());
let indent: String = text[..from].chars().map(|c| if c == '\t' { '\t' } else { ' ' }).collect();
let marks = text.get(from..to).map_or(0, |s| s.chars().count()).max(1);
out.push_str(&format!("{} | {}{}\n", pad, indent, "^".repeat(marks)));
}
}
if let Some(h) = help {
out.push_str(&format!("{} |\n{} = help: {}\n", pad, pad, h));
}
out
}
fn text_offset(f: &crate::syntax::SourceFile, offset: u32) -> u32 {
let before = &f.text[..(offset as usize).min(f.text.len())];
before.rfind('\n').map_or(before.len(), |i| before.len() - i - 1) as u32
}
#[derive(Debug, Default)]
pub struct Diagnostics {
pub errors: Vec<Diagnostic>,
pub warnings: Vec<String>,
pub continue_on_error: bool,
}
impl Diagnostics {
pub fn new(continue_on_error: bool) -> Diagnostics {
Diagnostics { errors: Vec::new(), warnings: Vec::new(), continue_on_error }
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn report(&mut self, d: Diagnostic) -> Result<(), Diagnostic> {
if self.continue_on_error {
self.errors.push(d);
Ok(())
} else {
Err(d)
}
}
pub fn tolerant<T>(&mut self, r: Result<T, Diagnostic>) -> Result<Option<T>, Diagnostic> {
match r {
Ok(v) => Ok(Some(v)),
Err(d) if self.continue_on_error => {
self.errors.push(d);
Ok(None)
}
Err(d) => Err(d),
}
}
pub fn warn(&mut self, msg: impl Into<String>) {
self.warnings.push(msg.into());
}
pub fn sorted_errors(&self, sm: &SourceMap) -> Vec<Diagnostic> {
let mut v: Vec<(usize, Diagnostic)> = self.errors.iter().cloned().enumerate().collect();
v.sort_by(|(ia, a), (ib, b)| {
let ka = sort_key(a, sm);
let kb = sort_key(b, sm);
ka.cmp(&kb).then(ia.cmp(ib))
});
v.into_iter().map(|(_, d)| d).collect()
}
}
fn sort_key(d: &Diagnostic, sm: &SourceMap) -> (String, i64, i64) {
match d.span {
Some(sp) if !sp.is_dummy() => {
let l = sm.location(sp);
(l.file.unwrap_or_default(), l.line, l.col)
}
_ => ("\u{ffff}".to_string(), 0, 0),
}
}
pub type Fatal = Diagnostic;