cairo_lang_diagnostics/
diagnostics.rs

1use std::fmt;
2use std::hash::Hash;
3use std::sync::Arc;
4
5use cairo_lang_debug::debug::DebugWithDb;
6use cairo_lang_filesystem::db::{FilesGroup, get_originating_location};
7use cairo_lang_filesystem::ids::FileId;
8use cairo_lang_filesystem::span::TextSpan;
9use cairo_lang_utils::Upcast;
10use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
11use itertools::Itertools;
12
13use crate::error_code::{ErrorCode, OptionErrorCodeExt};
14use crate::location_marks::get_location_marks;
15
16#[cfg(test)]
17#[path = "diagnostics_test.rs"]
18mod test;
19
20/// The severity of a diagnostic.
21#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Copy, Debug)]
22pub enum Severity {
23    Error,
24    Warning,
25}
26impl fmt::Display for Severity {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Severity::Error => write!(f, "error"),
30            Severity::Warning => write!(f, "warning"),
31        }
32    }
33}
34
35/// A trait for diagnostics (i.e., errors and warnings) across the compiler.
36/// Meant to be implemented by each module that may produce diagnostics.
37pub trait DiagnosticEntry: Clone + fmt::Debug + Eq + Hash {
38    type DbType: Upcast<dyn FilesGroup> + ?Sized;
39    fn format(&self, db: &Self::DbType) -> String;
40    fn location(&self, db: &Self::DbType) -> DiagnosticLocation;
41    fn notes(&self, _db: &Self::DbType) -> &[DiagnosticNote] {
42        &[]
43    }
44    fn severity(&self) -> Severity {
45        Severity::Error
46    }
47    fn error_code(&self) -> Option<ErrorCode> {
48        None
49    }
50    /// Returns true if the two should be regarded as the same kind when filtering duplicate
51    /// diagnostics.
52    fn is_same_kind(&self, other: &Self) -> bool;
53
54    // TODO(spapini): Add a way to inspect the diagnostic programmatically, e.g, downcast.
55}
56
57/// Diagnostic notes for diagnostics originating in the plugin generated files identified by
58/// [`FileId`].
59pub type PluginFileDiagnosticNotes = OrderedHashMap<FileId, DiagnosticNote>;
60
61// The representation of a source location inside a diagnostic.
62#[derive(Clone, Debug, Eq, Hash, PartialEq)]
63pub struct DiagnosticLocation {
64    pub file_id: FileId,
65    pub span: TextSpan,
66}
67impl DiagnosticLocation {
68    /// Get the location of right after this diagnostic's location (with width 0).
69    pub fn after(&self) -> Self {
70        Self { file_id: self.file_id, span: self.span.after() }
71    }
72
73    /// Get the location of the originating user code.
74    pub fn user_location(&self, db: &dyn FilesGroup) -> Self {
75        let (file_id, span) = get_originating_location(db, self.file_id, self.span, None);
76        Self { file_id, span }
77    }
78
79    /// Get the location of the originating user code,
80    /// along with [`DiagnosticNote`]s for this translation.
81    /// The notes are collected from the parent files of the originating location.
82    pub fn user_location_with_plugin_notes(
83        &self,
84        db: &dyn FilesGroup,
85        file_notes: &PluginFileDiagnosticNotes,
86    ) -> (Self, Vec<DiagnosticNote>) {
87        let mut parent_files = Vec::new();
88        let (file_id, span) =
89            get_originating_location(db, self.file_id, self.span, Some(&mut parent_files));
90        let diagnostic_notes = parent_files
91            .into_iter()
92            .rev()
93            .filter_map(|file_id| file_notes.get(&file_id).cloned())
94            .collect_vec();
95        (Self { file_id, span }, diagnostic_notes)
96    }
97
98    /// Helper function to format the location of a diagnostic.
99    pub fn fmt_location(&self, f: &mut fmt::Formatter<'_>, db: &dyn FilesGroup) -> fmt::Result {
100        let file_path = self.file_id.full_path(db);
101        let start = match self.span.start.position_in_file(db, self.file_id) {
102            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
103            None => "?".into(),
104        };
105
106        let end = match self.span.end.position_in_file(db, self.file_id) {
107            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
108            None => "?".into(),
109        };
110        write!(f, "{file_path}:{start}: {end}")
111    }
112}
113
114impl DebugWithDb<dyn FilesGroup> for DiagnosticLocation {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &dyn FilesGroup) -> fmt::Result {
116        let file_path = self.file_id.full_path(db);
117        let mut marks = String::new();
118        let mut ending_pos = String::new();
119        let starting_pos = match self.span.start.position_in_file(db, self.file_id) {
120            Some(starting_text_pos) => {
121                if let Some(ending_text_pos) = self.span.end.position_in_file(db, self.file_id) {
122                    if starting_text_pos.line != ending_text_pos.line {
123                        ending_pos =
124                            format!("-{}:{}", ending_text_pos.line + 1, ending_text_pos.col);
125                    }
126                }
127                marks = get_location_marks(db, self, true);
128                format!("{}:{}", starting_text_pos.line + 1, starting_text_pos.col + 1)
129            }
130            None => "?".into(),
131        };
132        write!(f, "{file_path}:{starting_pos}{ending_pos}\n{marks}")
133    }
134}
135
136/// A note about a diagnostic.
137/// May include a relevant diagnostic location.
138#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub struct DiagnosticNote {
140    pub text: String,
141    pub location: Option<DiagnosticLocation>,
142}
143impl DiagnosticNote {
144    pub fn text_only(text: String) -> Self {
145        Self { text, location: None }
146    }
147
148    pub fn with_location(text: String, location: DiagnosticLocation) -> Self {
149        Self { text, location: Some(location) }
150    }
151}
152
153impl DebugWithDb<dyn FilesGroup> for DiagnosticNote {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &(dyn FilesGroup + 'static)) -> fmt::Result {
155        write!(f, "{}", self.text)?;
156        if let Some(location) = &self.location {
157            write!(f, ":\n  --> ")?;
158            location.user_location(db).fmt(f, db)?;
159        }
160        Ok(())
161    }
162}
163
164/// This struct is used to ensure that when an error occurs, a diagnostic is properly reported.
165///
166/// It must not be constructed directly. Instead, it is returned by [DiagnosticsBuilder::add]
167/// when a diagnostic is reported.
168#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
169pub struct DiagnosticAdded;
170
171pub fn skip_diagnostic() -> DiagnosticAdded {
172    // TODO(lior): Consider adding a log here.
173    DiagnosticAdded
174}
175
176/// Represents an arbitrary type T or a missing output due to an error whose diagnostic was properly
177/// reported.
178pub type Maybe<T> = Result<T, DiagnosticAdded>;
179
180/// Temporary trait to allow conversions from the old `Option<T>` mechanism to `Maybe<T>`.
181// TODO(lior): Remove this trait after converting all the functions.
182pub trait ToMaybe<T> {
183    fn to_maybe(self) -> Maybe<T>;
184}
185impl<T> ToMaybe<T> for Option<T> {
186    fn to_maybe(self) -> Maybe<T> {
187        match self {
188            Some(val) => Ok(val),
189            None => Err(skip_diagnostic()),
190        }
191    }
192}
193
194/// Temporary trait to allow conversions from `Maybe<T>` to `Option<T>`.
195///
196/// The behavior is identical to [Result::ok]. It is used to mark all the location where there
197/// is a conversion between the two mechanisms.
198// TODO(lior): Remove this trait after converting all the functions.
199pub trait ToOption<T> {
200    fn to_option(self) -> Option<T>;
201}
202impl<T> ToOption<T> for Maybe<T> {
203    fn to_option(self) -> Option<T> {
204        self.ok()
205    }
206}
207
208/// A builder for Diagnostics, accumulating multiple diagnostic entries.
209#[derive(Clone, Debug, Eq, Hash, PartialEq)]
210pub struct DiagnosticsBuilder<TEntry: DiagnosticEntry> {
211    pub error_count: usize,
212    pub leaves: Vec<TEntry>,
213    pub subtrees: Vec<Diagnostics<TEntry>>,
214}
215impl<TEntry: DiagnosticEntry> DiagnosticsBuilder<TEntry> {
216    pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
217        if diagnostic.severity() == Severity::Error {
218            self.error_count += 1;
219        }
220        self.leaves.push(diagnostic);
221        DiagnosticAdded
222    }
223    pub fn extend(&mut self, diagnostics: Diagnostics<TEntry>) {
224        self.error_count += diagnostics.0.error_count;
225        self.subtrees.push(diagnostics);
226    }
227    pub fn build(self) -> Diagnostics<TEntry> {
228        Diagnostics(self.into())
229    }
230}
231impl<TEntry: DiagnosticEntry> From<Diagnostics<TEntry>> for DiagnosticsBuilder<TEntry> {
232    fn from(diagnostics: Diagnostics<TEntry>) -> Self {
233        let mut new_self = Self::default();
234        new_self.extend(diagnostics);
235        new_self
236    }
237}
238impl<TEntry: DiagnosticEntry> Default for DiagnosticsBuilder<TEntry> {
239    fn default() -> Self {
240        Self { leaves: Default::default(), subtrees: Default::default(), error_count: 0 }
241    }
242}
243
244pub fn format_diagnostics(
245    db: &(dyn FilesGroup + 'static),
246    message: &str,
247    location: DiagnosticLocation,
248) -> String {
249    format!("{message}\n --> {:?}\n", location.debug(db))
250}
251
252#[derive(Debug)]
253pub struct FormattedDiagnosticEntry {
254    severity: Severity,
255    error_code: Option<ErrorCode>,
256    message: String,
257}
258
259impl FormattedDiagnosticEntry {
260    pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
261        Self { severity, error_code, message }
262    }
263
264    pub fn is_empty(&self) -> bool {
265        self.message().is_empty()
266    }
267
268    pub fn severity(&self) -> Severity {
269        self.severity
270    }
271
272    pub fn error_code(&self) -> Option<ErrorCode> {
273        self.error_code
274    }
275
276    pub fn message(&self) -> &str {
277        &self.message
278    }
279}
280
281impl fmt::Display for FormattedDiagnosticEntry {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write!(
284            f,
285            "{severity}{code}: {message}",
286            severity = self.severity,
287            message = self.message,
288            code = self.error_code.display_bracketed()
289        )
290    }
291}
292
293/// A set of diagnostic entries that arose during a computation.
294#[derive(Clone, Debug, Eq, Hash, PartialEq)]
295pub struct Diagnostics<TEntry: DiagnosticEntry>(pub Arc<DiagnosticsBuilder<TEntry>>);
296impl<TEntry: DiagnosticEntry> Diagnostics<TEntry> {
297    pub fn new() -> Self {
298        Self(DiagnosticsBuilder::default().into())
299    }
300
301    /// Returns Ok if there are no errors, or DiagnosticAdded if there are.
302    pub fn check_error_free(&self) -> Maybe<()> {
303        if self.0.error_count == 0 { Ok(()) } else { Err(DiagnosticAdded) }
304    }
305
306    /// Checks if there are no entries inside `Diagnostics`
307    pub fn is_empty(&self) -> bool {
308        self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
309    }
310
311    /// Format entries to pairs of severity and message.
312    pub fn format_with_severity(
313        &self,
314        db: &TEntry::DbType,
315        file_notes: &OrderedHashMap<FileId, DiagnosticNote>,
316    ) -> Vec<FormattedDiagnosticEntry> {
317        let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
318
319        let files_db = db.upcast();
320        for entry in &self.get_diagnostics_without_duplicates(db) {
321            let mut msg = String::new();
322            let diag_location = entry.location(db);
323            let (user_location, parent_file_notes) =
324                diag_location.user_location_with_plugin_notes(files_db, file_notes);
325
326            let include_generated_location = diag_location != user_location
327                && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
328            msg += &format_diagnostics(files_db, &entry.format(db), user_location);
329
330            if include_generated_location {
331                msg += &format!(
332                    "note: The error originates from the generated code in {:?}\n",
333                    diag_location.debug(files_db)
334                );
335            }
336
337            for note in entry.notes(db) {
338                msg += &format!("note: {:?}\n", note.debug(files_db))
339            }
340            for note in parent_file_notes {
341                msg += &format!("note: {:?}\n", note.debug(files_db))
342            }
343            msg += "\n";
344
345            let formatted =
346                FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
347            res.push(formatted);
348        }
349        res
350    }
351
352    /// Format entries to a [`String`] with messages prefixed by severity.
353    pub fn format(&self, db: &TEntry::DbType) -> String {
354        self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
355    }
356
357    /// Asserts that no diagnostic has occurred, panicking with an error message on failure.
358    pub fn expect(&self, error_message: &str) {
359        assert!(self.is_empty(), "{error_message}\n{self:?}");
360    }
361
362    /// Same as [Self::expect], except that the diagnostics are formatted.
363    pub fn expect_with_db(&self, db: &TEntry::DbType, error_message: &str) {
364        assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
365    }
366
367    // TODO(spapini): This is temporary. Remove once the logic in language server doesn't use this.
368    /// Get all diagnostics.
369    pub fn get_all(&self) -> Vec<TEntry> {
370        let mut res = self.0.leaves.clone();
371        for subtree in &self.0.subtrees {
372            res.extend(subtree.get_all())
373        }
374        res
375    }
376
377    /// Get diagnostics without duplication.
378    ///
379    /// Two diagnostics are considered duplicated if both point to
380    /// the same location in the user code, and are of the same kind.
381    pub fn get_diagnostics_without_duplicates(&self, db: &TEntry::DbType) -> Vec<TEntry> {
382        let diagnostic_with_dup = self.get_all();
383        if diagnostic_with_dup.is_empty() {
384            return diagnostic_with_dup;
385        }
386        let files_db = db.upcast();
387        let mut indexed_dup_diagnostic =
388            diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
389                (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
390            });
391        let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
392        let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
393
394        for diag in indexed_dup_diagnostic {
395            if prev_diagnostic_indexed.1.is_same_kind(diag.1)
396                && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
397                    == diag.1.location(db).user_location(files_db).span
398            {
399                continue;
400            }
401            diagnostic_without_dup.push(diag);
402            prev_diagnostic_indexed = diag;
403        }
404        diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
405        diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
406    }
407}
408impl<TEntry: DiagnosticEntry> Default for Diagnostics<TEntry> {
409    fn default() -> Self {
410        Self::new()
411    }
412}
413impl<TEntry: DiagnosticEntry> FromIterator<TEntry> for Diagnostics<TEntry> {
414    fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
415        let mut builder = DiagnosticsBuilder::<TEntry>::default();
416        for diag in diags_iter {
417            builder.add(diag);
418        }
419        builder.build()
420    }
421}