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