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 use std::cell::RefCell;
10 diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink},
13 use itertools::Itertools;
14 use ra_db::{RelativePath, SourceDatabase, SourceDatabaseExt};
15 use ra_ide_db::RootDatabase;
19 ast::{self, make, AstNode},
20 SyntaxNode, TextRange, T,
22 use ra_text_edit::{TextEdit, TextEditBuilder};
24 use crate::{Diagnostic, FileId, FileSystemEdit, SourceChange, SourceFileEdit};
26 #[derive(Debug, Copy, Clone)]
32 pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> {
33 let _p = profile("diagnostics");
34 let sema = Semantics::new(db);
35 let parse = db.parse(file_id);
36 let mut res = Vec::new();
38 res.extend(parse.errors().iter().map(|err| Diagnostic {
40 message: format!("Syntax Error: {}", err),
41 severity: Severity::Error,
45 for node in parse.tree().syntax().descendants() {
46 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
47 check_struct_shorthand_initialization(&mut res, file_id, &node);
49 let res = RefCell::new(res);
50 let mut sink = DiagnosticSink::new(|d| {
51 res.borrow_mut().push(Diagnostic {
53 range: sema.diagnostics_range(d).range,
54 severity: Severity::Error,
58 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
59 let original_file = d.source().file_id.original_file(db);
60 let source_root = db.file_source_root(original_file);
62 .file_relative_path(original_file)
64 .unwrap_or_else(|| RelativePath::new(""))
66 let create_file = FileSystemEdit::CreateFile { source_root, path };
67 let fix = SourceChange::file_system_edit("Create module", create_file);
68 res.borrow_mut().push(Diagnostic {
69 range: sema.diagnostics_range(d).range,
71 severity: Severity::Error,
75 .on::<hir::diagnostics::MissingFields, _>(|d| {
76 // Note that although we could add a diagnostics to
77 // fill the missing tuple field, e.g :
79 // `let a = A { 0: () }`
80 // but it is uncommon usage and it should not be encouraged.
81 let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
84 let mut field_list = d.ast(db);
85 for f in d.missed_fields.iter() {
87 make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit()));
88 field_list = field_list.append_field(&field);
91 let mut builder = TextEditBuilder::default();
92 algo::diff(&d.ast(db).syntax(), &field_list.syntax()).into_text_edit(&mut builder);
94 Some(SourceChange::source_file_edit_from(
101 res.borrow_mut().push(Diagnostic {
102 range: sema.diagnostics_range(d).range,
103 message: d.message(),
104 severity: Severity::Error,
108 .on::<hir::diagnostics::MissingMatchArms, _>(|d| {
109 res.borrow_mut().push(Diagnostic {
110 range: sema.diagnostics_range(d).range,
111 message: d.message(),
112 severity: Severity::Error,
116 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
117 let node = d.ast(db);
118 let replacement = format!("Ok({})", node.syntax());
119 let edit = TextEdit::replace(node.syntax().text_range(), replacement);
120 let fix = SourceChange::source_file_edit_from("Wrap with ok", file_id, edit);
121 res.borrow_mut().push(Diagnostic {
122 range: sema.diagnostics_range(d).range,
123 message: d.message(),
124 severity: Severity::Error,
128 if let Some(m) = sema.to_module_def(file_id) {
129 m.diagnostics(db, &mut sink);
135 fn check_unnecessary_braces_in_use_statement(
136 acc: &mut Vec<Diagnostic>,
140 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
141 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
142 let range = use_tree_list.syntax().text_range();
144 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
146 let to_replace = single_use_tree.syntax().text().to_string();
147 let mut edit_builder = TextEditBuilder::default();
148 edit_builder.delete(range);
149 edit_builder.insert(range.start(), to_replace);
150 edit_builder.finish()
153 acc.push(Diagnostic {
155 message: "Unnecessary braces in use statement".to_string(),
156 severity: Severity::WeakWarning,
157 fix: Some(SourceChange::source_file_edit(
158 "Remove unnecessary braces",
159 SourceFileEdit { file_id, edit },
167 fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
168 single_use_tree: &ast::UseTree,
169 ) -> Option<TextEdit> {
170 let use_tree_list_node = single_use_tree.syntax().parent()?;
171 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
172 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
173 let end = use_tree_list_node.text_range().end();
174 let range = TextRange::new(start, end);
175 return Some(TextEdit::delete(range));
180 fn check_struct_shorthand_initialization(
181 acc: &mut Vec<Diagnostic>,
185 let record_lit = ast::RecordLit::cast(node.clone())?;
186 let record_field_list = record_lit.record_field_list()?;
187 for record_field in record_field_list.fields() {
188 if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) {
189 let field_name = name_ref.syntax().text().to_string();
190 let field_expr = expr.syntax().text().to_string();
191 if field_name == field_expr {
192 let mut edit_builder = TextEditBuilder::default();
193 edit_builder.delete(record_field.syntax().text_range());
194 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
195 let edit = edit_builder.finish();
197 acc.push(Diagnostic {
198 range: record_field.syntax().text_range(),
199 message: "Shorthand struct initialization".to_string(),
200 severity: Severity::WeakWarning,
201 fix: Some(SourceChange::source_file_edit(
202 "Use struct shorthand initialization",
203 SourceFileEdit { file_id, edit },
214 use insta::assert_debug_snapshot;
215 use ra_syntax::SourceFile;
217 use test_utils::assert_eq_text;
219 use crate::mock_analysis::{analysis_and_position, single_file};
223 type DiagnosticChecker = fn(&mut Vec<Diagnostic>, FileId, &SyntaxNode) -> Option<()>;
225 fn check_not_applicable(code: &str, func: DiagnosticChecker) {
226 let parse = SourceFile::parse(code);
227 let mut diagnostics = Vec::new();
228 for node in parse.tree().syntax().descendants() {
229 func(&mut diagnostics, FileId(0), &node);
231 assert!(diagnostics.is_empty());
234 fn check_apply(before: &str, after: &str, func: DiagnosticChecker) {
235 let parse = SourceFile::parse(before);
236 let mut diagnostics = Vec::new();
237 for node in parse.tree().syntax().descendants() {
238 func(&mut diagnostics, FileId(0), &node);
241 diagnostics.pop().unwrap_or_else(|| panic!("no diagnostics for:\n{}\n", before));
242 let mut fix = diagnostic.fix.unwrap();
243 let edit = fix.source_file_edits.pop().unwrap().edit;
245 let mut actual = before.to_string();
246 edit.apply(&mut actual);
249 assert_eq_text!(after, &actual);
252 /// Takes a multi-file input fixture with annotated cursor positions,
254 /// * a diagnostic is produced
255 /// * this diagnostic touches the input cursor position
256 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
257 fn check_apply_diagnostic_fix_from_position(fixture: &str, after: &str) {
258 let (analysis, file_position) = analysis_and_position(fixture);
259 let diagnostic = analysis.diagnostics(file_position.file_id).unwrap().pop().unwrap();
260 let mut fix = diagnostic.fix.unwrap();
261 let edit = fix.source_file_edits.pop().unwrap().edit;
262 let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
264 let mut actual = target_file_contents.to_string();
265 edit.apply(&mut actual);
269 // Strip indent and empty lines from `after`, to match the behaviour of
270 // `parse_fixture` called from `analysis_and_position`.
273 .filter(|it| it.trim_start().starts_with("//-"))
274 .map(|it| it.len() - it.trim_start().len())
276 .expect("empty fixture");
279 .filter_map(|line| if line.len() > margin { Some(&line[margin..]) } else { None })
284 assert_eq_text!(&after, &actual);
286 diagnostic.range.start() <= file_position.offset
287 && diagnostic.range.end() >= file_position.offset,
288 "diagnostic range {:?} does not touch cursor position {:?}",
294 fn check_apply_diagnostic_fix(before: &str, after: &str) {
295 let (analysis, file_id) = single_file(before);
296 let diagnostic = analysis.diagnostics(file_id).unwrap().pop().unwrap();
297 let mut fix = diagnostic.fix.unwrap();
298 let edit = fix.source_file_edits.pop().unwrap().edit;
300 let mut actual = before.to_string();
301 edit.apply(&mut actual);
304 assert_eq_text!(after, &actual);
307 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
308 /// apply to the file containing the cursor.
309 fn check_no_diagnostic_for_target_file(fixture: &str) {
310 let (analysis, file_position) = analysis_and_position(fixture);
311 let diagnostics = analysis.diagnostics(file_position.file_id).unwrap();
312 assert_eq!(diagnostics.len(), 0);
315 fn check_no_diagnostic(content: &str) {
316 let (analysis, file_id) = single_file(content);
317 let diagnostics = analysis.diagnostics(file_id).unwrap();
318 assert_eq!(diagnostics.len(), 0, "expected no diagnostic, found one");
322 fn test_wrap_return_type() {
325 use std::{string::String, result::Result::{self, Ok, Err}};
327 fn div(x: i32, y: i32) -> Result<i32, String> {
329 return Err("div by zero".into());
336 pub struct String { }
339 pub enum Result<T, E> { Ok(T), Err(E) }
343 use std::{string::String, result::Result::{self, Ok, Err}};
345 fn div(x: i32, y: i32) -> Result<i32, String> {
347 return Err("div by zero".into());
352 check_apply_diagnostic_fix_from_position(before, after);
356 fn test_wrap_return_type_handles_generic_functions() {
359 use std::result::Result::{self, Ok, Err};
361 fn div<T>(x: T) -> Result<T, i32> {
370 pub enum Result<T, E> { Ok(T), Err(E) }
374 use std::result::Result::{self, Ok, Err};
376 fn div<T>(x: T) -> Result<T, i32> {
383 check_apply_diagnostic_fix_from_position(before, after);
387 fn test_wrap_return_type_handles_type_aliases() {
390 use std::{string::String, result::Result::{self, Ok, Err}};
392 type MyResult<T> = Result<T, String>;
394 fn div(x: i32, y: i32) -> MyResult<i32> {
396 return Err("div by zero".into());
403 pub struct String { }
406 pub enum Result<T, E> { Ok(T), Err(E) }
410 use std::{string::String, result::Result::{self, Ok, Err}};
412 type MyResult<T> = Result<T, String>;
413 fn div(x: i32, y: i32) -> MyResult<i32> {
415 return Err("div by zero".into());
420 check_apply_diagnostic_fix_from_position(before, after);
424 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
427 use std::{string::String, result::Result::{self, Ok, Err}};
429 fn foo() -> Result<String, i32> {
435 pub struct String { }
438 pub enum Result<T, E> { Ok(T), Err(E) }
441 check_no_diagnostic_for_target_file(content);
445 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() {
448 use std::{string::String, result::Result::{self, Ok, Err}};
455 fn foo() -> SomeOtherEnum {
461 pub struct String { }
464 pub enum Result<T, E> { Ok(T), Err(E) }
467 check_no_diagnostic_for_target_file(content);
471 fn test_fill_struct_fields_empty() {
479 let s = TestStruct{};
489 let s = TestStruct{ one: (), two: ()};
492 check_apply_diagnostic_fix(before, after);
496 fn test_fill_struct_fields_self() {
515 let s = Self { one: ()};
519 check_apply_diagnostic_fix(before, after);
523 fn test_fill_struct_fields_enum() {
526 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
530 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
538 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
542 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
543 Expr::Bin { lhs: (), rhs: () <|> }
548 check_apply_diagnostic_fix(before, after);
552 fn test_fill_struct_fields_partial() {
560 let s = TestStruct{ two: 2 };
570 let s = TestStruct{ two: 2, one: () };
573 check_apply_diagnostic_fix(before, after);
577 fn test_fill_struct_fields_no_diagnostic() {
586 let s = TestStruct{ one, two: 2 };
590 check_no_diagnostic(content);
594 fn test_fill_struct_fields_no_diagnostic_on_spread() {
603 let s = TestStruct{ ..a };
607 check_no_diagnostic(content);
611 fn test_unresolved_module_diagnostic() {
612 let (analysis, file_id) = single_file("mod foo;");
613 let diagnostics = analysis.diagnostics(file_id).unwrap();
614 assert_debug_snapshot!(diagnostics, @r###"
617 message: "unresolved module",
621 label: "Create module",
622 source_file_edits: [],
625 source_root: SourceRootId(
631 cursor_position: None,
642 fn range_mapping_out_of_macros() {
643 let (analysis, file_id) = single_file(
650 ($($tt:tt)*) => { $($tt)*};
654 let _x = id![Foo { a: 42 }];
663 let diagnostics = analysis.diagnostics(file_id).unwrap();
664 assert_debug_snapshot!(diagnostics, @r###"
667 message: "Missing structure fields:\n- b",
671 label: "Fill struct fields",
680 insert: "{a:42, b: ()}",
687 file_system_edits: [],
688 cursor_position: None,
699 fn test_check_unnecessary_braces_in_use_statement() {
700 check_not_applicable(
705 check_unnecessary_braces_in_use_statement,
707 check_apply("use {b};", "use b;", check_unnecessary_braces_in_use_statement);
708 check_apply("use a::{c};", "use a::c;", check_unnecessary_braces_in_use_statement);
709 check_apply("use a::{self};", "use a;", check_unnecessary_braces_in_use_statement);
711 "use a::{c, d::{e}};",
713 check_unnecessary_braces_in_use_statement,
718 fn test_check_struct_shorthand_initialization() {
719 check_not_applicable(
731 check_struct_shorthand_initialization,
759 check_struct_shorthand_initialization,
793 check_struct_shorthand_initialization,