Skip to main content

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