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 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
38pub struct DiagnosticsReporter<'a> {
40 callback: Option<Box<dyn DiagnosticCallback + 'a>>,
41 ignore_all_warnings: bool,
43 ignore_warnings_crate_ids: Vec<CrateId>,
46 crate_ids: Vec<CrateId>,
49 allow_warnings: bool,
51 skip_lowering_diagnostics: bool,
53}
54
55impl DiagnosticsReporter<'static> {
56 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 pub fn stderr() -> Self {
70 Self::callback(|diagnostic| eprint!("{diagnostic}"))
71 }
72}
73
74impl<'a> DiagnosticsReporter<'a> {
75 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 pub fn write_to_string(string: &'a mut String) -> Self {
96 Self::callback(|diagnostic| {
97 write!(string, "{diagnostic}").unwrap();
98 })
99 }
100
101 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 pub fn with_crates(mut self, crate_ids: &[CrateId]) -> Self {
115 self.crate_ids = crate_ids.to_vec();
116 self
117 }
118
119 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 pub fn allow_warnings(mut self) -> Self {
130 self.allow_warnings = true;
131 self
132 }
133
134 pub fn ignore_all_warnings(mut self) -> Self {
136 self.ignore_all_warnings = true;
137 self
138 }
139
140 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 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 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 pub fn ensure(&mut self, db: &dyn LoweringGroup) -> Result<(), DiagnosticsError> {
249 if self.check(db) { Err(DiagnosticsError) } else { Ok(()) }
250 }
251
252 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
293pub 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}