cairo_lang_compiler/
diagnostics.rs

1use std::fmt::Write;
2
3use cairo_lang_defs::db::DefsGroup;
4use cairo_lang_defs::ids::ModuleId;
5use cairo_lang_diagnostics::{
6    DiagnosticEntry, Diagnostics, FormattedDiagnosticEntry, PluginFileDiagnosticNotes, Severity,
7};
8use cairo_lang_filesystem::ids::{CrateId, FileLongId};
9use cairo_lang_lowering::db::LoweringGroup;
10use cairo_lang_parser::db::ParserGroup;
11use cairo_lang_semantic::db::SemanticGroup;
12use cairo_lang_utils::LookupIntern;
13use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
14use rayon::ThreadPool;
15use thiserror::Error;
16
17use crate::db::RootDatabase;
18
19#[cfg(test)]
20#[path = "diagnostics_test.rs"]
21mod test;
22
23#[derive(Error, Debug, Eq, PartialEq)]
24#[error("Compilation failed.")]
25pub struct DiagnosticsError;
26
27trait DiagnosticCallback {
28    fn on_diagnostic(&mut self, diagnostic: FormattedDiagnosticEntry);
29}
30
31impl DiagnosticCallback for Option<Box<dyn DiagnosticCallback + '_>> {
32    fn on_diagnostic(&mut self, diagnostic: FormattedDiagnosticEntry) {
33        if let Some(callback) = self {
34            callback.on_diagnostic(diagnostic)
35        }
36    }
37}
38
39/// Collects compilation diagnostics and presents them in a preconfigured way.
40pub struct DiagnosticsReporter<'a> {
41    callback: Option<Box<dyn DiagnosticCallback + 'a>>,
42    // Ignore all warnings, the `ignore_warnings_crate_ids` field is irrelevant in this case.
43    ignore_all_warnings: bool,
44    /// Ignore warnings in specific crates. This should be a subset of `crate_ids`.
45    /// Adding ids that are not in `crate_ids` has no effect.
46    ignore_warnings_crate_ids: Vec<CrateId>,
47    /// Check diagnostics for these crates only.
48    /// If `None`, check all crates in the db.
49    /// If empty, do not check any crates at all.
50    crate_ids: Option<Vec<CrateId>>,
51    /// If true, compilation will not fail due to warnings.
52    allow_warnings: bool,
53    /// If true, will ignore diagnostics from LoweringGroup during the ensure function.
54    skip_lowering_diagnostics: bool,
55}
56
57impl DiagnosticsReporter<'static> {
58    /// Create a reporter which does not print or collect diagnostics at all.
59    pub fn ignoring() -> Self {
60        Self {
61            callback: None,
62            crate_ids: Default::default(),
63            ignore_all_warnings: false,
64            ignore_warnings_crate_ids: vec![],
65            allow_warnings: false,
66            skip_lowering_diagnostics: false,
67        }
68    }
69
70    /// Create a reporter which prints all diagnostics to [`std::io::Stderr`].
71    pub fn stderr() -> Self {
72        Self::callback(|diagnostic| eprint!("{diagnostic}"))
73    }
74}
75
76impl<'a> DiagnosticsReporter<'a> {
77    // NOTE(mkaput): If Rust will ever have intersection types, one could write
78    //   impl<F> DiagnosticCallback for F where F: FnMut(Severity,String)
79    //   and `new` could accept regular functions without need for this separate method.
80    /// Create a reporter which calls `callback` for each diagnostic.
81    pub fn callback(callback: impl FnMut(FormattedDiagnosticEntry) + 'a) -> Self {
82        struct Func<F>(F);
83
84        impl<F> DiagnosticCallback for Func<F>
85        where
86            F: FnMut(FormattedDiagnosticEntry),
87        {
88            fn on_diagnostic(&mut self, diagnostic: FormattedDiagnosticEntry) {
89                self.0(diagnostic)
90            }
91        }
92
93        Self::new(Func(callback))
94    }
95
96    /// Create a reporter which appends all diagnostics to provided string.
97    pub fn write_to_string(string: &'a mut String) -> Self {
98        Self::callback(|diagnostic| {
99            write!(string, "{diagnostic}").unwrap();
100        })
101    }
102
103    /// Create a reporter which calls [`DiagnosticCallback::on_diagnostic`].
104    fn new(callback: impl DiagnosticCallback + 'a) -> Self {
105        Self {
106            callback: Some(Box::new(callback)),
107            crate_ids: Default::default(),
108            ignore_all_warnings: false,
109            ignore_warnings_crate_ids: vec![],
110            allow_warnings: false,
111            skip_lowering_diagnostics: false,
112        }
113    }
114
115    /// Sets crates to be checked, instead of all crates in the db.
116    pub fn with_crates(mut self, crate_ids: &[CrateId]) -> Self {
117        self.crate_ids = Some(crate_ids.to_vec());
118        self
119    }
120
121    /// Ignore warnings in these crates.
122    /// This does not modify the set of crates to be checked.
123    /// Adding crates that are not checked here has no effect.
124    /// To change the set of crates to be checked, use `with_crates`.
125    pub fn with_ignore_warnings_crates(mut self, crate_ids: &[CrateId]) -> Self {
126        self.ignore_warnings_crate_ids = crate_ids.to_vec();
127        self
128    }
129
130    /// Allows the compilation to succeed if only warnings are emitted.
131    pub fn allow_warnings(mut self) -> Self {
132        self.allow_warnings = true;
133        self
134    }
135
136    /// Ignores warnings in all cargo crates.
137    pub fn ignore_all_warnings(mut self) -> Self {
138        self.ignore_all_warnings = true;
139        self
140    }
141
142    /// Returns the crate ids for which the diagnostics will be checked.
143    fn crates_of_interest(&self, db: &dyn LoweringGroup) -> Vec<CrateId> {
144        if let Some(crates) = self.crate_ids.as_ref() { crates.clone() } else { db.crates() }
145    }
146
147    /// Checks if there are diagnostics and reports them to the provided callback as strings.
148    /// Returns `true` if diagnostics were found.
149    pub fn check(&mut self, db: &dyn LoweringGroup) -> bool {
150        let mut found_diagnostics = false;
151
152        let crates = self.crates_of_interest(db);
153        for crate_id in &crates {
154            let Ok(module_file) = db.module_main_file(ModuleId::CrateRoot(*crate_id)) else {
155                found_diagnostics = true;
156                self.callback.on_diagnostic(FormattedDiagnosticEntry::new(
157                    Severity::Error,
158                    None,
159                    "Failed to get main module file".to_string(),
160                ));
161                continue;
162            };
163
164            if db.file_content(module_file).is_none() {
165                match module_file.lookup_intern(db) {
166                    FileLongId::OnDisk(path) => {
167                        self.callback.on_diagnostic(FormattedDiagnosticEntry::new(
168                            Severity::Error,
169                            None,
170                            format!("{} not found\n", path.display()),
171                        ))
172                    }
173                    FileLongId::Virtual(_) => panic!("Missing virtual file."),
174                    FileLongId::External(_) => panic!("Missing external file."),
175                }
176                found_diagnostics = true;
177            }
178
179            let ignore_warnings_in_crate =
180                self.ignore_all_warnings || self.ignore_warnings_crate_ids.contains(crate_id);
181            let modules = db.crate_modules(*crate_id);
182            let mut processed_file_ids = UnorderedHashSet::<_>::default();
183            for module_id in modules.iter() {
184                let diagnostic_notes =
185                    db.module_plugin_diagnostics_notes(*module_id).unwrap_or_default();
186
187                if let Ok(module_files) = db.module_files(*module_id) {
188                    for file_id in module_files.iter().copied() {
189                        if processed_file_ids.insert(file_id) {
190                            found_diagnostics |= self.check_diag_group(
191                                db.upcast(),
192                                db.file_syntax_diagnostics(file_id),
193                                ignore_warnings_in_crate,
194                                &diagnostic_notes,
195                            );
196                        }
197                    }
198                }
199
200                if let Ok(group) = db.module_semantic_diagnostics(*module_id) {
201                    found_diagnostics |= self.check_diag_group(
202                        db.upcast(),
203                        group,
204                        ignore_warnings_in_crate,
205                        &diagnostic_notes,
206                    );
207                }
208
209                if self.skip_lowering_diagnostics {
210                    continue;
211                }
212
213                if let Ok(group) = db.module_lowering_diagnostics(*module_id) {
214                    found_diagnostics |= self.check_diag_group(
215                        db.upcast(),
216                        group,
217                        ignore_warnings_in_crate,
218                        &diagnostic_notes,
219                    );
220                }
221            }
222        }
223        found_diagnostics
224    }
225
226    /// Checks if a diagnostics group contains any diagnostics and reports them to the provided
227    /// callback as strings. Returns `true` if diagnostics were found.
228    fn check_diag_group<TEntry: DiagnosticEntry>(
229        &mut self,
230        db: &TEntry::DbType,
231        group: Diagnostics<TEntry>,
232        skip_warnings: bool,
233        file_notes: &PluginFileDiagnosticNotes,
234    ) -> bool {
235        let mut found: bool = false;
236        for entry in group.format_with_severity(db, file_notes) {
237            if skip_warnings && entry.severity() == Severity::Warning {
238                continue;
239            }
240            if !entry.is_empty() {
241                self.callback.on_diagnostic(entry);
242                found |= !self.allow_warnings || group.check_error_free().is_err();
243            }
244        }
245        found
246    }
247
248    /// Checks if there are diagnostics and reports them to the provided callback as strings.
249    /// Returns `Err` if diagnostics were found.
250    pub fn ensure(&mut self, db: &dyn LoweringGroup) -> Result<(), DiagnosticsError> {
251        if self.check(db) { Err(DiagnosticsError) } else { Ok(()) }
252    }
253
254    /// Spawns threads to compute the diagnostics queries, making sure later calls for these queries
255    /// would be faster as the queries were already computed.
256    pub(crate) fn warm_up_diagnostics(&self, db: &RootDatabase, pool: &ThreadPool) {
257        let crates = self.crates_of_interest(db);
258        for crate_id in crates {
259            let snapshot = salsa::ParallelDatabase::snapshot(db);
260            pool.spawn(move || {
261                let db = &*snapshot;
262
263                let crate_modules = db.crate_modules(crate_id);
264                for module_id in crate_modules.iter().copied() {
265                    let snapshot = salsa::ParallelDatabase::snapshot(db);
266                    rayon::spawn(move || {
267                        let db = &*snapshot;
268                        for file_id in
269                            db.module_files(module_id).unwrap_or_default().iter().copied()
270                        {
271                            db.file_syntax_diagnostics(file_id);
272                        }
273
274                        let _ = db.module_semantic_diagnostics(module_id);
275
276                        let _ = db.module_lowering_diagnostics(module_id);
277                    });
278                }
279            });
280        }
281    }
282
283    pub fn skip_lowering_diagnostics(mut self) -> Self {
284        self.skip_lowering_diagnostics = true;
285        self
286    }
287}
288
289impl Default for DiagnosticsReporter<'static> {
290    fn default() -> Self {
291        DiagnosticsReporter::stderr()
292    }
293}
294
295/// Returns a string with all the diagnostics in the db.
296///
297/// This is a shortcut for `DiagnosticsReporter::write_to_string(&mut string).check(db)`.
298///
299/// If `crates_to_check` is `Some`, only diagnostics for these crates will be checked.
300/// If `crates_to_check` is `None`, diagnostics for all crates in the db will be checked.
301pub fn get_diagnostics_as_string(
302    db: &RootDatabase,
303    crates_to_check: Option<Vec<CrateId>>,
304) -> String {
305    let mut diagnostics = String::default();
306    let mut reporter = DiagnosticsReporter::write_to_string(&mut diagnostics);
307    if let Some(crates) = crates_to_check.as_ref() {
308        reporter = reporter.with_crates(crates);
309    }
310    reporter.check(db);
311    drop(reporter);
312    diagnostics
313}