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