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