use std::{
error::Error,
fmt::{self, Display, Formatter},
};
use annotate_snippets::{
display_list::{DisplayList, FormatOptions},
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
};
pub use crate::analysis::Lint;
use crate::{
codemap::{CodeMap, FileSpan, Span},
eval::CallStack,
values::string::{fast_string, CharIndex},
};
pub(crate) mod did_you_mean;
#[derive(Debug)]
pub struct Diagnostic {
pub message: anyhow::Error,
pub span: Option<FileSpan>,
pub call_stack: CallStack,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Frame {
pub name: String,
pub location: Option<FileSpan>,
}
impl Display for Frame {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.name)?;
if let Some(loc) = &self.location {
write!(f, " (called from {})", loc)?;
}
Ok(())
}
}
fn truncate_snippet(snippet: &str, max_len: usize) -> (&str, &str) {
let ddd = "...";
assert!(max_len >= ddd.len());
match fast_string::split_at(snippet, CharIndex(max_len - ddd.len())) {
None => (snippet, ""),
Some((_, b)) if b.chars().nth(3).is_none() => (snippet, ""),
Some((a, _)) => (a, "..."),
}
}
impl Frame {
pub(crate) fn write_two_lines(
&self,
indent: &str,
caller: &str,
write: &mut dyn fmt::Write,
) -> fmt::Result {
if let Some(location) = &self.location {
let line = location
.file
.source_line_at_pos(location.span.begin())
.trim();
let (line, ddd) = truncate_snippet(line, 50);
writeln!(
write,
"{}* {}:{}, in {}",
indent,
location.file.filename(),
location.file.find_line(location.span.begin()) + 1,
caller,
)?;
writeln!(write, "{} {}{}", indent, line, ddd)?;
} else {
writeln!(write, "{}File <builtin>, in {}", indent, caller)?;
}
Ok(())
}
}
impl Error for Diagnostic {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
Some(self.message.backtrace())
}
}
impl Diagnostic {
pub(crate) fn new(
message: impl Into<anyhow::Error>,
span: Span,
codemap: &CodeMap,
) -> anyhow::Error {
Self::modify(message.into(), |d| d.set_span(span, codemap))
}
pub fn modify(mut err: anyhow::Error, f: impl FnOnce(&mut Diagnostic)) -> anyhow::Error {
match err.downcast_mut::<Diagnostic>() {
Some(diag) => {
f(diag);
err
}
_ => {
let mut err = Self {
message: err,
span: None,
call_stack: CallStack::default(),
};
f(&mut err);
err.into()
}
}
}
pub(crate) fn set_span(&mut self, span: Span, codemap: &CodeMap) {
if self.span.is_none() {
self.span = Some(codemap.file_span(span));
}
}
pub fn set_call_stack(&mut self, call_stack: impl FnOnce() -> CallStack) {
if self.call_stack.is_empty() {
self.call_stack = call_stack();
}
}
pub fn eprint(err: &anyhow::Error) {
match err.downcast_ref::<Diagnostic>() {
None => eprintln!("{:#}", err),
Some(diag) => diagnostic_stderr(diag),
}
}
}
impl Display for Diagnostic {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
diagnostic_display(self, f)
}
}
fn get_display_list_for_diagnostic<'a>(
annotation_label: &'a str,
x: &'a Diagnostic,
color: bool,
) -> DisplayList<'a> {
fn convert_span_to_range_relative_to_first_line(
diagnostic_span: Span,
start_column: usize,
) -> (usize, usize) {
let span_length = diagnostic_span.len() as usize;
(start_column, start_column + span_length)
}
fn convert_span_to_slice<'a>(span: &'a FileSpan) -> Slice<'a> {
let region = span.resolve_span();
let first_line_span = span.file.line_span(region.begin_line);
let last_line_span = span.file.line_span(region.end_line);
let source_span = span.span.merge(first_line_span).merge(last_line_span);
Slice {
source: span.file.source_span(source_span),
line_start: 1 + region.begin_line,
origin: Some(span.file.filename()),
fold: false,
annotations: vec![SourceAnnotation {
label: "",
annotation_type: AnnotationType::Error,
range: convert_span_to_range_relative_to_first_line(span.span, region.begin_column),
}],
}
}
let slice = x.span.as_ref().map(convert_span_to_slice);
let snippet = Snippet {
title: Some(Annotation {
label: Some(annotation_label),
id: None,
annotation_type: AnnotationType::Error,
}),
footer: Vec::new(),
slices: slice.map(|s| vec![s]).unwrap_or_default(),
opt: FormatOptions {
color,
..Default::default()
},
};
DisplayList::from(snippet)
}
fn diagnostic_display(diagnostic: &Diagnostic, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", &diagnostic.call_stack)?;
let annotation_label = format!("{:#}", diagnostic.message);
let display_list = get_display_list_for_diagnostic(&annotation_label, diagnostic, false);
writeln!(f, "{}", display_list)
}
fn diagnostic_stderr(diagnostic: &Diagnostic) {
eprint!("{}", diagnostic.call_stack);
let annotation_label = format!("{:#}", diagnostic.message);
let display_list = get_display_list_for_diagnostic(&annotation_label, diagnostic, true);
eprintln!("{}", display_list);
}
#[cfg(test)]
mod tests {
use crate::errors::truncate_snippet;
#[test]
fn test_truncate_snippet() {
assert_eq!(("", ""), truncate_snippet("", 5));
assert_eq!(("a", ""), truncate_snippet("a", 5));
assert_eq!(("ab", ""), truncate_snippet("ab", 5));
assert_eq!(("abc", ""), truncate_snippet("abc", 5));
assert_eq!(("abcd", ""), truncate_snippet("abcd", 5));
assert_eq!(("abcde", ""), truncate_snippet("abcde", 5));
assert_eq!(("ab", "..."), truncate_snippet("abcdef", 5));
assert_eq!(("ab", "..."), truncate_snippet("abcdefg", 5));
assert_eq!(("ab", "..."), truncate_snippet("abcdefgh", 5));
assert_eq!(("ab", "..."), truncate_snippet("abcdefghi", 5));
assert_eq!(("Київ", ""), truncate_snippet("Київ", 5));
assert_eq!(("па", "..."), truncate_snippet("паляниця", 5));
}
}