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#[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: 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 fn is_same_kind(&self, other: &Self) -> bool;
53
54 }
56
57pub type PluginFileDiagnosticNotes = OrderedHashMap<FileId, DiagnosticNote>;
60
61#[derive(Clone, Debug, Eq, Hash, PartialEq)]
63pub struct DiagnosticLocation {
64 pub file_id: FileId,
65 pub span: TextSpan,
66}
67impl DiagnosticLocation {
68 pub fn after(&self) -> Self {
70 Self { file_id: self.file_id, span: self.span.after() }
71 }
72
73 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 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 pub fn fmt_location(&self, f: &mut fmt::Formatter<'_>, db: &dyn FilesGroup) -> fmt::Result {
100 let file_path = self.file_id.full_path(db);
101 let start = match self.span.start.position_in_file(db, self.file_id) {
102 Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
103 None => "?".into(),
104 };
105
106 let end = match self.span.end.position_in_file(db, self.file_id) {
107 Some(pos) => format!("{}:{}", pos.line + 1, pos.col + 1),
108 None => "?".into(),
109 };
110 write!(f, "{file_path}:{start}: {end}")
111 }
112}
113
114impl DebugWithDb<dyn FilesGroup> for DiagnosticLocation {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &dyn FilesGroup) -> fmt::Result {
116 let file_path = self.file_id.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 if starting_text_pos.line != ending_text_pos.line {
123 ending_pos =
124 format!("-{}:{}", ending_text_pos.line + 1, ending_text_pos.col);
125 }
126 }
127 marks = get_location_marks(db, self, true);
128 format!("{}:{}", starting_text_pos.line + 1, starting_text_pos.col + 1)
129 }
130 None => "?".into(),
131 };
132 write!(f, "{file_path}:{starting_pos}{ending_pos}\n{marks}")
133 }
134}
135
136#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub struct DiagnosticNote {
140 pub text: String,
141 pub location: Option<DiagnosticLocation>,
142}
143impl DiagnosticNote {
144 pub fn text_only(text: String) -> Self {
145 Self { text, location: None }
146 }
147
148 pub fn with_location(text: String, location: DiagnosticLocation) -> Self {
149 Self { text, location: Some(location) }
150 }
151}
152
153impl DebugWithDb<dyn FilesGroup> for DiagnosticNote {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &(dyn FilesGroup + 'static)) -> 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)]
169pub struct DiagnosticAdded;
170
171pub fn skip_diagnostic() -> DiagnosticAdded {
172 DiagnosticAdded
174}
175
176pub type Maybe<T> = Result<T, DiagnosticAdded>;
179
180pub trait ToMaybe<T> {
183 fn to_maybe(self) -> Maybe<T>;
184}
185impl<T> ToMaybe<T> for Option<T> {
186 fn to_maybe(self) -> Maybe<T> {
187 match self {
188 Some(val) => Ok(val),
189 None => Err(skip_diagnostic()),
190 }
191 }
192}
193
194pub trait ToOption<T> {
200 fn to_option(self) -> Option<T>;
201}
202impl<T> ToOption<T> for Maybe<T> {
203 fn to_option(self) -> Option<T> {
204 self.ok()
205 }
206}
207
208#[derive(Clone, Debug, Eq, Hash, PartialEq)]
210pub struct DiagnosticsBuilder<TEntry: DiagnosticEntry> {
211 pub error_count: usize,
212 pub leaves: Vec<TEntry>,
213 pub subtrees: Vec<Diagnostics<TEntry>>,
214}
215impl<TEntry: DiagnosticEntry> DiagnosticsBuilder<TEntry> {
216 pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
217 if diagnostic.severity() == Severity::Error {
218 self.error_count += 1;
219 }
220 self.leaves.push(diagnostic);
221 DiagnosticAdded
222 }
223 pub fn extend(&mut self, diagnostics: Diagnostics<TEntry>) {
224 self.error_count += diagnostics.0.error_count;
225 self.subtrees.push(diagnostics);
226 }
227 pub fn build(self) -> Diagnostics<TEntry> {
228 Diagnostics(self.into())
229 }
230}
231impl<TEntry: DiagnosticEntry> From<Diagnostics<TEntry>> for DiagnosticsBuilder<TEntry> {
232 fn from(diagnostics: Diagnostics<TEntry>) -> Self {
233 let mut new_self = Self::default();
234 new_self.extend(diagnostics);
235 new_self
236 }
237}
238impl<TEntry: DiagnosticEntry> Default for DiagnosticsBuilder<TEntry> {
239 fn default() -> Self {
240 Self { leaves: Default::default(), subtrees: Default::default(), error_count: 0 }
241 }
242}
243
244pub fn format_diagnostics(
245 db: &(dyn FilesGroup + 'static),
246 message: &str,
247 location: DiagnosticLocation,
248) -> String {
249 format!("{message}\n --> {:?}\n", location.debug(db))
250}
251
252#[derive(Debug)]
253pub struct FormattedDiagnosticEntry {
254 severity: Severity,
255 error_code: Option<ErrorCode>,
256 message: String,
257}
258
259impl FormattedDiagnosticEntry {
260 pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
261 Self { severity, error_code, message }
262 }
263
264 pub fn is_empty(&self) -> bool {
265 self.message().is_empty()
266 }
267
268 pub fn severity(&self) -> Severity {
269 self.severity
270 }
271
272 pub fn error_code(&self) -> Option<ErrorCode> {
273 self.error_code
274 }
275
276 pub fn message(&self) -> &str {
277 &self.message
278 }
279}
280
281impl fmt::Display for FormattedDiagnosticEntry {
282 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283 write!(
284 f,
285 "{severity}{code}: {message}",
286 severity = self.severity,
287 message = self.message,
288 code = self.error_code.display_bracketed()
289 )
290 }
291}
292
293#[derive(Clone, Debug, Eq, Hash, PartialEq)]
295pub struct Diagnostics<TEntry: DiagnosticEntry>(pub Arc<DiagnosticsBuilder<TEntry>>);
296impl<TEntry: DiagnosticEntry> Diagnostics<TEntry> {
297 pub fn new() -> Self {
298 Self(DiagnosticsBuilder::default().into())
299 }
300
301 pub fn check_error_free(&self) -> Maybe<()> {
303 if self.0.error_count == 0 { Ok(()) } else { Err(DiagnosticAdded) }
304 }
305
306 pub fn is_empty(&self) -> bool {
308 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
309 }
310
311 pub fn format_with_severity(
313 &self,
314 db: &TEntry::DbType,
315 file_notes: &OrderedHashMap<FileId, DiagnosticNote>,
316 ) -> Vec<FormattedDiagnosticEntry> {
317 let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
318
319 let files_db = db.upcast();
320 for entry in &self.get_diagnostics_without_duplicates(db) {
321 let mut msg = String::new();
322 let diag_location = entry.location(db);
323 let (user_location, parent_file_notes) =
324 diag_location.user_location_with_plugin_notes(files_db, file_notes);
325
326 let include_generated_location = diag_location != user_location
327 && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
328 msg += &format_diagnostics(files_db, &entry.format(db), user_location);
329
330 if include_generated_location {
331 msg += &format!(
332 "note: The error originates from the generated code in {:?}\n",
333 diag_location.debug(files_db)
334 );
335 }
336
337 for note in entry.notes(db) {
338 msg += &format!("note: {:?}\n", note.debug(files_db))
339 }
340 for note in parent_file_notes {
341 msg += &format!("note: {:?}\n", note.debug(files_db))
342 }
343 msg += "\n";
344
345 let formatted =
346 FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
347 res.push(formatted);
348 }
349 res
350 }
351
352 pub fn format(&self, db: &TEntry::DbType) -> String {
354 self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
355 }
356
357 pub fn expect(&self, error_message: &str) {
359 assert!(self.is_empty(), "{error_message}\n{self:?}");
360 }
361
362 pub fn expect_with_db(&self, db: &TEntry::DbType, error_message: &str) {
364 assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
365 }
366
367 pub fn get_all(&self) -> Vec<TEntry> {
370 let mut res = self.0.leaves.clone();
371 for subtree in &self.0.subtrees {
372 res.extend(subtree.get_all())
373 }
374 res
375 }
376
377 pub fn get_diagnostics_without_duplicates(&self, db: &TEntry::DbType) -> Vec<TEntry> {
382 let diagnostic_with_dup = self.get_all();
383 if diagnostic_with_dup.is_empty() {
384 return diagnostic_with_dup;
385 }
386 let files_db = db.upcast();
387 let mut indexed_dup_diagnostic =
388 diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
389 (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
390 });
391 let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
392 let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
393
394 for diag in indexed_dup_diagnostic {
395 if prev_diagnostic_indexed.1.is_same_kind(diag.1)
396 && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
397 == diag.1.location(db).user_location(files_db).span
398 {
399 continue;
400 }
401 diagnostic_without_dup.push(diag);
402 prev_diagnostic_indexed = diag;
403 }
404 diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
405 diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
406 }
407}
408impl<TEntry: DiagnosticEntry> Default for Diagnostics<TEntry> {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413impl<TEntry: DiagnosticEntry> FromIterator<TEntry> for Diagnostics<TEntry> {
414 fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
415 let mut builder = DiagnosticsBuilder::<TEntry>::default();
416 for diag in diags_iter {
417 builder.add(diag);
418 }
419 builder.build()
420 }
421}