1 //! Collects diagnostics & fixits for a single file.
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.
7 mod break_outside_of_loop;
11 mod mismatched_arg_count;
13 mod missing_match_arms;
14 mod missing_ok_or_some_in_tail_expr;
17 mod remove_this_semicolon;
18 mod replace_filter_map_next_with_find_map;
19 mod unimplemented_builtin_macro;
21 mod unresolved_extern_crate;
22 mod unresolved_import;
23 mod unresolved_macro_call;
24 mod unresolved_module;
25 mod unresolved_proc_macro;
29 use std::cell::RefCell;
32 diagnostics::{AnyDiagnostic, DiagnosticCode, DiagnosticSinkBuilder},
35 use ide_assists::AssistResolveStrategy;
36 use ide_db::{base_db::SourceDatabase, RootDatabase};
37 use itertools::Itertools;
38 use rustc_hash::FxHashSet;
41 SyntaxNode, TextRange,
43 use text_edit::TextEdit;
44 use unlinked_file::UnlinkedFile;
46 use crate::{Assist, AssistId, AssistKind, FileId, Label, SourceChange};
49 pub struct Diagnostic {
50 // pub name: Option<String>,
53 pub severity: Severity,
54 pub fixes: Option<Vec<Assist>>,
56 pub code: Option<DiagnosticCode>,
57 pub experimental: bool,
61 fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
62 let message = message.into();
63 let code = Some(DiagnosticCode(code));
67 severity: Severity::Error,
75 fn experimental(mut self) -> Diagnostic {
76 self.experimental = true;
80 fn severity(mut self, severity: Severity) -> Diagnostic {
81 self.severity = severity;
85 fn error(range: TextRange, message: String) -> Self {
89 severity: Severity::Error,
97 fn hint(range: TextRange, message: String) -> Self {
101 severity: Severity::WeakWarning,
109 fn with_fixes(self, fixes: Option<Vec<Assist>>) -> Self {
110 Self { fixes, ..self }
113 fn with_unused(self, unused: bool) -> Self {
114 Self { unused, ..self }
117 fn with_code(self, code: Option<DiagnosticCode>) -> Self {
118 Self { code, ..self }
122 #[derive(Debug, Copy, Clone)]
128 #[derive(Default, Debug, Clone)]
129 pub struct DiagnosticsConfig {
130 pub disable_experimental: bool,
131 pub disabled: FxHashSet<String>,
134 struct DiagnosticsContext<'a> {
135 config: &'a DiagnosticsConfig,
136 sema: Semantics<'a, RootDatabase>,
137 resolve: &'a AssistResolveStrategy,
140 pub(crate) fn diagnostics(
142 config: &DiagnosticsConfig,
143 resolve: &AssistResolveStrategy,
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();
151 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
157 .map(|err| Diagnostic::error(err.range(), format!("Syntax Error: {}", err))),
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);
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()));
170 // Finalize the `DiagnosticSink` building process.
171 let mut sink = sink_builder
172 // Diagnostics not handled above get no fix and default treatment.
174 res.borrow_mut().push(
176 sema.diagnostics_display_range(d.display_source()).range,
179 .with_code(Some(d.code())),
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)
192 let mut res = res.into_inner();
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);
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),
222 AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) {
231 if let Some(code) = d.code {
232 if ctx.config.disabled.contains(code.as_str()) {
236 if ctx.config.disable_experimental && d.experimental {
245 fn check_unnecessary_braces_in_use_statement(
246 acc: &mut Vec<Diagnostic>,
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() {
258 let use_range = use_tree_list.syntax().text_range();
260 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
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()
270 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
271 .with_fixes(Some(vec![fix(
273 "Remove unnecessary braces",
274 SourceChange::from_text_edit(file_id, edit),
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)));
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);
301 fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
302 assert!(!id.contains(' '));
304 id: AssistId(id, AssistKind::QuickFix),
305 label: Label::new(label),
314 use expect_test::Expect;
315 use ide_assists::AssistResolveStrategy;
316 use stdx::trim_indent;
317 use test_utils::{assert_eq_text, extract_annotations};
319 use crate::{fixture, DiagnosticsConfig};
321 /// Takes a multi-file input fixture with annotated cursor positions,
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
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);
330 /// Takes a multi-file input fixture with annotated cursor positions,
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)
342 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
343 let after = trim_indent(ra_fixture_after);
345 let (analysis, file_position) = fixture::position(ra_fixture_before);
346 let diagnostic = analysis
348 &DiagnosticsConfig::default(),
349 AssistResolveStrategy::All,
350 file_position.file_id,
355 let fix = &diagnostic.fixes.unwrap()[nth];
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();
361 for edit in source_change.source_file_edits.values() {
362 edit.apply(&mut actual);
367 assert_eq_text!(&after, &actual);
369 fix.target.contains_inclusive(file_position.offset),
370 "diagnostic fix range {:?} does not touch cursor position {:?}",
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
381 &DiagnosticsConfig::default(),
382 AssistResolveStrategy::All,
383 file_position.file_id,
388 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
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)
396 expect.assert_debug_eq(&diagnostics)
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)
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 {
411 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
413 let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
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);
422 fn test_check_unnecessary_braces_in_use_statement() {
494 mod a { mod c {} mod d { mod e {} } }
495 use a::{c, d::{e$0}};
498 mod a { mod c {} mod d { mod e {} } }
505 fn test_disabled_diagnostics() {
506 let mut config = DiagnosticsConfig::default();
507 config.disabled.insert("unresolved-module".into());
509 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
512 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
513 assert!(diagnostics.is_empty());
515 let diagnostics = analysis
516 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
518 assert!(!diagnostics.is_empty());
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.
527 //- /lib.rs crate:lib deps:jwt
530 use permissions::jwt;
534 jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
542 //- /jwt/lib.rs crate:jwt