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#[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
34pub 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 fn is_same_kind(&self, other: &Self) -> bool;
51}
52
53pub type PluginFileDiagnosticNotes<'a> = OrderedHashMap<FileId<'a>, DiagnosticNote<'a>>;
56
57pub trait UserLocationWithPluginNotes<'db> {
59 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#[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, HeapSize, salsa::Update)]
119pub struct DiagnosticAdded;
120
121pub fn skip_diagnostic() -> DiagnosticAdded {
122 DiagnosticAdded
124}
125
126pub type Maybe<T> = Result<T, DiagnosticAdded>;
129
130pub 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
141pub 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
155pub 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#[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#[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 pub fn has_errors(&self) -> bool {
272 self.0.error_count > 0
273 }
274
275 pub fn check_error_free(&self) -> Maybe<()> {
277 if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
283 }
284
285 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 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 pub fn expect(&self, error_message: &str) {
333 assert!(self.is_empty(), "{error_message}\n{self:?}");
334 }
335
336 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 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 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 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}