#[cfg(test)]
#[path = "diagnostics_test.rs"]
mod test;
use std::sync::Arc;
use cairo_lang_debug::debug::DebugWithDb;
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;
fn notes(&self, _db: &Self::DbType) -> &[DiagnosticNote] {
&[]
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
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() }
}
}
impl DebugWithDb<dyn FilesGroup> for DiagnosticLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>, db: &dyn FilesGroup) -> std::fmt::Result {
let file_path = self.file_id.full_path(db);
let marks = get_location_marks(db, self);
let pos = match self.span.start.position_in_file(db, self.file_id) {
Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
None => "?".into(),
};
write!(f, "{file_path}:{pos}\n{marks}")
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DiagnosticNote {
pub text: String,
pub location: Option<DiagnosticLocation>,
}
impl DiagnosticNote {
pub fn text_only(text: String) -> Self {
Self { text, location: None }
}
pub fn with_location(text: String, location: DiagnosticLocation) -> Self {
Self { text, location: Some(location) }
}
}
impl DebugWithDb<dyn FilesGroup> for DiagnosticNote {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
db: &(dyn FilesGroup + 'static),
) -> std::fmt::Result {
write!(f, "{}", self.text)?;
if let Some(location) = &self.location {
write!(f, ":\n --> ")?;
location.fmt(f, db)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct DiagnosticAdded;
pub fn skip_diagnostic() -> DiagnosticAdded {
DiagnosticAdded
}
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
}
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 + 'static),
message: &str,
location: DiagnosticLocation,
) -> String {
format!("error: {message}\n --> {:?}\n", location.debug(db))
}
#[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();
let files_db = db.upcast();
for entry in &self.0.leaves {
let message = entry.format(db);
res += &format_diagnostics(files_db, &message, entry.location(db));
for note in entry.notes(db) {
res += &format!("note: {:?}\n", note.debug(files_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()
}
}