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