use alloc::{boxed::Box, vec::Vec};
use core::fmt;
use diag_lang::{Code, Diagnostic, Label, Severity, SourceMap};
use syntax_lang::Span;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
diagnostics: Vec<Diagnostic>,
line: u32,
column: u32,
path: Option<Box<str>>,
}
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,
path: None,
}
}
pub(crate) fn in_map(diagnostics: Vec<Diagnostic>, map: &SourceMap) -> Self {
let mut error = Self {
diagnostics,
line: 1,
column: 1,
path: None,
};
let at = error
.diagnostics
.first()
.map(|d| d.primary().span().start());
if let Some((id, local)) = at.and_then(|at| map.locate(at)) {
if let Some(file) = map.source(id) {
let (line, column) = line_col(file.text(), local.to_usize());
error.line = line;
error.column = column;
error.path = Some(Box::from(file.name()));
}
}
error
}
pub(crate) fn located(diagnostics: Vec<Diagnostic>, line: u32, column: u32) -> Self {
Self {
diagnostics,
line,
column,
path: None,
}
}
#[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());
if let Some(path) = &self.path {
write!(f, "{path}:")?;
}
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)
}
pub(crate) fn coded(code: Code, span: Span, message: impl Into<Box<str>>) -> Diagnostic {
Diagnostic::new(Severity::Error, message, Label::unlabelled(span)).with_code(code)
}
#[derive(Debug)]
pub(crate) struct Report {
diagnostics: Vec<Diagnostic>,
errors: usize,
budget: u64,
}
impl Default for Report {
fn default() -> Self {
Self {
diagnostics: Vec::new(),
errors: 0,
budget: crate::suggest::BUDGET,
}
}
}
impl Report {
pub(crate) fn suggest<'a>(
&mut self,
name: &str,
candidates: impl Iterator<Item = &'a str>,
limit: usize,
) -> Option<alloc::string::String> {
crate::suggest::suggest_with(name, candidates, limit, &mut self.budget)
}
pub(crate) fn error(&mut self, code: Code, span: Span, message: impl Into<Box<str>>) {
self.diagnostic(coded(code, span, message));
}
pub(crate) fn error_help(
&mut self,
code: Code,
span: Span,
message: impl Into<Box<str>>,
help: impl Into<Box<str>>,
) {
self.diagnostic(coded(code, span, message).with_help(help));
}
pub(crate) fn warning(&mut self, code: Code, span: Span, message: impl Into<Box<str>>) {
self.diagnostic(
Diagnostic::new(Severity::Warning, message, Label::unlabelled(span)).with_code(code),
);
}
pub(crate) fn diagnostic(&mut self, diagnostic: Diagnostic) {
if diagnostic.severity() == Severity::Error {
self.errors += 1;
}
self.diagnostics.push(diagnostic);
}
pub(crate) fn is_clean(&self) -> bool {
self.errors == 0
}
pub(crate) fn errors(&self) -> usize {
self.errors
}
fn sort(&mut self) {
self.diagnostics
.sort_by_key(|d| d.primary().span().start().to_u32());
}
pub(crate) fn into_error(mut self, text: &str) -> Error {
self.sort();
Error::new(self.diagnostics, text)
}
pub(crate) fn into_error_v2(self, text: &str) -> Error {
let diagnostics = sorted_for_sketch(self.diagnostics, &|_| 0);
Error::new(diagnostics, text)
}
pub(crate) fn into_warnings_v2(self) -> Vec<Diagnostic> {
sorted_for_sketch(self.diagnostics, &|_| 0)
}
pub(crate) fn into_sketch_error(self, map: &SourceMap, rank: &dyn Fn(Span) -> u32) -> Error {
let diagnostics = sorted_for_sketch(self.diagnostics, rank);
Error::in_map(diagnostics, map)
}
pub(crate) fn into_warnings(mut self) -> Vec<Diagnostic> {
self.sort();
self.diagnostics
}
pub(crate) fn into_sketch_warnings(self, rank: &dyn Fn(Span) -> u32) -> Vec<Diagnostic> {
sorted_for_sketch(self.diagnostics, rank)
}
}
fn sorted_for_sketch(
mut diagnostics: Vec<Diagnostic>,
rank: &dyn Fn(Span) -> u32,
) -> Vec<Diagnostic> {
diagnostics.sort_by(|a, b| {
let (sa, sb) = (a.primary().span(), b.primary().span());
rank(sa)
.cmp(&rank(sb))
.then(sa.start().cmp(&sb.start()))
.then(sa.end().cmp(&sb.end()))
.then(a.code().cmp(&b.code()))
.then(a.message().cmp(b.message()))
});
diagnostics.dedup_by(|a, b| {
a.primary().span() == b.primary().span()
&& a.code() == b.code()
&& a.message() == b.message()
});
diagnostics
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use alloc::string::ToString;
use super::*;
use crate::codes;
#[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(codes::MISSING, Span::new(4, 5), "second");
report.error(codes::MISSING, 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(codes::MISSING, 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(codes::MISSING, 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"]);
assert_eq!(err.diagnostics()[0].code(), Some(codes::MISSING));
}
#[test]
fn test_report_warnings_keep_it_clean() {
let mut report = Report::default();
report.warning(codes::UNUSED_RULE, Span::new(0, 1), "unused");
assert!(report.is_clean());
assert_eq!(report.errors(), 0);
let warnings = report.into_warnings();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].severity(), Severity::Warning);
}
#[test]
fn test_sketch_errors_name_the_file_and_dedup() {
let mut map = SourceMap::new();
let _ = map.add("a.lsf", "x\n").expect("fits");
let _ = map.add("b.lsf", "yy\nzz\n").expect("fits");
let mut report = Report::default();
report.error(codes::MISSING, Span::new(6, 7), "late");
report.error(codes::MISSING, Span::new(6, 7), "late");
report.error(codes::MISSING, Span::new(1, 2), "early");
let err = report.into_sketch_error(&map, &|_| 0);
assert_eq!(err.diagnostics().len(), 2);
assert_eq!(err.to_string(), "a.lsf:1:2: early (and 1 more error)");
}
}