cairo_lang_compiler/
diagnostics.rs1use 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
39pub struct DiagnosticsReporter<'a> {
41 callback: Option<Box<dyn DiagnosticCallback + 'a>>,
42 ignore_all_warnings: bool,
44 ignore_warnings_crate_ids: Vec<CrateId>,
47 crate_ids: Option<Vec<CrateId>>,
51 allow_warnings: bool,
53 skip_lowering_diagnostics: bool,
55}
56
57impl DiagnosticsReporter<'static> {
58 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 pub fn stderr() -> Self {
72 Self::callback(|diagnostic| eprint!("{diagnostic}"))
73 }
74}
75
76impl<'a> DiagnosticsReporter<'a> {
77 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 pub fn write_to_string(string: &'a mut String) -> Self {
98 Self::callback(|diagnostic| {
99 write!(string, "{diagnostic}").unwrap();
100 })
101 }
102
103 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 pub fn with_crates(mut self, crate_ids: &[CrateId]) -> Self {
117 self.crate_ids = Some(crate_ids.to_vec());
118 self
119 }
120
121 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 pub fn allow_warnings(mut self) -> Self {
132 self.allow_warnings = true;
133 self
134 }
135
136 pub fn ignore_all_warnings(mut self) -> Self {
138 self.ignore_all_warnings = true;
139 self
140 }
141
142 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 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 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 pub fn ensure(&mut self, db: &dyn LoweringGroup) -> Result<(), DiagnosticsError> {
251 if self.check(db) { Err(DiagnosticsError) } else { Ok(()) }
252 }
253
254 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
295pub 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}