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 module = sema.to_module_def(file_id);
185 if let Some(m) = module {
186 diags = m.diagnostics(db, &mut sink)
191 let mut res = res.into_inner();
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);
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),
221 AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) {
230 if let Some(code) = d.code {
231 if ctx.config.disabled.contains(code.as_str()) {
235 if ctx.config.disable_experimental && d.experimental {
244 fn check_unnecessary_braces_in_use_statement(
245 acc: &mut Vec<Diagnostic>,
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() {
257 let use_range = use_tree_list.syntax().text_range();
259 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
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()
269 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
270 .with_fixes(Some(vec![fix(
272 "Remove unnecessary braces",
273 SourceChange::from_text_edit(file_id, edit),
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)));
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);
300 fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
301 assert!(!id.contains(' '));
303 id: AssistId(id, AssistKind::QuickFix),
304 label: Label::new(label),
313 use expect_test::Expect;
314 use ide_assists::AssistResolveStrategy;
315 use stdx::trim_indent;
316 use test_utils::{assert_eq_text, extract_annotations};
318 use crate::{fixture, DiagnosticsConfig};
320 /// Takes a multi-file input fixture with annotated cursor positions,
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
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);
329 /// Takes a multi-file input fixture with annotated cursor positions,
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)
341 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
342 let after = trim_indent(ra_fixture_after);
344 let (analysis, file_position) = fixture::position(ra_fixture_before);
345 let diagnostic = analysis
347 &DiagnosticsConfig::default(),
348 AssistResolveStrategy::All,
349 file_position.file_id,
354 let fix = &diagnostic.fixes.unwrap()[nth];
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();
360 for edit in source_change.source_file_edits.values() {
361 edit.apply(&mut actual);
366 assert_eq_text!(&after, &actual);
368 fix.target.contains_inclusive(file_position.offset),
369 "diagnostic fix range {:?} does not touch cursor position {:?}",
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
380 &DiagnosticsConfig::default(),
381 AssistResolveStrategy::All,
382 file_position.file_id,
387 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
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)
395 expect.assert_debug_eq(&diagnostics)
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)
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 {
410 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
412 let expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
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);
421 fn test_check_unnecessary_braces_in_use_statement() {
493 mod a { mod c {} mod d { mod e {} } }
494 use a::{c, d::{e$0}};
497 mod a { mod c {} mod d { mod e {} } }
504 fn test_disabled_diagnostics() {
505 let mut config = DiagnosticsConfig::default();
506 config.disabled.insert("unresolved-module".into());
508 let (analysis, file_id) = fixture::file(r#"mod foo;"#);
511 analysis.diagnostics(&config, AssistResolveStrategy::All, file_id).unwrap();
512 assert!(diagnostics.is_empty());
514 let diagnostics = analysis
515 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
517 assert!(!diagnostics.is_empty());
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.
526 //- /lib.rs crate:lib deps:jwt
529 use permissions::jwt;
533 jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
541 //- /jwt/lib.rs crate:jwt