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