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;
8use cairo_lang_filesystem::span::TextSpan;
9use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
10use itertools::Itertools;
11use salsa::Database;
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#[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
35pub trait DiagnosticEntry<'db>: Clone + fmt::Debug + Eq + Hash {
38 fn format(&self, db: &'db dyn Database) -> String;
39 fn location(&self, db: &'db dyn Database) -> DiagnosticLocation<'db>;
40 fn notes(&self, _db: &'db dyn Database) -> &[DiagnosticNote<'_>] {
41 &[]
42 }
43 fn severity(&self) -> Severity {
44 Severity::Error
45 }
46 fn error_code(&self) -> Option<ErrorCode> {
47 None
48 }
49 fn is_same_kind(&self, other: &Self) -> bool;
52
53 }
55
56pub type PluginFileDiagnosticNotes<'a> = OrderedHashMap<FileId<'a>, DiagnosticNote<'a>>;
59
60#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
62pub struct DiagnosticLocation<'a> {
63 pub file_id: FileId<'a>,
64 pub span: TextSpan,
65}
66impl<'a> DiagnosticLocation<'a> {
67 pub fn after(&self) -> Self {
69 Self { file_id: self.file_id, span: self.span.after() }
70 }
71
72 pub fn user_location(&self, db: &'a dyn Database) -> Self {
74 let (file_id, span) = get_originating_location(db, self.file_id, self.span, None);
75 Self { file_id, span }
76 }
77
78 pub fn user_location_with_plugin_notes(
82 &self,
83 db: &'a dyn Database,
84 file_notes: &PluginFileDiagnosticNotes<'a>,
85 ) -> (Self, Vec<DiagnosticNote<'_>>) {
86 let mut parent_files = Vec::new();
87 let (file_id, span) =
88 get_originating_location(db, self.file_id, self.span, Some(&mut parent_files));
89 let diagnostic_notes = parent_files
90 .into_iter()
91 .rev()
92 .filter_map(|file_id| file_notes.get(&file_id).cloned())
93 .collect_vec();
94 (Self { file_id, span }, diagnostic_notes)
95 }
96
97 pub fn fmt_location(&self, f: &mut fmt::Formatter<'_>, db: &dyn Database) -> fmt::Result {
99 let file_path = self.file_id.long(db).full_path(db);
100 let start = match self.span.start.position_in_file(db, self.file_id) {
101 Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
102 None => "?".into(),
103 };
104
105 let end = match self.span.end.position_in_file(db, self.file_id) {
106 Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
107 None => "?".into(),
108 };
109 write!(f, "{file_path}:{start}: {end}")
110 }
111}
112
113impl<'a> DebugWithDb<'a> for DiagnosticLocation<'a> {
114 type Db = dyn Database;
115 fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &'a dyn Database) -> fmt::Result {
116 let file_path = self.file_id.long(db).full_path(db);
117 let mut marks = String::new();
118 let mut ending_pos = String::new();
119 let starting_pos = match self.span.start.position_in_file(db, self.file_id) {
120 Some(starting_text_pos) => {
121 if let Some(ending_text_pos) = self.span.end.position_in_file(db, self.file_id)
122 && starting_text_pos.line != ending_text_pos.line
123 {
124 ending_pos = format!("-{}:{}", ending_text_pos.line + 1, ending_text_pos.col);
125 }
126 marks = get_location_marks(db, self, true);
127 format!("{}:{}", starting_text_pos.line + 1, starting_text_pos.col + 1)
128 }
129 None => "?".into(),
130 };
131 write!(f, "{file_path}:{starting_pos}{ending_pos}\n{marks}")
132 }
133}
134
135#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
138pub struct DiagnosticNote<'a> {
139 pub text: String,
140 pub location: Option<DiagnosticLocation<'a>>,
141}
142impl<'a> DiagnosticNote<'a> {
143 pub fn text_only(text: String) -> Self {
144 Self { text, location: None }
145 }
146
147 pub fn with_location(text: String, location: DiagnosticLocation<'a>) -> Self {
148 Self { text, location: Some(location) }
149 }
150}
151
152impl<'a> DebugWithDb<'a> for DiagnosticNote<'a> {
153 type Db = dyn Database;
154 fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &'a dyn Database) -> fmt::Result {
155 write!(f, "{}", self.text)?;
156 if let Some(location) = &self.location {
157 write!(f, ":\n --> ")?;
158 location.user_location(db).fmt(f, db)?;
159 }
160 Ok(())
161 }
162}
163
164#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, salsa::Update)]
169pub struct DiagnosticAdded;
170
171pub fn skip_diagnostic() -> DiagnosticAdded {
172 DiagnosticAdded
174}
175
176pub type Maybe<T> = Result<T, DiagnosticAdded>;
179
180pub trait MaybeAsRef<T> {
182 fn maybe_as_ref(&self) -> Maybe<&T>;
183}
184
185impl<T> MaybeAsRef<T> for Maybe<T> {
186 fn maybe_as_ref(&self) -> Maybe<&T> {
187 self.as_ref().map_err(|e| *e)
188 }
189}
190
191pub trait ToMaybe<T> {
194 fn to_maybe(self) -> Maybe<T>;
195}
196impl<T> ToMaybe<T> for Option<T> {
197 fn to_maybe(self) -> Maybe<T> {
198 match self {
199 Some(val) => Ok(val),
200 None => Err(skip_diagnostic()),
201 }
202 }
203}
204
205pub trait ToOption<T> {
211 fn to_option(self) -> Option<T>;
212}
213impl<T> ToOption<T> for Maybe<T> {
214 fn to_option(self) -> Option<T> {
215 self.ok()
216 }
217}
218
219#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
221pub struct DiagnosticsBuilder<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> {
222 pub error_count: usize,
223 pub leaves: Vec<TEntry>,
224 pub subtrees: Vec<Diagnostics<'db, TEntry>>,
225 _marker: std::marker::PhantomData<&'db ()>,
226}
227impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> DiagnosticsBuilder<'db, TEntry> {
228 pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
229 if diagnostic.severity() == Severity::Error {
230 self.error_count += 1;
231 }
232 self.leaves.push(diagnostic);
233 DiagnosticAdded
234 }
235 pub fn extend(&mut self, diagnostics: Diagnostics<'db, TEntry>) {
236 self.error_count += diagnostics.0.error_count;
237 self.subtrees.push(diagnostics);
238 }
239 pub fn build(self) -> Diagnostics<'db, TEntry> {
240 Diagnostics(self.into())
241 }
242}
243impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> From<Diagnostics<'db, TEntry>>
244 for DiagnosticsBuilder<'db, TEntry>
245{
246 fn from(diagnostics: Diagnostics<'db, TEntry>) -> Self {
247 let mut new_self = Self::default();
248 new_self.extend(diagnostics);
249 new_self
250 }
251}
252impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default
253 for DiagnosticsBuilder<'db, TEntry>
254{
255 fn default() -> Self {
256 Self {
257 leaves: Default::default(),
258 subtrees: Default::default(),
259 error_count: 0,
260 _marker: Default::default(),
261 }
262 }
263}
264
265pub fn format_diagnostics(
266 db: &dyn Database,
267 message: &str,
268 location: DiagnosticLocation<'_>,
269) -> String {
270 format!("{message}\n --> {:?}\n", location.debug(db))
271}
272
273#[derive(Debug)]
274pub struct FormattedDiagnosticEntry {
275 severity: Severity,
276 error_code: Option<ErrorCode>,
277 message: String,
278}
279
280impl FormattedDiagnosticEntry {
281 pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
282 Self { severity, error_code, message }
283 }
284
285 pub fn is_empty(&self) -> bool {
286 self.message().is_empty()
287 }
288
289 pub fn severity(&self) -> Severity {
290 self.severity
291 }
292
293 pub fn error_code(&self) -> Option<ErrorCode> {
294 self.error_code
295 }
296
297 pub fn message(&self) -> &str {
298 &self.message
299 }
300}
301
302impl fmt::Display for FormattedDiagnosticEntry {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 write!(
305 f,
306 "{severity}{code}: {message}",
307 severity = self.severity,
308 message = self.message,
309 code = self.error_code.display_bracketed()
310 )
311 }
312}
313
314#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
316pub struct Diagnostics<'db, TEntry: DiagnosticEntry<'db> + salsa::Update>(
317 pub Arc<DiagnosticsBuilder<'db, TEntry>>,
318);
319impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Diagnostics<'db, TEntry> {
320 pub fn new() -> Self {
321 Self(DiagnosticsBuilder::default().into())
322 }
323
324 pub fn has_errors(&self) -> bool {
326 self.0.error_count > 0
327 }
328
329 pub fn check_error_free(&self) -> Maybe<()> {
331 if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
332 }
333
334 pub fn is_empty(&self) -> bool {
336 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
337 }
338
339 pub fn format_with_severity(
341 &self,
342 db: &'db dyn Database,
343 file_notes: &OrderedHashMap<FileId<'db>, DiagnosticNote<'db>>,
344 ) -> Vec<FormattedDiagnosticEntry> {
345 let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
346
347 let files_db: &'db dyn Database = db;
348 for entry in &self.get_diagnostics_without_duplicates(db) {
349 let mut msg = String::new();
350 let diag_location = entry.location(db);
351 let (user_location, parent_file_notes) =
352 diag_location.user_location_with_plugin_notes(files_db, file_notes);
353
354 let include_generated_location = diag_location != user_location
355 && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
356 msg += &format_diagnostics(files_db, &entry.format(db), user_location);
357
358 if include_generated_location {
359 msg += &format!(
360 "note: The error originates from the generated code in {:?}\n",
361 diag_location.debug(files_db)
362 );
363 }
364
365 for note in entry.notes(db) {
366 msg += &format!("note: {:?}\n", note.debug(files_db))
367 }
368 for note in parent_file_notes {
369 msg += &format!("note: {:?}\n", note.debug(files_db))
370 }
371 msg += "\n";
372
373 let formatted =
374 FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
375 res.push(formatted);
376 }
377 res
378 }
379
380 pub fn format(&self, db: &'db dyn Database) -> String {
382 self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
383 }
384
385 pub fn expect(&self, error_message: &str) {
387 assert!(self.is_empty(), "{error_message}\n{self:?}");
388 }
389
390 pub fn expect_with_db(&self, db: &'db dyn Database, error_message: &str) {
392 assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
393 }
394
395 pub fn get_all(&self) -> Vec<TEntry> {
398 let mut res = self.0.leaves.clone();
399 for subtree in &self.0.subtrees {
400 res.extend(subtree.get_all())
401 }
402 res
403 }
404
405 pub fn get_diagnostics_without_duplicates(&self, db: &'db dyn Database) -> Vec<TEntry> {
410 let diagnostic_with_dup = self.get_all();
411 if diagnostic_with_dup.is_empty() {
412 return diagnostic_with_dup;
413 }
414 let files_db: &'db dyn Database = db;
415 let mut indexed_dup_diagnostic =
416 diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
417 (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
418 });
419 let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
420 let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
421
422 for diag in indexed_dup_diagnostic {
423 if prev_diagnostic_indexed.1.is_same_kind(diag.1)
424 && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
425 == diag.1.location(db).user_location(files_db).span
426 {
427 continue;
428 }
429 diagnostic_without_dup.push(diag);
430 prev_diagnostic_indexed = diag;
431 }
432 diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
433 diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
434 }
435
436 pub fn merge(self, other: Self) -> Self {
438 let mut builder = DiagnosticsBuilder::default();
439 builder.extend(self);
440 builder.extend(other);
441 builder.build()
442 }
443}
444impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default for Diagnostics<'db, TEntry> {
445 fn default() -> Self {
446 Self::new()
447 }
448}
449impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> FromIterator<TEntry>
450 for Diagnostics<'db, TEntry>
451{
452 fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
453 let mut builder = DiagnosticsBuilder::<'db, TEntry>::default();
454 for diag in diags_iter {
455 builder.add(diag);
456 }
457 builder.build()
458 }
459}