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