diff options
Diffstat (limited to 'crates/ide_diagnostics/src/lib.rs')
-rw-r--r-- | crates/ide_diagnostics/src/lib.rs | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs new file mode 100644 index 000000000..6ad1b4373 --- /dev/null +++ b/crates/ide_diagnostics/src/lib.rs | |||
@@ -0,0 +1,374 @@ | |||
1 | //! Diagnostics rendering and fixits. | ||
2 | //! | ||
3 | //! Most of the diagnostics originate from the dark depth of the compiler, and | ||
4 | //! are originally expressed in term of IR. When we emit the diagnostic, we are | ||
5 | //! usually not in the position to decide how to best "render" it in terms of | ||
6 | //! user-authored source code. We are especially not in the position to offer | ||
7 | //! fixits, as the compiler completely lacks the infrastructure to edit the | ||
8 | //! source code. | ||
9 | //! | ||
10 | //! Instead, we "bubble up" raw, structured diagnostics until the `hir` crate, | ||
11 | //! where we "cook" them so that each diagnostic is formulated in terms of `hir` | ||
12 | //! types. Well, at least that's the aspiration, the "cooking" is somewhat | ||
13 | //! ad-hoc at the moment. Anyways, we get a bunch of ide-friendly diagnostic | ||
14 | //! structs from hir, and we want to render them to unified serializable | ||
15 | //! representation (span, level, message) here. If we can, we also provide | ||
16 | //! fixits. By the way, that's why we want to keep diagnostics structured | ||
17 | //! internally -- so that we have all the info to make fixes. | ||
18 | //! | ||
19 | //! We have one "handler" module per diagnostic code. Such a module contains | ||
20 | //! rendering, optional fixes and tests. It's OK if some low-level compiler | ||
21 | //! functionality ends up being tested via a diagnostic. | ||
22 | //! | ||
23 | //! There are also a couple of ad-hoc diagnostics implemented directly here, we | ||
24 | //! don't yet have a great pattern for how to do them properly. | ||
25 | |||
26 | mod handlers { | ||
27 | pub(crate) mod break_outside_of_loop; | ||
28 | pub(crate) mod inactive_code; | ||
29 | pub(crate) mod incorrect_case; | ||
30 | pub(crate) mod macro_error; | ||
31 | pub(crate) mod mismatched_arg_count; | ||
32 | pub(crate) mod missing_fields; | ||
33 | pub(crate) mod missing_match_arms; | ||
34 | pub(crate) mod missing_ok_or_some_in_tail_expr; | ||
35 | pub(crate) mod missing_unsafe; | ||
36 | pub(crate) mod no_such_field; | ||
37 | pub(crate) mod remove_this_semicolon; | ||
38 | pub(crate) mod replace_filter_map_next_with_find_map; | ||
39 | pub(crate) mod unimplemented_builtin_macro; | ||
40 | pub(crate) mod unresolved_extern_crate; | ||
41 | pub(crate) mod unresolved_import; | ||
42 | pub(crate) mod unresolved_macro_call; | ||
43 | pub(crate) mod unresolved_module; | ||
44 | pub(crate) mod unresolved_proc_macro; | ||
45 | |||
46 | // The handlers bellow are unusual, the implement the diagnostics as well. | ||
47 | pub(crate) mod field_shorthand; | ||
48 | pub(crate) mod useless_braces; | ||
49 | pub(crate) mod unlinked_file; | ||
50 | } | ||
51 | |||
52 | use hir::{diagnostics::AnyDiagnostic, Semantics}; | ||
53 | use ide_db::{ | ||
54 | assists::{Assist, AssistId, AssistKind, AssistResolveStrategy}, | ||
55 | base_db::{FileId, SourceDatabase}, | ||
56 | label::Label, | ||
57 | source_change::SourceChange, | ||
58 | RootDatabase, | ||
59 | }; | ||
60 | use rustc_hash::FxHashSet; | ||
61 | use syntax::{ast::AstNode, TextRange}; | ||
62 | |||
63 | #[derive(Copy, Clone, Debug, PartialEq)] | ||
64 | pub struct DiagnosticCode(pub &'static str); | ||
65 | |||
66 | impl DiagnosticCode { | ||
67 | pub fn as_str(&self) -> &str { | ||
68 | self.0 | ||
69 | } | ||
70 | } | ||
71 | |||
72 | #[derive(Debug)] | ||
73 | pub struct Diagnostic { | ||
74 | pub code: DiagnosticCode, | ||
75 | pub message: String, | ||
76 | pub range: TextRange, | ||
77 | pub severity: Severity, | ||
78 | pub unused: bool, | ||
79 | pub experimental: bool, | ||
80 | pub fixes: Option<Vec<Assist>>, | ||
81 | } | ||
82 | |||
83 | impl Diagnostic { | ||
84 | fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic { | ||
85 | let message = message.into(); | ||
86 | Diagnostic { | ||
87 | code: DiagnosticCode(code), | ||
88 | message, | ||
89 | range, | ||
90 | severity: Severity::Error, | ||
91 | unused: false, | ||
92 | experimental: false, | ||
93 | fixes: None, | ||
94 | } | ||
95 | } | ||
96 | |||
97 | fn experimental(mut self) -> Diagnostic { | ||
98 | self.experimental = true; | ||
99 | self | ||
100 | } | ||
101 | |||
102 | fn severity(mut self, severity: Severity) -> Diagnostic { | ||
103 | self.severity = severity; | ||
104 | self | ||
105 | } | ||
106 | |||
107 | fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic { | ||
108 | self.fixes = fixes; | ||
109 | self | ||
110 | } | ||
111 | |||
112 | fn with_unused(mut self, unused: bool) -> Diagnostic { | ||
113 | self.unused = unused; | ||
114 | self | ||
115 | } | ||
116 | } | ||
117 | |||
118 | #[derive(Debug, Copy, Clone)] | ||
119 | pub enum Severity { | ||
120 | Error, | ||
121 | // We don't actually emit this one yet, but we should at some point. | ||
122 | // Warning, | ||
123 | WeakWarning, | ||
124 | } | ||
125 | |||
126 | #[derive(Default, Debug, Clone)] | ||
127 | pub struct DiagnosticsConfig { | ||
128 | pub disable_experimental: bool, | ||
129 | pub disabled: FxHashSet<String>, | ||
130 | } | ||
131 | |||
132 | struct DiagnosticsContext<'a> { | ||
133 | config: &'a DiagnosticsConfig, | ||
134 | sema: Semantics<'a, RootDatabase>, | ||
135 | resolve: &'a AssistResolveStrategy, | ||
136 | } | ||
137 | |||
138 | pub fn diagnostics( | ||
139 | db: &RootDatabase, | ||
140 | config: &DiagnosticsConfig, | ||
141 | resolve: &AssistResolveStrategy, | ||
142 | file_id: FileId, | ||
143 | ) -> Vec<Diagnostic> { | ||
144 | let _p = profile::span("diagnostics"); | ||
145 | let sema = Semantics::new(db); | ||
146 | let parse = db.parse(file_id); | ||
147 | let mut res = Vec::new(); | ||
148 | |||
149 | // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily. | ||
150 | res.extend( | ||
151 | parse.errors().iter().take(128).map(|err| { | ||
152 | Diagnostic::new("syntax-error", format!("Syntax Error: {}", err), err.range()) | ||
153 | }), | ||
154 | ); | ||
155 | |||
156 | for node in parse.tree().syntax().descendants() { | ||
157 | handlers::useless_braces::useless_braces(&mut res, file_id, &node); | ||
158 | handlers::field_shorthand::field_shorthand(&mut res, file_id, &node); | ||
159 | } | ||
160 | |||
161 | let module = sema.to_module_def(file_id); | ||
162 | |||
163 | let ctx = DiagnosticsContext { config, sema, resolve }; | ||
164 | if module.is_none() { | ||
165 | handlers::unlinked_file::unlinked_file(&ctx, &mut res, file_id); | ||
166 | } | ||
167 | |||
168 | let mut diags = Vec::new(); | ||
169 | if let Some(m) = module { | ||
170 | m.diagnostics(db, &mut diags) | ||
171 | } | ||
172 | |||
173 | for diag in diags { | ||
174 | #[rustfmt::skip] | ||
175 | let d = match diag { | ||
176 | AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d), | ||
177 | AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d), | ||
178 | AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d), | ||
179 | AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d), | ||
180 | AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d), | ||
181 | AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d), | ||
182 | AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => handlers::missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d), | ||
183 | AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d), | ||
184 | AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d), | ||
185 | AnyDiagnostic::RemoveThisSemicolon(d) => handlers::remove_this_semicolon::remove_this_semicolon(&ctx, &d), | ||
186 | AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), | ||
187 | AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), | ||
188 | AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), | ||
189 | AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d), | ||
190 | AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d), | ||
191 | AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d), | ||
192 | AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d), | ||
193 | |||
194 | AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) { | ||
195 | Some(it) => it, | ||
196 | None => continue, | ||
197 | } | ||
198 | }; | ||
199 | res.push(d) | ||
200 | } | ||
201 | |||
202 | res.retain(|d| { | ||
203 | !ctx.config.disabled.contains(d.code.as_str()) | ||
204 | && !(ctx.config.disable_experimental && d.experimental) | ||
205 | }); | ||
206 | |||
207 | res | ||
208 | } | ||
209 | |||
210 | fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist { | ||
211 | let mut res = unresolved_fix(id, label, target); | ||
212 | res.source_change = Some(source_change); | ||
213 | res | ||
214 | } | ||
215 | |||
216 | fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist { | ||
217 | assert!(!id.contains(' ')); | ||
218 | Assist { | ||
219 | id: AssistId(id, AssistKind::QuickFix), | ||
220 | label: Label::new(label), | ||
221 | group: None, | ||
222 | target, | ||
223 | source_change: None, | ||
224 | } | ||
225 | } | ||
226 | |||
227 | #[cfg(test)] | ||
228 | mod tests { | ||
229 | use expect_test::Expect; | ||
230 | use ide_db::{ | ||
231 | assists::AssistResolveStrategy, | ||
232 | base_db::{fixture::WithFixture, SourceDatabaseExt}, | ||
233 | RootDatabase, | ||
234 | }; | ||
235 | use stdx::trim_indent; | ||
236 | use test_utils::{assert_eq_text, extract_annotations}; | ||
237 | |||
238 | use crate::{DiagnosticsConfig, Severity}; | ||
239 | |||
240 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
241 | /// and checks that: | ||
242 | /// * a diagnostic is produced | ||
243 | /// * the first diagnostic fix trigger range touches the input cursor position | ||
244 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | ||
245 | #[track_caller] | ||
246 | pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { | ||
247 | check_nth_fix(0, ra_fixture_before, ra_fixture_after); | ||
248 | } | ||
249 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
250 | /// and checks that: | ||
251 | /// * a diagnostic is produced | ||
252 | /// * every diagnostic fixes trigger range touches the input cursor position | ||
253 | /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied | ||
254 | pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) { | ||
255 | for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() { | ||
256 | check_nth_fix(i, ra_fixture_before, ra_fixture_after) | ||
257 | } | ||
258 | } | ||
259 | |||
260 | #[track_caller] | ||
261 | fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
262 | let after = trim_indent(ra_fixture_after); | ||
263 | |||
264 | let (db, file_position) = RootDatabase::with_position(ra_fixture_before); | ||
265 | let diagnostic = super::diagnostics( | ||
266 | &db, | ||
267 | &DiagnosticsConfig::default(), | ||
268 | &AssistResolveStrategy::All, | ||
269 | file_position.file_id, | ||
270 | ) | ||
271 | .pop() | ||
272 | .expect("no diagnostics"); | ||
273 | let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth]; | ||
274 | let actual = { | ||
275 | let source_change = fix.source_change.as_ref().unwrap(); | ||
276 | let file_id = *source_change.source_file_edits.keys().next().unwrap(); | ||
277 | let mut actual = db.file_text(file_id).to_string(); | ||
278 | |||
279 | for edit in source_change.source_file_edits.values() { | ||
280 | edit.apply(&mut actual); | ||
281 | } | ||
282 | actual | ||
283 | }; | ||
284 | |||
285 | assert_eq_text!(&after, &actual); | ||
286 | assert!( | ||
287 | fix.target.contains_inclusive(file_position.offset), | ||
288 | "diagnostic fix range {:?} does not touch cursor position {:?}", | ||
289 | fix.target, | ||
290 | file_position.offset | ||
291 | ); | ||
292 | } | ||
293 | |||
294 | /// Checks that there's a diagnostic *without* fix at `$0`. | ||
295 | pub(crate) fn check_no_fix(ra_fixture: &str) { | ||
296 | let (db, file_position) = RootDatabase::with_position(ra_fixture); | ||
297 | let diagnostic = super::diagnostics( | ||
298 | &db, | ||
299 | &DiagnosticsConfig::default(), | ||
300 | &AssistResolveStrategy::All, | ||
301 | file_position.file_id, | ||
302 | ) | ||
303 | .pop() | ||
304 | .unwrap(); | ||
305 | assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic); | ||
306 | } | ||
307 | |||
308 | pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) { | ||
309 | let (db, file_id) = RootDatabase::with_single_file(ra_fixture); | ||
310 | let diagnostics = super::diagnostics( | ||
311 | &db, | ||
312 | &DiagnosticsConfig::default(), | ||
313 | &AssistResolveStrategy::All, | ||
314 | file_id, | ||
315 | ); | ||
316 | expect.assert_debug_eq(&diagnostics) | ||
317 | } | ||
318 | |||
319 | #[track_caller] | ||
320 | pub(crate) fn check_diagnostics(ra_fixture: &str) { | ||
321 | let mut config = DiagnosticsConfig::default(); | ||
322 | config.disabled.insert("inactive-code".to_string()); | ||
323 | check_diagnostics_with_config(config, ra_fixture) | ||
324 | } | ||
325 | |||
326 | #[track_caller] | ||
327 | pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) { | ||
328 | let (db, files) = RootDatabase::with_many_files(ra_fixture); | ||
329 | for file_id in files { | ||
330 | let diagnostics = | ||
331 | super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id); | ||
332 | |||
333 | let expected = extract_annotations(&*db.file_text(file_id)); | ||
334 | let mut actual = diagnostics | ||
335 | .into_iter() | ||
336 | .map(|d| { | ||
337 | let mut annotation = String::new(); | ||
338 | if let Some(fixes) = &d.fixes { | ||
339 | assert!(!fixes.is_empty()); | ||
340 | annotation.push_str("💡 ") | ||
341 | } | ||
342 | annotation.push_str(match d.severity { | ||
343 | Severity::Error => "error", | ||
344 | Severity::WeakWarning => "weak", | ||
345 | }); | ||
346 | annotation.push_str(": "); | ||
347 | annotation.push_str(&d.message); | ||
348 | (d.range, annotation) | ||
349 | }) | ||
350 | .collect::<Vec<_>>(); | ||
351 | actual.sort_by_key(|(range, _)| range.start()); | ||
352 | assert_eq!(expected, actual); | ||
353 | } | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn test_disabled_diagnostics() { | ||
358 | let mut config = DiagnosticsConfig::default(); | ||
359 | config.disabled.insert("unresolved-module".into()); | ||
360 | |||
361 | let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#); | ||
362 | |||
363 | let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id); | ||
364 | assert!(diagnostics.is_empty()); | ||
365 | |||
366 | let diagnostics = super::diagnostics( | ||
367 | &db, | ||
368 | &DiagnosticsConfig::default(), | ||
369 | &AssistResolveStrategy::All, | ||
370 | file_id, | ||
371 | ); | ||
372 | assert!(!diagnostics.is_empty()); | ||
373 | } | ||
374 | } | ||