#[cfg(test)]
#[path = "diagnostics_test.rs"]
mod test;
use std::sync::Arc;
use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::FileId;
use cairo_lang_filesystem::span::TextSpan;
use cairo_lang_utils::Upcast;
use itertools::Itertools;
use crate::location_marks::get_location_marks;
pub trait DiagnosticEntry: Clone + std::fmt::Debug + Eq + std::hash::Hash {
type DbType: Upcast<dyn FilesGroup> + ?Sized;
fn format(&self, db: &Self::DbType) -> String;
fn location(&self, db: &Self::DbType) -> DiagnosticLocation;
}
pub struct DiagnosticLocation {
pub file_id: FileId,
pub span: TextSpan,
}
impl DiagnosticLocation {
pub fn after(&self) -> Self {
Self { file_id: self.file_id, span: self.span.after() }
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct DiagnosticAdded;
pub fn skip_diagnostic() -> DiagnosticAdded {
DiagnosticAdded::default()
}
pub type Maybe<T> = Result<T, DiagnosticAdded>;
pub trait ToMaybe<T> {
fn to_maybe(self) -> Maybe<T>;
}
impl<T> ToMaybe<T> for Option<T> {
fn to_maybe(self) -> Maybe<T> {
match self {
Some(val) => Ok(val),
None => Err(skip_diagnostic()),
}
}
}
pub trait ToOption<T> {
fn to_option(self) -> Option<T>;
}
impl<T> ToOption<T> for Maybe<T> {
fn to_option(self) -> Option<T> {
self.ok()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DiagnosticsBuilder<TEntry: DiagnosticEntry> {
pub count: usize,
pub leaves: Vec<TEntry>,
pub subtrees: Vec<Diagnostics<TEntry>>,
}
impl<TEntry: DiagnosticEntry> DiagnosticsBuilder<TEntry> {
pub fn new() -> Self {
Self { leaves: Default::default(), subtrees: Default::default(), count: 0 }
}
pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
self.leaves.push(diagnostic);
self.count += 1;
DiagnosticAdded::default()
}
pub fn extend(&mut self, diagnostics: Diagnostics<TEntry>) {
self.count += diagnostics.len();
self.subtrees.push(diagnostics);
}
pub fn build(self) -> Diagnostics<TEntry> {
Diagnostics(Arc::new(self))
}
}
impl<TEntry: DiagnosticEntry> Default for DiagnosticsBuilder<TEntry> {
fn default() -> Self {
Self::new()
}
}
pub fn format_diagnostics(
db: &dyn FilesGroup,
message: &str,
location: DiagnosticLocation,
) -> String {
let file_name = location.file_id.file_name(db);
let marks = get_location_marks(db, &location);
let pos = match location.span.start.position_in_file(db, location.file_id) {
Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
None => "?".into(),
};
format!("error: {message}\n --> {file_name}:{pos}\n{marks}\n")
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Diagnostics<TEntry: DiagnosticEntry>(pub Arc<DiagnosticsBuilder<TEntry>>);
impl<TEntry: DiagnosticEntry> Diagnostics<TEntry> {
pub fn new() -> Self {
Self(DiagnosticsBuilder::default().into())
}
pub fn len(&self) -> usize {
self.0.count
}
pub fn is_empty(&self) -> bool {
self.0.count == 0
}
pub fn is_diagnostic_free(&self) -> Maybe<()> {
if self.is_empty() { Ok(()) } else { Err(DiagnosticAdded) }
}
pub fn format(&self, db: &TEntry::DbType) -> String {
let mut res = String::new();
for entry in &self.0.leaves {
let message = entry.format(db);
res += &format_diagnostics(db.upcast(), &message, entry.location(db));
res += "\n";
}
res += &self.0.subtrees.iter().map(|subtree| subtree.format(db)).join("");
res
}
pub fn expect(&self, error_message: &str) {
assert!(self.0.leaves.is_empty(), "{error_message}\n{self:?}");
for subtree in &self.0.subtrees {
subtree.expect(error_message);
}
}
pub fn expect_with_db(&self, db: &TEntry::DbType, error_message: &str) {
assert!(self.0.leaves.is_empty(), "{}\n{}", error_message, self.format(db));
for subtree in &self.0.subtrees {
subtree.expect_with_db(db, error_message);
}
}
pub fn get_all(&self) -> Vec<TEntry> {
let mut res = self.0.leaves.clone();
for subtree in &self.0.subtrees {
res.extend(subtree.get_all())
}
res
}
}
impl<TEntry: DiagnosticEntry> Default for Diagnostics<TEntry> {
fn default() -> Self {
Self::new()
}
}