aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_diagnostics/src/lib.rs
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-06-14 17:46:25 +0100
committerGitHub <[email protected]>2021-06-14 17:46:25 +0100
commit38ae18b7592f97a7058d97928307bccbd881a582 (patch)
tree5f6f59f48f05999495654bf2e4250e029e6f010f /crates/ide_diagnostics/src/lib.rs
parent401d79ac0674ec62689949c3a531836420cb9beb (diff)
parent4768e5fb23c058eba90f0a1dcd6e9d5c0ecdee1b (diff)
Merge #9272
9272: internal: move diagnostics to a dedicated crate r=matklad a=matklad bors r+ Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide_diagnostics/src/lib.rs')
-rw-r--r--crates/ide_diagnostics/src/lib.rs530
1 files changed, 530 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..88037be5a
--- /dev/null
+++ b/crates/ide_diagnostics/src/lib.rs
@@ -0,0 +1,530 @@
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
26mod 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 unlinked_file;
41 pub(crate) mod unresolved_extern_crate;
42 pub(crate) mod unresolved_import;
43 pub(crate) mod unresolved_macro_call;
44 pub(crate) mod unresolved_module;
45 pub(crate) mod unresolved_proc_macro;
46}
47
48mod field_shorthand;
49
50use hir::{diagnostics::AnyDiagnostic, Semantics};
51use ide_db::{
52 assists::{Assist, AssistId, AssistKind, AssistResolveStrategy},
53 base_db::{FileId, SourceDatabase},
54 label::Label,
55 source_change::SourceChange,
56 RootDatabase,
57};
58use itertools::Itertools;
59use rustc_hash::FxHashSet;
60use syntax::{
61 ast::{self, AstNode},
62 SyntaxNode, TextRange,
63};
64use text_edit::TextEdit;
65
66use crate::handlers::unlinked_file::UnlinkedFile;
67
68#[derive(Copy, Clone, Debug, PartialEq)]
69pub struct DiagnosticCode(pub &'static str);
70
71impl DiagnosticCode {
72 pub fn as_str(&self) -> &str {
73 self.0
74 }
75}
76
77#[derive(Debug)]
78pub struct Diagnostic {
79 pub code: DiagnosticCode,
80 pub message: String,
81 pub range: TextRange,
82 pub severity: Severity,
83 pub unused: bool,
84 pub experimental: bool,
85 pub fixes: Option<Vec<Assist>>,
86}
87
88impl Diagnostic {
89 fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
90 let message = message.into();
91 Diagnostic {
92 code: DiagnosticCode(code),
93 message,
94 range,
95 severity: Severity::Error,
96 unused: false,
97 experimental: false,
98 fixes: None,
99 }
100 }
101
102 fn experimental(mut self) -> Diagnostic {
103 self.experimental = true;
104 self
105 }
106
107 fn severity(mut self, severity: Severity) -> Diagnostic {
108 self.severity = severity;
109 self
110 }
111
112 fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic {
113 self.fixes = fixes;
114 self
115 }
116
117 fn with_unused(mut self, unused: bool) -> Diagnostic {
118 self.unused = unused;
119 self
120 }
121}
122
123#[derive(Debug, Copy, Clone)]
124pub enum Severity {
125 Error,
126 WeakWarning,
127}
128
129#[derive(Default, Debug, Clone)]
130pub struct DiagnosticsConfig {
131 pub disable_experimental: bool,
132 pub disabled: FxHashSet<String>,
133}
134
135struct DiagnosticsContext<'a> {
136 config: &'a DiagnosticsConfig,
137 sema: Semantics<'a, RootDatabase>,
138 resolve: &'a AssistResolveStrategy,
139}
140
141pub fn diagnostics(
142 db: &RootDatabase,
143 config: &DiagnosticsConfig,
144 resolve: &AssistResolveStrategy,
145 file_id: FileId,
146) -> Vec<Diagnostic> {
147 let _p = profile::span("diagnostics");
148 let sema = Semantics::new(db);
149 let parse = db.parse(file_id);
150 let mut res = Vec::new();
151
152 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
153 res.extend(
154 parse.errors().iter().take(128).map(|err| {
155 Diagnostic::new("syntax-error", format!("Syntax Error: {}", err), err.range())
156 }),
157 );
158
159 for node in parse.tree().syntax().descendants() {
160 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
161 field_shorthand::check(&mut res, file_id, &node);
162 }
163
164 let mut diags = Vec::new();
165 let module = sema.to_module_def(file_id);
166 if let Some(m) = module {
167 m.diagnostics(db, &mut diags)
168 }
169
170 let ctx = DiagnosticsContext { config, sema, resolve };
171 if module.is_none() {
172 let d = UnlinkedFile { file: file_id };
173 let d = handlers::unlinked_file::unlinked_file(&ctx, &d);
174 res.push(d)
175 }
176
177 for diag in diags {
178 #[rustfmt::skip]
179 let d = match diag {
180 AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
181 AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
182 AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
183 AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
184 AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
185 AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
186 AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => handlers::missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d),
187 AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
188 AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
189 AnyDiagnostic::RemoveThisSemicolon(d) => handlers::remove_this_semicolon::remove_this_semicolon(&ctx, &d),
190 AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
191 AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
192 AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
193 AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d),
194 AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d),
195 AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
196 AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d),
197
198 AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
199 Some(it) => it,
200 None => continue,
201 }
202 };
203 res.push(d)
204 }
205
206 res.retain(|d| {
207 !ctx.config.disabled.contains(d.code.as_str())
208 && !(ctx.config.disable_experimental && d.experimental)
209 });
210
211 res
212}
213
214fn check_unnecessary_braces_in_use_statement(
215 acc: &mut Vec<Diagnostic>,
216 file_id: FileId,
217 node: &SyntaxNode,
218) -> Option<()> {
219 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
220 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
221 // If there is a comment inside the bracketed `use`,
222 // assume it is a commented out module path and don't show diagnostic.
223 if use_tree_list.has_inner_comment() {
224 return Some(());
225 }
226
227 let use_range = use_tree_list.syntax().text_range();
228 let edit =
229 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
230 .unwrap_or_else(|| {
231 let to_replace = single_use_tree.syntax().text().to_string();
232 let mut edit_builder = TextEdit::builder();
233 edit_builder.delete(use_range);
234 edit_builder.insert(use_range.start(), to_replace);
235 edit_builder.finish()
236 });
237
238 acc.push(
239 Diagnostic::new(
240 "unnecessary-braces",
241 "Unnecessary braces in use statement".to_string(),
242 use_range,
243 )
244 .severity(Severity::WeakWarning)
245 .with_fixes(Some(vec![fix(
246 "remove_braces",
247 "Remove unnecessary braces",
248 SourceChange::from_text_edit(file_id, edit),
249 use_range,
250 )])),
251 );
252 }
253
254 Some(())
255}
256
257fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
258 single_use_tree: &ast::UseTree,
259) -> Option<TextEdit> {
260 let use_tree_list_node = single_use_tree.syntax().parent()?;
261 if single_use_tree.path()?.segment()?.self_token().is_some() {
262 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
263 let end = use_tree_list_node.text_range().end();
264 return Some(TextEdit::delete(TextRange::new(start, end)));
265 }
266 None
267}
268
269fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
270 let mut res = unresolved_fix(id, label, target);
271 res.source_change = Some(source_change);
272 res
273}
274
275fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
276 assert!(!id.contains(' '));
277 Assist {
278 id: AssistId(id, AssistKind::QuickFix),
279 label: Label::new(label),
280 group: None,
281 target,
282 source_change: None,
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use expect_test::Expect;
289 use ide_db::{
290 assists::AssistResolveStrategy,
291 base_db::{fixture::WithFixture, SourceDatabaseExt},
292 RootDatabase,
293 };
294 use stdx::trim_indent;
295 use test_utils::{assert_eq_text, extract_annotations};
296
297 use crate::DiagnosticsConfig;
298
299 /// Takes a multi-file input fixture with annotated cursor positions,
300 /// and checks that:
301 /// * a diagnostic is produced
302 /// * the first diagnostic fix trigger range touches the input cursor position
303 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
304 #[track_caller]
305 pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
306 check_nth_fix(0, ra_fixture_before, ra_fixture_after);
307 }
308 /// Takes a multi-file input fixture with annotated cursor positions,
309 /// and checks that:
310 /// * a diagnostic is produced
311 /// * every diagnostic fixes trigger range touches the input cursor position
312 /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
313 pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
314 for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
315 check_nth_fix(i, ra_fixture_before, ra_fixture_after)
316 }
317 }
318
319 #[track_caller]
320 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
321 let after = trim_indent(ra_fixture_after);
322
323 let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
324 let diagnostic = super::diagnostics(
325 &db,
326 &DiagnosticsConfig::default(),
327 &AssistResolveStrategy::All,
328 file_position.file_id,
329 )
330 .pop()
331 .expect("no diagnostics");
332 let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth];
333 let actual = {
334 let source_change = fix.source_change.as_ref().unwrap();
335 let file_id = *source_change.source_file_edits.keys().next().unwrap();
336 let mut actual = db.file_text(file_id).to_string();
337
338 for edit in source_change.source_file_edits.values() {
339 edit.apply(&mut actual);
340 }
341 actual
342 };
343
344 assert_eq_text!(&after, &actual);
345 assert!(
346 fix.target.contains_inclusive(file_position.offset),
347 "diagnostic fix range {:?} does not touch cursor position {:?}",
348 fix.target,
349 file_position.offset
350 );
351 }
352
353 /// Checks that there's a diagnostic *without* fix at `$0`.
354 pub(crate) fn check_no_fix(ra_fixture: &str) {
355 let (db, file_position) = RootDatabase::with_position(ra_fixture);
356 let diagnostic = super::diagnostics(
357 &db,
358 &DiagnosticsConfig::default(),
359 &AssistResolveStrategy::All,
360 file_position.file_id,
361 )
362 .pop()
363 .unwrap();
364 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
365 }
366
367 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
368 let (db, file_id) = RootDatabase::with_single_file(ra_fixture);
369 let diagnostics = super::diagnostics(
370 &db,
371 &DiagnosticsConfig::default(),
372 &AssistResolveStrategy::All,
373 file_id,
374 );
375 expect.assert_debug_eq(&diagnostics)
376 }
377
378 #[track_caller]
379 pub(crate) fn check_diagnostics(ra_fixture: &str) {
380 let mut config = DiagnosticsConfig::default();
381 config.disabled.insert("inactive-code".to_string());
382 check_diagnostics_with_config(config, ra_fixture)
383 }
384
385 #[track_caller]
386 pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
387 let (db, files) = RootDatabase::with_many_files(ra_fixture);
388 for file_id in files {
389 let diagnostics =
390 super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
391
392 let expected = extract_annotations(&*db.file_text(file_id));
393 let mut actual =
394 diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>();
395 actual.sort_by_key(|(range, _)| range.start());
396 assert_eq!(expected, actual);
397 }
398 }
399
400 #[test]
401 fn test_check_unnecessary_braces_in_use_statement() {
402 check_diagnostics(
403 r#"
404use a;
405use a::{c, d::e};
406
407mod a {
408 mod c {}
409 mod d {
410 mod e {}
411 }
412}
413"#,
414 );
415 check_diagnostics(
416 r#"
417use a;
418use a::{
419 c,
420 // d::e
421};
422
423mod a {
424 mod c {}
425 mod d {
426 mod e {}
427 }
428}
429"#,
430 );
431 check_fix(
432 r"
433 mod b {}
434 use {$0b};
435 ",
436 r"
437 mod b {}
438 use b;
439 ",
440 );
441 check_fix(
442 r"
443 mod b {}
444 use {b$0};
445 ",
446 r"
447 mod b {}
448 use b;
449 ",
450 );
451 check_fix(
452 r"
453 mod a { mod c {} }
454 use a::{c$0};
455 ",
456 r"
457 mod a { mod c {} }
458 use a::c;
459 ",
460 );
461 check_fix(
462 r"
463 mod a {}
464 use a::{self$0};
465 ",
466 r"
467 mod a {}
468 use a;
469 ",
470 );
471 check_fix(
472 r"
473 mod a { mod c {} mod d { mod e {} } }
474 use a::{c, d::{e$0}};
475 ",
476 r"
477 mod a { mod c {} mod d { mod e {} } }
478 use a::{c, d::e};
479 ",
480 );
481 }
482
483 #[test]
484 fn test_disabled_diagnostics() {
485 let mut config = DiagnosticsConfig::default();
486 config.disabled.insert("unresolved-module".into());
487
488 let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
489
490 let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
491 assert!(diagnostics.is_empty());
492
493 let diagnostics = super::diagnostics(
494 &db,
495 &DiagnosticsConfig::default(),
496 &AssistResolveStrategy::All,
497 file_id,
498 );
499 assert!(!diagnostics.is_empty());
500 }
501
502 #[test]
503 fn import_extern_crate_clash_with_inner_item() {
504 // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
505
506 check_diagnostics(
507 r#"
508//- /lib.rs crate:lib deps:jwt
509mod permissions;
510
511use permissions::jwt;
512
513fn f() {
514 fn inner() {}
515 jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
516}
517
518//- /permissions.rs
519pub mod jwt {
520 pub struct Claims {}
521}
522
523//- /jwt/lib.rs crate:jwt
524pub struct Claims {
525 field: u8,
526}
527 "#,
528 );
529 }
530}