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::SalsaValue)]
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::SalsaValue)]
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::SalsaValue)]
171pub struct DiagnosticsBuilder<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> {
172 pub error_count: usize,
173 pub leaves: Vec<TEntry>,
174 #[salsa_value(unsafe(prove_safe_to_retain_manually))]
178 pub subtrees: Vec<Diagnostics<'db, TEntry>>,
179 _marker: std::marker::PhantomData<&'db ()>,
180}
181impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> DiagnosticsBuilder<'db, TEntry> {
182 pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
183 if diagnostic.severity() == Severity::Error {
184 self.error_count += 1;
185 }
186 self.leaves.push(diagnostic);
187 DiagnosticAdded
188 }
189 pub fn extend(&mut self, diagnostics: Diagnostics<'db, TEntry>) {
190 self.error_count += diagnostics.0.error_count;
191 self.subtrees.push(diagnostics);
192 }
193 pub fn build(self) -> Diagnostics<'db, TEntry> {
194 Diagnostics(self.into())
195 }
196}
197impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> From<Diagnostics<'db, TEntry>>
198 for DiagnosticsBuilder<'db, TEntry>
199{
200 fn from(diagnostics: Diagnostics<'db, TEntry>) -> Self {
201 let mut new_self = Self::default();
202 new_self.extend(diagnostics);
203 new_self
204 }
205}
206impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Default
207 for DiagnosticsBuilder<'db, TEntry>
208{
209 fn default() -> Self {
210 Self {
211 leaves: Default::default(),
212 subtrees: Default::default(),
213 error_count: 0,
214 _marker: Default::default(),
215 }
216 }
217}
218
219pub fn format_diagnostics(db: &dyn Database, message: &str, location: SpanInFile<'_>) -> String {
220 format!("{message}\n --> {:?}\n", location.debug(db))
221}
222
223#[derive(Debug)]
224pub struct FormattedDiagnosticEntry {
225 severity: Severity,
226 error_code: Option<ErrorCode>,
227 message: String,
228}
229
230impl FormattedDiagnosticEntry {
231 pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
232 Self { severity, error_code, message }
233 }
234
235 pub fn is_empty(&self) -> bool {
236 self.message().is_empty()
237 }
238
239 pub fn severity(&self) -> Severity {
240 self.severity
241 }
242
243 pub fn error_code(&self) -> Option<ErrorCode> {
244 self.error_code
245 }
246
247 pub fn message(&self) -> &str {
248 &self.message
249 }
250}
251
252impl fmt::Display for FormattedDiagnosticEntry {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 write!(
255 f,
256 "{severity}{code}: {message}",
257 severity = self.severity,
258 message = self.message,
259 code = self.error_code.display_bracketed()
260 )
261 }
262}
263
264#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::SalsaValue)]
266pub struct Diagnostics<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue>(
267 pub Arc<DiagnosticsBuilder<'db, TEntry>>,
268);
269impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Diagnostics<'db, TEntry> {
270 pub fn new() -> Self {
271 Self(DiagnosticsBuilder::default().into())
272 }
273
274 pub fn has_errors(&self) -> bool {
276 self.0.error_count > 0
277 }
278
279 pub fn check_error_free(&self) -> Maybe<()> {
281 if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
282 }
283
284 pub fn is_empty(&self) -> bool {
286 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
287 }
288
289 pub fn format_with_severity(
291 &self,
292 db: &'db dyn Database,
293 file_notes: &OrderedHashMap<FileId<'db>, DiagnosticNote<'db>>,
294 ) -> Vec<FormattedDiagnosticEntry> {
295 let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
296
297 let files_db: &'db dyn Database = db;
298 for entry in &self.get_diagnostics_without_duplicates(db) {
299 let mut msg = String::new();
300 let diag_location = entry.location(db);
301 let (user_location, parent_file_notes) =
302 diag_location.user_location_with_plugin_notes(files_db, file_notes);
303
304 let include_generated_location = diag_location != user_location
305 && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
306 msg += &format_diagnostics(files_db, &entry.format(db), user_location);
307
308 if include_generated_location {
309 msg += &format!(
310 "note: The error originates from the generated code in {:?}\n",
311 diag_location.debug(files_db)
312 );
313 }
314
315 for note in entry.notes(db) {
316 msg += &format!("note: {:?}\n", note.debug(files_db))
317 }
318 for note in parent_file_notes {
319 msg += &format!("note: {:?}\n", note.debug(files_db))
320 }
321 msg += "\n";
322
323 let formatted =
324 FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
325 res.push(formatted);
326 }
327 res
328 }
329
330 pub fn format(&self, db: &'db dyn Database) -> String {
332 self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
333 }
334
335 pub fn expect(&self, error_message: &str) {
337 assert!(self.is_empty(), "{error_message}\n{self:?}");
338 }
339
340 pub fn expect_with_db(&self, db: &'db dyn Database, error_message: &str) {
342 assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
343 }
344
345 pub fn get_all(&self) -> Vec<TEntry> {
348 let mut res = self.0.leaves.clone();
349 for subtree in &self.0.subtrees {
350 res.extend(subtree.get_all())
351 }
352 res
353 }
354
355 pub fn get_diagnostics_without_duplicates(&self, db: &'db dyn Database) -> Vec<TEntry> {
360 let diagnostic_with_dup = self.get_all();
361 if diagnostic_with_dup.is_empty() {
362 return diagnostic_with_dup;
363 }
364 let files_db: &'db dyn Database = db;
365 let mut indexed_dup_diagnostic =
366 diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
367 (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
368 });
369 let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
370 let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
371
372 for diag in indexed_dup_diagnostic {
373 if prev_diagnostic_indexed.1.is_same_kind(diag.1)
374 && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
375 == diag.1.location(db).user_location(files_db).span
376 {
377 continue;
378 }
379 diagnostic_without_dup.push(diag);
380 prev_diagnostic_indexed = diag;
381 }
382 diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
383 diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
384 }
385
386 pub fn merge(self, other: Self) -> Self {
388 let mut builder = DiagnosticsBuilder::default();
389 builder.extend(self);
390 builder.extend(other);
391 builder.build()
392 }
393}
394impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> Default for Diagnostics<'db, TEntry> {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399impl<'db, TEntry: DiagnosticEntry<'db> + salsa::SalsaValue> FromIterator<TEntry>
400 for Diagnostics<'db, TEntry>
401{
402 fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
403 let mut builder = DiagnosticsBuilder::<'db, TEntry>::default();
404 for diag in diags_iter {
405 builder.add(diag);
406 }
407 builder.build()
408 }
409}