]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/diagnostics.rs
internal: use cov-mark rather than bailing out diagnostic
[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 module = sema.to_module_def(file_id);
185     if let Some(m) = module {
186         diags = m.diagnostics(db, &mut sink)
187     }
188
189     drop(sink);
190
191     let mut res = res.into_inner();
192
193     let ctx = DiagnosticsContext { config, sema, resolve };
194     if module.is_none() {
195         let d = UnlinkedFile { file: file_id };
196         let d = unlinked_file::unlinked_file(&ctx, &d);
197         res.push(d)
198     }
199
200     for diag in diags {
201         #[rustfmt::skip]
202         let d = match diag {
203             AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d),
204             AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d),
205             AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d),
206             AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d),
207             AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d),
208             AnyDiagnostic::MissingMatchArms(d) => missing_match_arms::missing_match_arms(&ctx, &d),
209             AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d),
210             AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d),
211             AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d),
212             AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d),
213             AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
214             AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
215             AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
216             AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d),
217             AnyDiagnostic::UnresolvedMacroCall(d) => unresolved_macro_call::unresolved_macro_call(&ctx, &d),
218             AnyDiagnostic::UnresolvedModule(d) => unresolved_module::unresolved_module(&ctx, &d),
219             AnyDiagnostic::UnresolvedProcMacro(d) => unresolved_proc_macro::unresolved_proc_macro(&ctx, &d),
220
221             AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) {
222                 Some(it) => it,
223                 None => continue,
224             }
225         };
226         res.push(d)
227     }
228
229     res.retain(|d| {
230         if let Some(code) = d.code {
231             if ctx.config.disabled.contains(code.as_str()) {
232                 return false;
233             }
234         }
235         if ctx.config.disable_experimental && d.experimental {
236             return false;
237         }
238         true
239     });
240
241     res
242 }
243
244 fn check_unnecessary_braces_in_use_statement(
245     acc: &mut Vec<Diagnostic>,
246     file_id: FileId,
247     node: &SyntaxNode,
248 ) -> Option<()> {
249     let use_tree_list = ast::UseTreeList::cast(node.clone())?;
250     if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
251         // If there is a comment inside the bracketed `use`,
252         // assume it is a commented out module path and don't show diagnostic.
253         if use_tree_list.has_inner_comment() {
254             return Some(());
255         }
256
257         let use_range = use_tree_list.syntax().text_range();
258         let edit =
259             text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
260                 .unwrap_or_else(|| {
261                     let to_replace = single_use_tree.syntax().text().to_string();
262                     let mut edit_builder = TextEdit::builder();
263                     edit_builder.delete(use_range);
264                     edit_builder.insert(use_range.start(), to_replace);
265                     edit_builder.finish()
266                 });
267
268         acc.push(
269             Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
270                 .with_fixes(Some(vec![fix(
271                     "remove_braces",
272                     "Remove unnecessary braces",
273                     SourceChange::from_text_edit(file_id, edit),
274                     use_range,
275                 )])),
276         );
277     }
278
279     Some(())
280 }
281
282 fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
283     single_use_tree: &ast::UseTree,
284 ) -> Option<TextEdit> {
285     let use_tree_list_node = single_use_tree.syntax().parent()?;
286     if single_use_tree.path()?.segment()?.self_token().is_some() {
287         let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
288         let end = use_tree_list_node.text_range().end();
289         return Some(TextEdit::delete(TextRange::new(start, end)));
290     }
291     None
292 }
293
294 fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
295     let mut res = unresolved_fix(id, label, target);
296     res.source_change = Some(source_change);
297     res
298 }
299
300 fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
301     assert!(!id.contains(' '));
302     Assist {
303         id: AssistId(id, AssistKind::QuickFix),
304         label: Label::new(label),
305         group: None,
306         target,
307         source_change: None,
308     }
309 }
310
311 #[cfg(test)]
312 mod tests {
313     use expect_test::Expect;
314     use ide_assists::AssistResolveStrategy;
315     use stdx::trim_indent;
316     use test_utils::{assert_eq_text, extract_annotations};
317
318     use crate::{fixture, DiagnosticsConfig};
319
320     /// Takes a multi-file input fixture with annotated cursor positions,
321     /// and checks that:
322     ///  * a diagnostic is produced
323     ///  * the first diagnostic fix trigger range touches the input cursor position
324     ///  * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
325     #[track_caller]
326     pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
327         check_nth_fix(0, ra_fixture_before, ra_fixture_after);
328     }
329     /// Takes a multi-file input fixture with annotated cursor positions,
330     /// and checks that:
331     ///  * a diagnostic is produced
332     ///  * every diagnostic fixes trigger range touches the input cursor position
333     ///  * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
334     pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
335         for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
336             check_nth_fix(i, ra_fixture_before, ra_fixture_after)
337         }
338     }
339
340     #[track_caller]
341     fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
342         let after = trim_indent(ra_fixture_after);
343
344         let (analysis, file_position) = fixture::position(ra_fixture_before);
345         let diagnostic = analysis
346             .diagnostics(
347                 &DiagnosticsConfig::default(),
348                 AssistResolveStrategy::All,
349                 file_position.file_id,
350             )
351             .unwrap()
352             .pop()
353             .unwrap();
354         let fix = &diagnostic.fixes.unwrap()[nth];
355         let actual = {
356             let source_change = fix.source_change.as_ref().unwrap();
357             let file_id = *source_change.source_file_edits.keys().next().unwrap();
358             let mut actual = analysis.file_text(file_id).unwrap().to_string();
359
360             for edit in source_change.source_file_edits.values() {
361                 edit.apply(&mut actual);
362             }
363             actual
364         };
365
366         assert_eq_text!(&after, &actual);
367         assert!(
368             fix.target.contains_inclusive(file_position.offset),
369             "diagnostic fix range {:?} does not touch cursor position {:?}",
370             fix.target,
371             file_position.offset
372         );
373     }
374
375     /// Checks that there's a diagnostic *without* fix at `$0`.
376     pub(crate) fn check_no_fix(ra_fixture: &str) {
377         let (analysis, file_position) = fixture::position(ra_fixture);
378         let diagnostic = analysis
379             .diagnostics(
380                 &DiagnosticsConfig::default(),
381                 AssistResolveStrategy::All,
382                 file_position.file_id,
383             )
384             .unwrap()
385             .pop()
386             .unwrap();
387         assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
388     }
389
390     pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
391         let (analysis, file_id) = fixture::file(ra_fixture);
392         let diagnostics = analysis
393             .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
394             .unwrap();
395         expect.assert_debug_eq(&diagnostics)
396     }
397
398     #[track_caller]
399     pub(crate) fn check_diagnostics(ra_fixture: &str) {
400         let mut config = DiagnosticsConfig::default();
401         config.disabled.insert("inactive-code".to_string());
402         check_diagnostics_with_config(config, ra_fixture)
403     }
404
405     #[track_caller]
406     pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
407         let (analysis, files) = fixture::files(ra_fixture);
408         for file_id in files {
409             let diagnostics =
410                 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
411
412             let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
413             let mut actual =
414                 diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>();
415             actual.sort_by_key(|(range, _)| range.start());
416             assert_eq!(expected, actual);
417         }
418     }
419
420     #[test]
421     fn test_check_unnecessary_braces_in_use_statement() {
422         check_diagnostics(
423             r#"
424 use a;
425 use a::{c, d::e};
426
427 mod a {
428     mod c {}
429     mod d {
430         mod e {}
431     }
432 }
433 "#,
434         );
435         check_diagnostics(
436             r#"
437 use a;
438 use a::{
439     c,
440     // d::e
441 };
442
443 mod a {
444     mod c {}
445     mod d {
446         mod e {}
447     }
448 }
449 "#,
450         );
451         check_fix(
452             r"
453             mod b {}
454             use {$0b};
455             ",
456             r"
457             mod b {}
458             use b;
459             ",
460         );
461         check_fix(
462             r"
463             mod b {}
464             use {b$0};
465             ",
466             r"
467             mod b {}
468             use b;
469             ",
470         );
471         check_fix(
472             r"
473             mod a { mod c {} }
474             use a::{c$0};
475             ",
476             r"
477             mod a { mod c {} }
478             use a::c;
479             ",
480         );
481         check_fix(
482             r"
483             mod a {}
484             use a::{self$0};
485             ",
486             r"
487             mod a {}
488             use a;
489             ",
490         );
491         check_fix(
492             r"
493             mod a { mod c {} mod d { mod e {} } }
494             use a::{c, d::{e$0}};
495             ",
496             r"
497             mod a { mod c {} mod d { mod e {} } }
498             use a::{c, d::e};
499             ",
500         );
501     }
502
503     #[test]
504     fn test_disabled_diagnostics() {
505         let mut config = DiagnosticsConfig::default();
506         config.disabled.insert("unresolved-module".into());
507
508         let (analysis, file_id) = fixture::file(r#"mod foo;"#);
509
510         let diagnostics =
511             analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
512         assert!(diagnostics.is_empty());
513
514         let diagnostics = analysis
515             .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
516             .unwrap();
517         assert!(!diagnostics.is_empty());
518     }
519
520     #[test]
521     fn import_extern_crate_clash_with_inner_item() {
522         // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
523
524         check_diagnostics(
525             r#"
526 //- /lib.rs crate:lib deps:jwt
527 mod permissions;
528
529 use permissions::jwt;
530
531 fn f() {
532     fn inner() {}
533     jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
534 }
535
536 //- /permissions.rs
537 pub mod jwt  {
538     pub struct Claims {}
539 }
540
541 //- /jwt/lib.rs crate:jwt
542 pub struct Claims {
543     field: u8,
544 }
545         "#,
546         );
547     }
548 }