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::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
38pub struct DiagnosticsReporter<'a> {
40 callback: Option<Box<dyn DiagnosticCallback + 'a>>,
41 ignore_all_warnings: bool,
43 ignore_warnings_crate_ids: Vec<CrateInput>,
46 crates: Option<Vec<CrateInput>>,
50 allow_warnings: bool,
52 skip_lowering_diagnostics: bool,
54}
55
56impl DiagnosticsReporter<'_> {
57 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 pub fn stderr() -> Self {
71 Self::callback(|diagnostic| eprint!("{diagnostic}"))
72 }
73}
74
75impl<'a> DiagnosticsReporter<'a> {
76 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 pub fn write_to_string(string: &'a mut String) -> Self {
97 Self::callback(move |diagnostic| {
98 write!(string, "{diagnostic}").unwrap();
99 })
100 }
101
102 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 pub fn with_crates(mut self, crates: &[CrateInput]) -> Self {
116 self.crates = Some(crates.to_vec());
117 self
118 }
119
120 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 pub fn allow_warnings(mut self) -> Self {
131 self.allow_warnings = true;
132 self
133 }
134
135 pub fn ignore_all_warnings(mut self) -> Self {
137 self.ignore_all_warnings = true;
138 self
139 }
140
141 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 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 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 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
273pub 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}