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::SalsaValue)]
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::SalsaValue)]
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::SalsaValue)]
171pub struct DiagnosticsBuilder<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> {
172    pub error_count: usize,
173    pub leaves: Vec<TEntry>,
174    /// SAFETY: the field is recursive in `Self`, so a structural `SalsaValue` bound would
175    /// overflow the trait solver; `Diagnostics<'db, TEntry>` is a `SalsaValue` whenever the
176    /// other fields' checks pass, as it is composed of the same `TEntry` values.
177    #[salsa_value(unsafe(prove_safe_to_retain_manually))]
178    pub subtrees: Vec<Diagnostics<'db, TEntry>>,
179    _marker: std::marker::PhantomData<&'db ()>,
180}
181impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> DiagnosticsBuilder<'db, TEntry> {
182    pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
183        if diagnostic.severity() == Severity::Error {
184            self.error_count += 1;
185        }
186        self.leaves.push(diagnostic);
187        DiagnosticAdded
188    }
189    pub fn extend(&mut self, diagnostics: Diagnostics<'db, TEntry>) {
190        self.error_count += diagnostics.0.error_count;
191        self.subtrees.push(diagnostics);
192    }
193    pub fn build(self) -> Diagnostics<'db, TEntry> {
194        Diagnostics(self.into())
195    }
196}
197impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> From<Diagnostics<'db, TEntry>>
198    for DiagnosticsBuilder<'db, TEntry>
199{
200    fn from(diagnostics: Diagnostics<'db, TEntry>) -> Self {
201        let mut new_self = Self::default();
202        new_self.extend(diagnostics);
203        new_self
204    }
205}
206impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Default
207    for DiagnosticsBuilder<'db, TEntry>
208{
209    fn default() -> Self {
210        Self {
211            leaves: Default::default(),
212            subtrees: Default::default(),
213            error_count: 0,
214            _marker: Default::default(),
215        }
216    }
217}
218
219pub fn format_diagnostics(db: &dyn Database, message: &str, location: SpanInFile<'_>) -> String {
220    format!("{message}\n --> {:?}\n", location.debug(db))
221}
222
223#[derive(Debug)]
224pub struct FormattedDiagnosticEntry {
225    severity: Severity,
226    error_code: Option<ErrorCode>,
227    message: String,
228}
229
230impl FormattedDiagnosticEntry {
231    pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
232        Self { severity, error_code, message }
233    }
234
235    pub fn is_empty(&self) -> bool {
236        self.message().is_empty()
237    }
238
239    pub fn severity(&self) -> Severity {
240        self.severity
241    }
242
243    pub fn error_code(&self) -> Option<ErrorCode> {
244        self.error_code
245    }
246
247    pub fn message(&self) -> &str {
248        &self.message
249    }
250}
251
252impl fmt::Display for FormattedDiagnosticEntry {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        write!(
255            f,
256            "{severity}{code}: {message}",
257            severity = self.severity,
258            message = self.message,
259            code = self.error_code.display_bracketed()
260        )
261    }
262}
263
264/// A set of diagnostic entries that arose during a computation.
265#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::SalsaValue)]
266pub struct Diagnostics<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue>(
267    pub Arc<DiagnosticsBuilder<'db, TEntry>>,
268);
269impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Diagnostics<'db, TEntry> {
270    pub fn new() -> Self {
271        Self(DiagnosticsBuilder::default().into())
272    }
273
274    /// Returns `true` if there are errors, or `false` otherwise.
275    pub fn has_errors(&self) -> bool {
276        self.0.error_count > 0
277    }
278
279    /// Returns `Ok` if there are no errors, or `DiagnosticAdded` if there are.
280    pub fn check_error_free(&self) -> Maybe<()> {
281        if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
282    }
283
284    /// Checks if there are no entries inside `Diagnostics`.
285    pub fn is_empty(&self) -> bool {
286        self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
287    }
288
289    /// Format entries to pairs of severity and message.
290    pub fn format_with_severity(
291        &self,
292        db: &'db dyn Database,
293        file_notes: &OrderedHashMap<FileId<'db>, DiagnosticNote<'db>>,
294    ) -> Vec<FormattedDiagnosticEntry> {
295        let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
296
297        let files_db: &'db dyn Database = db;
298        for entry in &self.get_diagnostics_without_duplicates(db) {
299            let mut msg = String::new();
300            let diag_location = entry.location(db);
301            let (user_location, parent_file_notes) =
302                diag_location.user_location_with_plugin_notes(files_db, file_notes);
303
304            let include_generated_location = diag_location != user_location
305                && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
306            msg += &format_diagnostics(files_db, &entry.format(db), user_location);
307
308            if include_generated_location {
309                msg += &format!(
310                    "note: The error originates from the generated code in {:?}\n",
311                    diag_location.debug(files_db)
312                );
313            }
314
315            for note in entry.notes(db) {
316                msg += &format!("note: {:?}\n", note.debug(files_db))
317            }
318            for note in parent_file_notes {
319                msg += &format!("note: {:?}\n", note.debug(files_db))
320            }
321            msg += "\n";
322
323            let formatted =
324                FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
325            res.push(formatted);
326        }
327        res
328    }
329
330    /// Format entries to a [`String`] with messages prefixed by severity.
331    pub fn format(&self, db: &'db dyn Database) -> String {
332        self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
333    }
334
335    /// Asserts that no diagnostic has occurred, panicking with an error message on failure.
336    pub fn expect(&self, error_message: &str) {
337        assert!(self.is_empty(), "{error_message}\n{self:?}");
338    }
339
340    /// Same as [Self::expect], except that the diagnostics are formatted.
341    pub fn expect_with_db(&self, db: &'db dyn Database, error_message: &str) {
342        assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
343    }
344
345    // TODO(spapini): This is temporary. Remove once the logic in language server doesn't use this.
346    /// Get all diagnostics.
347    pub fn get_all(&self) -> Vec<TEntry> {
348        let mut res = self.0.leaves.clone();
349        for subtree in &self.0.subtrees {
350            res.extend(subtree.get_all())
351        }
352        res
353    }
354
355    /// Get diagnostics without duplication.
356    ///
357    /// Two diagnostics are considered duplicated if both point to
358    /// the same location in the user code, and are of the same kind.
359    pub fn get_diagnostics_without_duplicates(&self, db: &'db dyn Database) -> Vec<TEntry> {
360        let diagnostic_with_dup = self.get_all();
361        if diagnostic_with_dup.is_empty() {
362            return diagnostic_with_dup;
363        }
364        let files_db: &'db dyn Database = db;
365        let mut indexed_dup_diagnostic =
366            diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
367                (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
368            });
369        let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
370        let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
371
372        for diag in indexed_dup_diagnostic {
373            if prev_diagnostic_indexed.1.is_same_kind(diag.1)
374                && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
375                    == diag.1.location(db).user_location(files_db).span
376            {
377                continue;
378            }
379            diagnostic_without_dup.push(diag);
380            prev_diagnostic_indexed = diag;
381        }
382        diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
383        diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
384    }
385
386    /// Merges two sets of diagnostics.
387    pub fn merge(self, other: Self) -> Self {
388        let mut builder = DiagnosticsBuilder::default();
389        builder.extend(self);
390        builder.extend(other);
391        builder.build()
392    }
393}
394impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Default for Diagnostics<'db, TEntry> {
395    fn default() -> Self {
396        Self::new()
397    }
398}
399impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> FromIterator<TEntry>
400    for Diagnostics<'db, TEntry>
401{
402    fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
403        let mut builder = DiagnosticsBuilder::<'db, TEntry>::default();
404        for diag in diags_iter {
405            builder.add(diag);
406        }
407        builder.build()
408    }
409}