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 }
54
55pub type PluginFileDiagnosticNotes<'a> = OrderedHashMap<FileId<'a>, DiagnosticNote<'a>>;
58
59pub trait UserLocationWithPluginNotes<'db> {
61 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#[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#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, HeapSize, salsa::Update)]
121pub struct DiagnosticAdded;
122
123pub fn skip_diagnostic() -> DiagnosticAdded {
124 DiagnosticAdded
126}
127
128pub type Maybe<T> = Result<T, DiagnosticAdded>;
131
132pub 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
143pub 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
157pub 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#[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#[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 pub fn has_errors(&self) -> bool {
274 self.0.error_count > 0
275 }
276
277 pub fn check_error_free(&self) -> Maybe<()> {
279 if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
280 }
281
282 pub fn is_empty(&self) -> bool {
284 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
285 }
286
287 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 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 pub fn expect(&self, error_message: &str) {
335 assert!(self.is_empty(), "{error_message}\n{self:?}");
336 }
337
338 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 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 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 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}