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::get_originating_location;
7use cairo_lang_filesystem::ids::FileId;
8use cairo_lang_filesystem::span::TextSpan;
9use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
10use itertools::Itertools;
11use salsa::Database;
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<'db>: Clone + fmt::Debug + Eq + Hash {
38    fn format(&self, db: &'db dyn Database) -> String;
39    fn location(&self, db: &'db dyn Database) -> DiagnosticLocation<'db>;
40    fn notes(&self, _db: &'db dyn Database) -> &[DiagnosticNote<'_>] {
41        &[]
42    }
43    fn severity(&self) -> Severity {
44        Severity::Error
45    }
46    fn error_code(&self) -> Option<ErrorCode> {
47        None
48    }
49    /// Returns true if the two should be regarded as the same kind when filtering duplicate
50    /// diagnostics.
51    fn is_same_kind(&self, other: &Self) -> bool;
52
53    // TODO(spapini): Add a way to inspect the diagnostic programmatically, e.g, downcast.
54}
55
56/// Diagnostic notes for diagnostics originating in the plugin generated files identified by
57/// [`FileId`].
58pub type PluginFileDiagnosticNotes<'a> = OrderedHashMap<FileId<'a>, DiagnosticNote<'a>>;
59
60// The representation of a source location inside a diagnostic.
61#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
62pub struct DiagnosticLocation<'a> {
63    pub file_id: FileId<'a>,
64    pub span: TextSpan,
65}
66impl<'a> DiagnosticLocation<'a> {
67    /// Get the location of right after this diagnostic's location (with width 0).
68    pub fn after(&self) -> Self {
69        Self { file_id: self.file_id, span: self.span.after() }
70    }
71
72    /// Get the location of the originating user code.
73    pub fn user_location(&self, db: &'a dyn Database) -> Self {
74        let (file_id, span) = get_originating_location(db, self.file_id, self.span, None);
75        Self { file_id, span }
76    }
77
78    /// Get the location of the originating user code,
79    /// along with [`DiagnosticNote`]s for this translation.
80    /// The notes are collected from the parent files of the originating location.
81    pub fn user_location_with_plugin_notes(
82        &self,
83        db: &'a dyn Database,
84        file_notes: &PluginFileDiagnosticNotes<'a>,
85    ) -> (Self, Vec<DiagnosticNote<'_>>) {
86        let mut parent_files = Vec::new();
87        let (file_id, span) =
88            get_originating_location(db, self.file_id, self.span, Some(&mut parent_files));
89        let diagnostic_notes = parent_files
90            .into_iter()
91            .rev()
92            .filter_map(|file_id| file_notes.get(&file_id).cloned())
93            .collect_vec();
94        (Self { file_id, span }, diagnostic_notes)
95    }
96
97    /// Helper function to format the location of a diagnostic.
98    pub fn fmt_location(&self, f: &mut fmt::Formatter<'_>, db: &dyn Database) -> fmt::Result {
99        let file_path = self.file_id.long(db).full_path(db);
100        let start = match self.span.start.position_in_file(db, self.file_id) {
101            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
102            None => "?".into(),
103        };
104
105        let end = match self.span.end.position_in_file(db, self.file_id) {
106            Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
107            None => "?".into(),
108        };
109        write!(f, "{file_path}:{start}: {end}")
110    }
111}
112
113impl<'a> DebugWithDb<'a> for DiagnosticLocation<'a> {
114    type Db = dyn Database;
115    fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &'a dyn Database) -> fmt::Result {
116        let file_path = self.file_id.long(db).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                    && starting_text_pos.line != ending_text_pos.line
123                {
124                    ending_pos = format!("-{}:{}", ending_text_pos.line + 1, ending_text_pos.col);
125                }
126                marks = get_location_marks(db, self, true);
127                format!("{}:{}", starting_text_pos.line + 1, starting_text_pos.col + 1)
128            }
129            None => "?".into(),
130        };
131        write!(f, "{file_path}:{starting_pos}{ending_pos}\n{marks}")
132    }
133}
134
135/// A note about a diagnostic.
136/// May include a relevant diagnostic location.
137#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
138pub struct DiagnosticNote<'a> {
139    pub text: String,
140    pub location: Option<DiagnosticLocation<'a>>,
141}
142impl<'a> DiagnosticNote<'a> {
143    pub fn text_only(text: String) -> Self {
144        Self { text, location: None }
145    }
146
147    pub fn with_location(text: String, location: DiagnosticLocation<'a>) -> Self {
148        Self { text, location: Some(location) }
149    }
150}
151
152impl<'a> DebugWithDb<'a> for DiagnosticNote<'a> {
153    type Db = dyn Database;
154    fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &'a dyn Database) -> 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, salsa::Update)]
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/// Trait to convert a `Maybe<T>` to a `Maybe<&T>`.
181pub trait MaybeAsRef<T> {
182    fn maybe_as_ref(&self) -> Maybe<&T>;
183}
184
185impl<T> MaybeAsRef<T> for Maybe<T> {
186    fn maybe_as_ref(&self) -> Maybe<&T> {
187        self.as_ref().map_err(|e| *e)
188    }
189}
190
191/// Temporary trait to allow conversions from the old `Option<T>` mechanism to `Maybe<T>`.
192// TODO(lior): Remove this trait after converting all the functions.
193pub trait ToMaybe<T> {
194    fn to_maybe(self) -> Maybe<T>;
195}
196impl<T> ToMaybe<T> for Option<T> {
197    fn to_maybe(self) -> Maybe<T> {
198        match self {
199            Some(val) => Ok(val),
200            None => Err(skip_diagnostic()),
201        }
202    }
203}
204
205/// Temporary trait to allow conversions from `Maybe<T>` to `Option<T>`.
206///
207/// The behavior is identical to [Result::ok]. It is used to mark all the locations where there
208/// is a conversion between the two mechanisms.
209// TODO(lior): Remove this trait after converting all the functions.
210pub trait ToOption<T> {
211    fn to_option(self) -> Option<T>;
212}
213impl<T> ToOption<T> for Maybe<T> {
214    fn to_option(self) -> Option<T> {
215        self.ok()
216    }
217}
218
219/// A builder for Diagnostics, accumulating multiple diagnostic entries.
220#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
221pub struct DiagnosticsBuilder<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> {
222    pub error_count: usize,
223    pub leaves: Vec<TEntry>,
224    pub subtrees: Vec<Diagnostics<'db, TEntry>>,
225    _marker: std::marker::PhantomData<&'db ()>,
226}
227impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> DiagnosticsBuilder<'db, TEntry> {
228    pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
229        if diagnostic.severity() == Severity::Error {
230            self.error_count += 1;
231        }
232        self.leaves.push(diagnostic);
233        DiagnosticAdded
234    }
235    pub fn extend(&mut self, diagnostics: Diagnostics<'db, TEntry>) {
236        self.error_count += diagnostics.0.error_count;
237        self.subtrees.push(diagnostics);
238    }
239    pub fn build(self) -> Diagnostics<'db, TEntry> {
240        Diagnostics(self.into())
241    }
242}
243impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> From<Diagnostics<'db, TEntry>>
244    for DiagnosticsBuilder<'db, TEntry>
245{
246    fn from(diagnostics: Diagnostics<'db, TEntry>) -> Self {
247        let mut new_self = Self::default();
248        new_self.extend(diagnostics);
249        new_self
250    }
251}
252impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default
253    for DiagnosticsBuilder<'db, TEntry>
254{
255    fn default() -> Self {
256        Self {
257            leaves: Default::default(),
258            subtrees: Default::default(),
259            error_count: 0,
260            _marker: Default::default(),
261        }
262    }
263}
264
265pub fn format_diagnostics(
266    db: &dyn Database,
267    message: &str,
268    location: DiagnosticLocation<'_>,
269) -> String {
270    format!("{message}\n --> {:?}\n", location.debug(db))
271}
272
273#[derive(Debug)]
274pub struct FormattedDiagnosticEntry {
275    severity: Severity,
276    error_code: Option<ErrorCode>,
277    message: String,
278}
279
280impl FormattedDiagnosticEntry {
281    pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
282        Self { severity, error_code, message }
283    }
284
285    pub fn is_empty(&self) -> bool {
286        self.message().is_empty()
287    }
288
289    pub fn severity(&self) -> Severity {
290        self.severity
291    }
292
293    pub fn error_code(&self) -> Option<ErrorCode> {
294        self.error_code
295    }
296
297    pub fn message(&self) -> &str {
298        &self.message
299    }
300}
301
302impl fmt::Display for FormattedDiagnosticEntry {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(
305            f,
306            "{severity}{code}: {message}",
307            severity = self.severity,
308            message = self.message,
309            code = self.error_code.display_bracketed()
310        )
311    }
312}
313
314/// A set of diagnostic entries that arose during a computation.
315#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
316pub struct Diagnostics<'db, TEntry: DiagnosticEntry<'db> + salsa::Update>(
317    pub Arc<DiagnosticsBuilder<'db, TEntry>>,
318);
319impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Diagnostics<'db, TEntry> {
320    pub fn new() -> Self {
321        Self(DiagnosticsBuilder::default().into())
322    }
323
324    /// Returns `true` if there are errors, or `false` otherwise.
325    pub fn has_errors(&self) -> bool {
326        self.0.error_count > 0
327    }
328
329    /// Returns `Ok` if there are no errors, or `DiagnosticAdded` if there are.
330    pub fn check_error_free(&self) -> Maybe<()> {
331        if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
332    }
333
334    /// Checks if there are no entries inside `Diagnostics`
335    pub fn is_empty(&self) -> bool {
336        self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
337    }
338
339    /// Format entries to pairs of severity and message.
340    pub fn format_with_severity(
341        &self,
342        db: &'db dyn Database,
343        file_notes: &OrderedHashMap<FileId<'db>, DiagnosticNote<'db>>,
344    ) -> Vec<FormattedDiagnosticEntry> {
345        let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
346
347        let files_db: &'db dyn Database = db;
348        for entry in &self.get_diagnostics_without_duplicates(db) {
349            let mut msg = String::new();
350            let diag_location = entry.location(db);
351            let (user_location, parent_file_notes) =
352                diag_location.user_location_with_plugin_notes(files_db, file_notes);
353
354            let include_generated_location = diag_location != user_location
355                && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
356            msg += &format_diagnostics(files_db, &entry.format(db), user_location);
357
358            if include_generated_location {
359                msg += &format!(
360                    "note: The error originates from the generated code in {:?}\n",
361                    diag_location.debug(files_db)
362                );
363            }
364
365            for note in entry.notes(db) {
366                msg += &format!("note: {:?}\n", note.debug(files_db))
367            }
368            for note in parent_file_notes {
369                msg += &format!("note: {:?}\n", note.debug(files_db))
370            }
371            msg += "\n";
372
373            let formatted =
374                FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
375            res.push(formatted);
376        }
377        res
378    }
379
380    /// Format entries to a [`String`] with messages prefixed by severity.
381    pub fn format(&self, db: &'db dyn Database) -> String {
382        self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
383    }
384
385    /// Asserts that no diagnostic has occurred, panicking with an error message on failure.
386    pub fn expect(&self, error_message: &str) {
387        assert!(self.is_empty(), "{error_message}\n{self:?}");
388    }
389
390    /// Same as [Self::expect], except that the diagnostics are formatted.
391    pub fn expect_with_db(&self, db: &'db dyn Database, error_message: &str) {
392        assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
393    }
394
395    // TODO(spapini): This is temporary. Remove once the logic in language server doesn't use this.
396    /// Get all diagnostics.
397    pub fn get_all(&self) -> Vec<TEntry> {
398        let mut res = self.0.leaves.clone();
399        for subtree in &self.0.subtrees {
400            res.extend(subtree.get_all())
401        }
402        res
403    }
404
405    /// Get diagnostics without duplication.
406    ///
407    /// Two diagnostics are considered duplicated if both point to
408    /// the same location in the user code, and are of the same kind.
409    pub fn get_diagnostics_without_duplicates(&self, db: &'db dyn Database) -> Vec<TEntry> {
410        let diagnostic_with_dup = self.get_all();
411        if diagnostic_with_dup.is_empty() {
412            return diagnostic_with_dup;
413        }
414        let files_db: &'db dyn Database = db;
415        let mut indexed_dup_diagnostic =
416            diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
417                (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
418            });
419        let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
420        let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
421
422        for diag in indexed_dup_diagnostic {
423            if prev_diagnostic_indexed.1.is_same_kind(diag.1)
424                && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
425                    == diag.1.location(db).user_location(files_db).span
426            {
427                continue;
428            }
429            diagnostic_without_dup.push(diag);
430            prev_diagnostic_indexed = diag;
431        }
432        diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
433        diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
434    }
435
436    /// Merges two sets of diagnostics.
437    pub fn merge(self, other: Self) -> Self {
438        let mut builder = DiagnosticsBuilder::default();
439        builder.extend(self);
440        builder.extend(other);
441        builder.build()
442    }
443}
444impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default for Diagnostics<'db, TEntry> {
445    fn default() -> Self {
446        Self::new()
447    }
448}
449impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> FromIterator<TEntry>
450    for Diagnostics<'db, TEntry>
451{
452    fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
453        let mut builder = DiagnosticsBuilder::<'db, TEntry>::default();
454        for diag in diags_iter {
455            builder.add(diag);
456        }
457        builder.build()
458    }
459}