use alloc::{boxed::Box, vec::Vec};
use core::fmt;
use diag_lang::{Diagnostic, Label, Severity};
use syntax_lang::Span;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
diagnostics: Vec<Diagnostic>,
line: u32,
column: u32,
}
impl Error {
pub(crate) fn new(diagnostics: Vec<Diagnostic>, text: &str) -> Self {
let offset = diagnostics
.first()
.map_or(0, |d| d.primary().span().start().to_usize());
let (line, column) = line_col(text, offset);
Self {
diagnostics,
line,
column,
}
}
pub(crate) fn located(diagnostics: Vec<Diagnostic>, line: u32, column: u32) -> Self {
Self {
diagnostics,
line,
column,
}
}
#[inline]
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = self.diagnostics.first().map_or("", |d| d.message());
write!(f, "{}:{}: {}", self.line, self.column, message)?;
match self.diagnostics.len() {
0 | 1 => Ok(()),
2 => f.write_str(" (and 1 more error)"),
n => write!(f, " (and {} more errors)", n - 1),
}
}
}
impl core::error::Error for Error {}
pub(crate) fn line_col(text: &str, offset: usize) -> (u32, u32) {
let mut offset = offset.min(text.len());
while !text.is_char_boundary(offset) {
offset -= 1;
}
let before = &text[..offset];
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let line = before.bytes().filter(|b| *b == b'\n').count() + 1;
let column = before[line_start..].chars().count() + 1;
(saturate(line), saturate(column))
}
fn saturate(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
#[derive(Debug, Default)]
pub(crate) struct Report {
diagnostics: Vec<Diagnostic>,
}
impl Report {
pub(crate) fn error(&mut self, span: Span, message: impl Into<Box<str>>) {
self.diagnostics.push(Diagnostic::new(
Severity::Error,
message,
Label::unlabelled(span),
));
}
pub(crate) fn error_help(
&mut self,
span: Span,
message: impl Into<Box<str>>,
help: impl Into<Box<str>>,
) {
self.diagnostics.push(
Diagnostic::new(Severity::Error, message, Label::unlabelled(span)).with_help(help),
);
}
pub(crate) fn diagnostic(&mut self, diagnostic: Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub(crate) fn is_clean(&self) -> bool {
self.diagnostics.is_empty()
}
pub(crate) fn into_error(mut self, text: &str) -> Error {
self.diagnostics
.sort_by_key(|d| d.primary().span().start().to_u32());
Error::new(self.diagnostics, text)
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn test_line_col_counts_characters_not_bytes() {
let text = "ab\nçd\n";
assert_eq!(line_col(text, 0), (1, 1));
assert_eq!(line_col(text, 3), (2, 1));
assert_eq!(line_col(text, 5), (2, 2));
assert_eq!(line_col(text, 999), (3, 1));
}
#[test]
fn test_line_col_inside_multibyte_char_does_not_panic() {
assert_eq!(line_col("ç", 1), (1, 1));
}
#[test]
fn test_display_counts_additional_errors() {
let mut report = Report::default();
report.error(Span::new(4, 5), "second");
report.error(Span::new(0, 1), "first");
let err = report.into_error("abcdef");
assert_eq!(err.to_string(), "1:1: first (and 1 more error)");
let mut report = Report::default();
for _ in 0..3 {
report.error(Span::new(0, 0), "same");
}
assert_eq!(
report.into_error("").to_string(),
"1:1: same (and 2 more errors)"
);
}
#[test]
fn test_report_help_is_attached() {
let mut report = Report::default();
report.error_help(Span::new(0, 1), "bad", "fix it");
assert!(!report.is_clean());
let err = report.into_error("x");
assert_eq!(err.diagnostics()[0].help().collect::<Vec<_>>(), ["fix it"]);
}
}