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_utils::ordered_hash_map::OrderedHashMap;
9use itertools::Itertools;
10use salsa::Database;
11
12use crate::error_code::{ErrorCode, OptionErrorCodeExt};
13
14#[cfg(test)]
15#[path = "diagnostics_test.rs"]
16mod test;
17
18#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Copy, Debug)]
20pub enum Severity {
21 Error,
22 Warning,
23}
24impl fmt::Display for Severity {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Severity::Error => write!(f, "error"),
28 Severity::Warning => write!(f, "warning"),
29 }
30 }
31}
32
33pub trait DiagnosticEntry<'db>: Clone + fmt::Debug + Eq + Hash {
36 fn format(&self, db: &'db dyn Database) -> String;
37 fn location(&self, db: &'db dyn Database) -> SpanInFile<'db>;
38 fn notes(&self, _db: &'db dyn Database) -> &[DiagnosticNote<'_>] {
39 &[]
40 }
41 fn severity(&self) -> Severity {
42 Severity::Error
43 }
44 fn error_code(&self) -> Option<ErrorCode> {
45 None
46 }
47 fn is_same_kind(&self, other: &Self) -> bool;
50
51 }
53
54pub type PluginFileDiagnosticNotes<'a> = OrderedHashMap<FileId<'a>, DiagnosticNote<'a>>;
57
58pub trait UserLocationWithPluginNotes<'db> {
60 fn user_location_with_plugin_notes(
64 &self,
65 db: &'db dyn Database,
66 file_notes: &PluginFileDiagnosticNotes<'db>,
67 ) -> (SpanInFile<'db>, Vec<DiagnosticNote<'db>>);
68}
69impl<'db> UserLocationWithPluginNotes<'db> for SpanInFile<'db> {
70 fn user_location_with_plugin_notes(
71 &self,
72 db: &'db dyn Database,
73 file_notes: &PluginFileDiagnosticNotes<'db>,
74 ) -> (SpanInFile<'db>, Vec<DiagnosticNote<'db>>) {
75 let mut parent_files = Vec::new();
76 let origin = get_originating_location(db, *self, Some(&mut parent_files));
77 let diagnostic_notes = parent_files
78 .into_iter()
79 .rev()
80 .filter_map(|file_id| file_notes.get(&file_id).cloned())
81 .collect_vec();
82 (origin, diagnostic_notes)
83 }
84}
85
86#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
89pub struct DiagnosticNote<'a> {
90 pub text: String,
91 pub location: Option<SpanInFile<'a>>,
92}
93impl<'a> DiagnosticNote<'a> {
94 pub fn text_only(text: String) -> Self {
95 Self { text, location: None }
96 }
97
98 pub fn with_location(text: String, location: SpanInFile<'a>) -> Self {
99 Self { text, location: Some(location) }
100 }
101}
102
103impl<'a> DebugWithDb<'a> for DiagnosticNote<'a> {
104 type Db = dyn Database;
105 fn fmt(&self, f: &mut fmt::Formatter<'_>, db: &'a dyn Database) -> fmt::Result {
106 write!(f, "{}", self.text)?;
107 if let Some(location) = &self.location {
108 write!(f, ":\n --> ")?;
109 location.user_location(db).fmt(f, db)?;
110 }
111 Ok(())
112 }
113}
114
115#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, salsa::Update)]
120pub struct DiagnosticAdded;
121
122pub fn skip_diagnostic() -> DiagnosticAdded {
123 DiagnosticAdded
125}
126
127pub type Maybe<T> = Result<T, DiagnosticAdded>;
130
131pub trait MaybeAsRef<T> {
133 fn maybe_as_ref(&self) -> Maybe<&T>;
134}
135
136impl<T> MaybeAsRef<T> for Maybe<T> {
137 fn maybe_as_ref(&self) -> Maybe<&T> {
138 self.as_ref().map_err(|e| *e)
139 }
140}
141
142pub trait ToMaybe<T> {
145 fn to_maybe(self) -> Maybe<T>;
146}
147impl<T> ToMaybe<T> for Option<T> {
148 fn to_maybe(self) -> Maybe<T> {
149 match self {
150 Some(val) => Ok(val),
151 None => Err(skip_diagnostic()),
152 }
153 }
154}
155
156pub trait ToOption<T> {
162 fn to_option(self) -> Option<T>;
163}
164impl<T> ToOption<T> for Maybe<T> {
165 fn to_option(self) -> Option<T> {
166 self.ok()
167 }
168}
169
170#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
172pub struct DiagnosticsBuilder<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> {
173 pub error_count: usize,
174 pub leaves: Vec<TEntry>,
175 pub subtrees: Vec<Diagnostics<'db, TEntry>>,
176 _marker: std::marker::PhantomData<&'db ()>,
177}
178impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> DiagnosticsBuilder<'db, TEntry> {
179 pub fn add(&mut self, diagnostic: TEntry) -> DiagnosticAdded {
180 if diagnostic.severity() == Severity::Error {
181 self.error_count += 1;
182 }
183 self.leaves.push(diagnostic);
184 DiagnosticAdded
185 }
186 pub fn extend(&mut self, diagnostics: Diagnostics<'db, TEntry>) {
187 self.error_count += diagnostics.0.error_count;
188 self.subtrees.push(diagnostics);
189 }
190 pub fn build(self) -> Diagnostics<'db, TEntry> {
191 Diagnostics(self.into())
192 }
193}
194impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> From<Diagnostics<'db, TEntry>>
195 for DiagnosticsBuilder<'db, TEntry>
196{
197 fn from(diagnostics: Diagnostics<'db, TEntry>) -> Self {
198 let mut new_self = Self::default();
199 new_self.extend(diagnostics);
200 new_self
201 }
202}
203impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default
204 for DiagnosticsBuilder<'db, TEntry>
205{
206 fn default() -> Self {
207 Self {
208 leaves: Default::default(),
209 subtrees: Default::default(),
210 error_count: 0,
211 _marker: Default::default(),
212 }
213 }
214}
215
216pub fn format_diagnostics(db: &dyn Database, message: &str, location: SpanInFile<'_>) -> String {
217 format!("{message}\n --> {:?}\n", location.debug(db))
218}
219
220#[derive(Debug)]
221pub struct FormattedDiagnosticEntry {
222 severity: Severity,
223 error_code: Option<ErrorCode>,
224 message: String,
225}
226
227impl FormattedDiagnosticEntry {
228 pub fn new(severity: Severity, error_code: Option<ErrorCode>, message: String) -> Self {
229 Self { severity, error_code, message }
230 }
231
232 pub fn is_empty(&self) -> bool {
233 self.message().is_empty()
234 }
235
236 pub fn severity(&self) -> Severity {
237 self.severity
238 }
239
240 pub fn error_code(&self) -> Option<ErrorCode> {
241 self.error_code
242 }
243
244 pub fn message(&self) -> &str {
245 &self.message
246 }
247}
248
249impl fmt::Display for FormattedDiagnosticEntry {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 write!(
252 f,
253 "{severity}{code}: {message}",
254 severity = self.severity,
255 message = self.message,
256 code = self.error_code.display_bracketed()
257 )
258 }
259}
260
261#[derive(Clone, Debug, Eq, Hash, PartialEq, salsa::Update)]
263pub struct Diagnostics<'db, TEntry: DiagnosticEntry<'db> + salsa::Update>(
264 pub Arc<DiagnosticsBuilder<'db, TEntry>>,
265);
266impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Diagnostics<'db, TEntry> {
267 pub fn new() -> Self {
268 Self(DiagnosticsBuilder::default().into())
269 }
270
271 pub fn has_errors(&self) -> bool {
273 self.0.error_count > 0
274 }
275
276 pub fn check_error_free(&self) -> Maybe<()> {
278 if self.has_errors() { Err(DiagnosticAdded) } else { Ok(()) }
279 }
280
281 pub fn is_empty(&self) -> bool {
283 self.0.leaves.is_empty() && self.0.subtrees.iter().all(|subtree| subtree.is_empty())
284 }
285
286 pub fn format_with_severity(
288 &self,
289 db: &'db dyn Database,
290 file_notes: &OrderedHashMap<FileId<'db>, DiagnosticNote<'db>>,
291 ) -> Vec<FormattedDiagnosticEntry> {
292 let mut res: Vec<FormattedDiagnosticEntry> = Vec::new();
293
294 let files_db: &'db dyn Database = db;
295 for entry in &self.get_diagnostics_without_duplicates(db) {
296 let mut msg = String::new();
297 let diag_location = entry.location(db);
298 let (user_location, parent_file_notes) =
299 diag_location.user_location_with_plugin_notes(files_db, file_notes);
300
301 let include_generated_location = diag_location != user_location
302 && std::env::var("CAIRO_DEBUG_GENERATED_CODE").is_ok();
303 msg += &format_diagnostics(files_db, &entry.format(db), user_location);
304
305 if include_generated_location {
306 msg += &format!(
307 "note: The error originates from the generated code in {:?}\n",
308 diag_location.debug(files_db)
309 );
310 }
311
312 for note in entry.notes(db) {
313 msg += &format!("note: {:?}\n", note.debug(files_db))
314 }
315 for note in parent_file_notes {
316 msg += &format!("note: {:?}\n", note.debug(files_db))
317 }
318 msg += "\n";
319
320 let formatted =
321 FormattedDiagnosticEntry::new(entry.severity(), entry.error_code(), msg);
322 res.push(formatted);
323 }
324 res
325 }
326
327 pub fn format(&self, db: &'db dyn Database) -> String {
329 self.format_with_severity(db, &Default::default()).iter().map(ToString::to_string).join("")
330 }
331
332 pub fn expect(&self, error_message: &str) {
334 assert!(self.is_empty(), "{error_message}\n{self:?}");
335 }
336
337 pub fn expect_with_db(&self, db: &'db dyn Database, error_message: &str) {
339 assert!(self.is_empty(), "{}\n{}", error_message, self.format(db));
340 }
341
342 pub fn get_all(&self) -> Vec<TEntry> {
345 let mut res = self.0.leaves.clone();
346 for subtree in &self.0.subtrees {
347 res.extend(subtree.get_all())
348 }
349 res
350 }
351
352 pub fn get_diagnostics_without_duplicates(&self, db: &'db dyn Database) -> Vec<TEntry> {
357 let diagnostic_with_dup = self.get_all();
358 if diagnostic_with_dup.is_empty() {
359 return diagnostic_with_dup;
360 }
361 let files_db: &'db dyn Database = db;
362 let mut indexed_dup_diagnostic =
363 diagnostic_with_dup.iter().enumerate().sorted_by_cached_key(|(idx, diag)| {
364 (diag.location(db).user_location(files_db).span, diag.format(db), *idx)
365 });
366 let mut prev_diagnostic_indexed = indexed_dup_diagnostic.next().unwrap();
367 let mut diagnostic_without_dup = vec![prev_diagnostic_indexed];
368
369 for diag in indexed_dup_diagnostic {
370 if prev_diagnostic_indexed.1.is_same_kind(diag.1)
371 && prev_diagnostic_indexed.1.location(db).user_location(files_db).span
372 == diag.1.location(db).user_location(files_db).span
373 {
374 continue;
375 }
376 diagnostic_without_dup.push(diag);
377 prev_diagnostic_indexed = diag;
378 }
379 diagnostic_without_dup.sort_by_key(|(idx, _)| *idx);
380 diagnostic_without_dup.into_iter().map(|(_, diag)| diag.clone()).collect()
381 }
382
383 pub fn merge(self, other: Self) -> Self {
385 let mut builder = DiagnosticsBuilder::default();
386 builder.extend(self);
387 builder.extend(other);
388 builder.build()
389 }
390}
391impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> Default for Diagnostics<'db, TEntry> {
392 fn default() -> Self {
393 Self::new()
394 }
395}
396impl<'db, TEntry: DiagnosticEntry<'db> + salsa::Update> FromIterator<TEntry>
397 for Diagnostics<'db, TEntry>
398{
399 fn from_iter<T: IntoIterator<Item = TEntry>>(diags_iter: T) -> Self {
400 let mut builder = DiagnosticsBuilder::<'db, TEntry>::default();
401 for diag in diags_iter {
402 builder.add(diag);
403 }
404 builder.build()
405 }
406}