aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/diagnostics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/diagnostics.rs')
-rw-r--r--crates/ra_ide/src/diagnostics.rs189
1 files changed, 33 insertions, 156 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs
index 5ce900bf4..79dbb0865 100644
--- a/crates/ra_ide/src/diagnostics.rs
+++ b/crates/ra_ide/src/diagnostics.rs
@@ -6,22 +6,21 @@
6 6
7use std::cell::RefCell; 7use std::cell::RefCell;
8 8
9use hir::{ 9use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
10 diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder},
11 HasSource, HirDisplay, Semantics, VariantDef,
12};
13use itertools::Itertools; 10use itertools::Itertools;
14use ra_db::SourceDatabase; 11use ra_db::SourceDatabase;
15use ra_ide_db::RootDatabase; 12use ra_ide_db::RootDatabase;
16use ra_prof::profile; 13use ra_prof::profile;
17use ra_syntax::{ 14use ra_syntax::{
18 algo, 15 ast::{self, AstNode},
19 ast::{self, edit::IndentLevel, make, AstNode},
20 SyntaxNode, TextRange, T, 16 SyntaxNode, TextRange, T,
21}; 17};
22use ra_text_edit::{TextEdit, TextEditBuilder}; 18use ra_text_edit::{TextEdit, TextEditBuilder};
23 19
24use crate::{AnalysisConfig, Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; 20use crate::{AnalysisConfig, Diagnostic, FileId, Fix, SourceFileEdit};
21
22mod diagnostics_with_fix;
23use diagnostics_with_fix::DiagnosticWithFix;
25 24
26#[derive(Debug, Copy, Clone)] 25#[derive(Debug, Copy, Clone)]
27pub enum Severity { 26pub enum Severity {
@@ -56,77 +55,16 @@ pub(crate) fn diagnostics(
56 let res = RefCell::new(res); 55 let res = RefCell::new(res);
57 let mut sink_builder = DiagnosticSinkBuilder::new() 56 let mut sink_builder = DiagnosticSinkBuilder::new()
58 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 57 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
59 let original_file = d.source().file_id.original_file(db); 58 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
60 let fix = Fix::new(
61 "Create module",
62 FileSystemEdit::CreateFile { anchor: original_file, dst: d.candidate.clone() }
63 .into(),
64 );
65 res.borrow_mut().push(Diagnostic {
66 name: Some(d.name().into()),
67 range: sema.diagnostics_range(d).range,
68 message: d.message(),
69 severity: Severity::Error,
70 fix: Some(fix),
71 })
72 }) 59 })
73 .on::<hir::diagnostics::MissingFields, _>(|d| { 60 .on::<hir::diagnostics::MissingFields, _>(|d| {
74 // Note that although we could add a diagnostics to 61 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
75 // fill the missing tuple field, e.g :
76 // `struct A(usize);`
77 // `let a = A { 0: () }`
78 // but it is uncommon usage and it should not be encouraged.
79 let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
80 None
81 } else {
82 let mut field_list = d.ast(db);
83 for f in d.missed_fields.iter() {
84 let field = make::record_expr_field(
85 make::name_ref(&f.to_string()),
86 Some(make::expr_unit()),
87 );
88 field_list = field_list.append_field(&field);
89 }
90
91 let edit = {
92 let mut builder = TextEditBuilder::default();
93 algo::diff(&d.ast(db).syntax(), &field_list.syntax())
94 .into_text_edit(&mut builder);
95 builder.finish()
96 };
97 Some(Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into()))
98 };
99
100 res.borrow_mut().push(Diagnostic {
101 name: Some(d.name().into()),
102 range: sema.diagnostics_range(d).range,
103 message: d.message(),
104 severity: Severity::Error,
105 fix,
106 })
107 }) 62 })
108 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { 63 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
109 let node = d.ast(db); 64 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
110 let replacement = format!("Ok({})", node.syntax());
111 let edit = TextEdit::replace(node.syntax().text_range(), replacement);
112 let source_change = SourceFileEdit { file_id, edit }.into();
113 let fix = Fix::new("Wrap with ok", source_change);
114 res.borrow_mut().push(Diagnostic {
115 name: Some(d.name().into()),
116 range: sema.diagnostics_range(d).range,
117 message: d.message(),
118 severity: Severity::Error,
119 fix: Some(fix),
120 })
121 }) 65 })
122 .on::<hir::diagnostics::NoSuchField, _>(|d| { 66 .on::<hir::diagnostics::NoSuchField, _>(|d| {
123 res.borrow_mut().push(Diagnostic { 67 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
124 name: Some(d.name().into()),
125 range: sema.diagnostics_range(d).range,
126 message: d.message(),
127 severity: Severity::Error,
128 fix: missing_struct_field_fix(&sema, file_id, d),
129 })
130 }) 68 })
131 // Only collect experimental diagnostics when they're enabled. 69 // Only collect experimental diagnostics when they're enabled.
132 .filter(|diag| !diag.is_experimental() || enable_experimental); 70 .filter(|diag| !diag.is_experimental() || enable_experimental);
@@ -144,7 +82,7 @@ pub(crate) fn diagnostics(
144 res.borrow_mut().push(Diagnostic { 82 res.borrow_mut().push(Diagnostic {
145 name: Some(d.name().into()), 83 name: Some(d.name().into()),
146 message: d.message(), 84 message: d.message(),
147 range: sema.diagnostics_range(d).range, 85 range: sema.diagnostics_display_range(d).range,
148 severity: Severity::Error, 86 severity: Severity::Error,
149 fix: None, 87 fix: None,
150 }) 88 })
@@ -157,77 +95,13 @@ pub(crate) fn diagnostics(
157 res.into_inner() 95 res.into_inner()
158} 96}
159 97
160fn missing_struct_field_fix( 98fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
161 sema: &Semantics<RootDatabase>, 99 Diagnostic {
162 usage_file_id: FileId, 100 name: Some(d.name().into()),
163 d: &hir::diagnostics::NoSuchField, 101 range: sema.diagnostics_display_range(d).range,
164) -> Option<Fix> { 102 message: d.message(),
165 let record_expr = sema.ast(d); 103 severity: Severity::Error,
166 104 fix: d.fix(&sema),
167 let record_lit = ast::RecordExpr::cast(record_expr.syntax().parent()?.parent()?)?;
168 let def_id = sema.resolve_variant(record_lit)?;
169 let module;
170 let def_file_id;
171 let record_fields = match VariantDef::from(def_id) {
172 VariantDef::Struct(s) => {
173 module = s.module(sema.db);
174 let source = s.source(sema.db);
175 def_file_id = source.file_id;
176 let fields = source.value.field_list()?;
177 record_field_list(fields)?
178 }
179 VariantDef::Union(u) => {
180 module = u.module(sema.db);
181 let source = u.source(sema.db);
182 def_file_id = source.file_id;
183 source.value.record_field_list()?
184 }
185 VariantDef::EnumVariant(e) => {
186 module = e.module(sema.db);
187 let source = e.source(sema.db);
188 def_file_id = source.file_id;
189 let fields = source.value.field_list()?;
190 record_field_list(fields)?
191 }
192 };
193 let def_file_id = def_file_id.original_file(sema.db);
194
195 let new_field_type = sema.type_of_expr(&record_expr.expr()?)?;
196 if new_field_type.is_unknown() {
197 return None;
198 }
199 let new_field = make::record_field(
200 record_expr.field_name()?,
201 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
202 );
203
204 let last_field = record_fields.fields().last()?;
205 let last_field_syntax = last_field.syntax();
206 let indent = IndentLevel::from_node(last_field_syntax);
207
208 let mut new_field = new_field.to_string();
209 if usage_file_id != def_file_id {
210 new_field = format!("pub(crate) {}", new_field);
211 }
212 new_field = format!("\n{}{}", indent, new_field);
213
214 let needs_comma = !last_field_syntax.to_string().ends_with(',');
215 if needs_comma {
216 new_field = format!(",{}", new_field);
217 }
218
219 let source_change = SourceFileEdit {
220 file_id: def_file_id,
221 edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
222 };
223 let fix = Fix::new("Create field", source_change.into());
224 return Some(fix);
225
226 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
227 match field_def_list {
228 ast::FieldList::RecordFieldList(it) => Some(it),
229 ast::FieldList::TupleFieldList(_) => None,
230 }
231 } 105 }
232} 106}
233 107
@@ -238,25 +112,26 @@ fn check_unnecessary_braces_in_use_statement(
238) -> Option<()> { 112) -> Option<()> {
239 let use_tree_list = ast::UseTreeList::cast(node.clone())?; 113 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
240 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { 114 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
241 let range = use_tree_list.syntax().text_range(); 115 let use_range = use_tree_list.syntax().text_range();
242 let edit = 116 let edit =
243 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) 117 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
244 .unwrap_or_else(|| { 118 .unwrap_or_else(|| {
245 let to_replace = single_use_tree.syntax().text().to_string(); 119 let to_replace = single_use_tree.syntax().text().to_string();
246 let mut edit_builder = TextEditBuilder::default(); 120 let mut edit_builder = TextEditBuilder::default();
247 edit_builder.delete(range); 121 edit_builder.delete(use_range);
248 edit_builder.insert(range.start(), to_replace); 122 edit_builder.insert(use_range.start(), to_replace);
249 edit_builder.finish() 123 edit_builder.finish()
250 }); 124 });
251 125
252 acc.push(Diagnostic { 126 acc.push(Diagnostic {
253 name: None, 127 name: None,
254 range, 128 range: use_range,
255 message: "Unnecessary braces in use statement".to_string(), 129 message: "Unnecessary braces in use statement".to_string(),
256 severity: Severity::WeakWarning, 130 severity: Severity::WeakWarning,
257 fix: Some(Fix::new( 131 fix: Some(Fix::new(
258 "Remove unnecessary braces", 132 "Remove unnecessary braces",
259 SourceFileEdit { file_id, edit }.into(), 133 SourceFileEdit { file_id, edit }.into(),
134 use_range,
260 )), 135 )),
261 }); 136 });
262 } 137 }
@@ -271,8 +146,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
271 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { 146 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
272 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); 147 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
273 let end = use_tree_list_node.text_range().end(); 148 let end = use_tree_list_node.text_range().end();
274 let range = TextRange::new(start, end); 149 return Some(TextEdit::delete(TextRange::new(start, end)));
275 return Some(TextEdit::delete(range));
276 } 150 }
277 None 151 None
278} 152}
@@ -295,14 +169,16 @@ fn check_struct_shorthand_initialization(
295 edit_builder.insert(record_field.syntax().text_range().start(), field_name); 169 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
296 let edit = edit_builder.finish(); 170 let edit = edit_builder.finish();
297 171
172 let field_range = record_field.syntax().text_range();
298 acc.push(Diagnostic { 173 acc.push(Diagnostic {
299 name: None, 174 name: None,
300 range: record_field.syntax().text_range(), 175 range: field_range,
301 message: "Shorthand struct initialization".to_string(), 176 message: "Shorthand struct initialization".to_string(),
302 severity: Severity::WeakWarning, 177 severity: Severity::WeakWarning,
303 fix: Some(Fix::new( 178 fix: Some(Fix::new(
304 "Use struct shorthand initialization", 179 "Use struct shorthand initialization",
305 SourceFileEdit { file_id, edit }.into(), 180 SourceFileEdit { file_id, edit }.into(),
181 field_range,
306 )), 182 )),
307 }); 183 });
308 } 184 }
@@ -326,7 +202,7 @@ mod tests {
326 /// Takes a multi-file input fixture with annotated cursor positions, 202 /// Takes a multi-file input fixture with annotated cursor positions,
327 /// and checks that: 203 /// and checks that:
328 /// * a diagnostic is produced 204 /// * a diagnostic is produced
329 /// * this diagnostic touches the input cursor position 205 /// * this diagnostic fix trigger range touches the input cursor position
330 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied 206 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
331 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { 207 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
332 let after = trim_indent(ra_fixture_after); 208 let after = trim_indent(ra_fixture_after);
@@ -344,10 +220,10 @@ mod tests {
344 220
345 assert_eq_text!(&after, &actual); 221 assert_eq_text!(&after, &actual);
346 assert!( 222 assert!(
347 diagnostic.range.start() <= file_position.offset 223 fix.fix_trigger_range.start() <= file_position.offset
348 && diagnostic.range.end() >= file_position.offset, 224 && fix.fix_trigger_range.end() >= file_position.offset,
349 "diagnostic range {:?} does not touch cursor position {:?}", 225 "diagnostic fix range {:?} does not touch cursor position {:?}",
350 diagnostic.range, 226 fix.fix_trigger_range,
351 file_position.offset 227 file_position.offset
352 ); 228 );
353 } 229 }
@@ -712,6 +588,7 @@ fn test_fn() {
712 ], 588 ],
713 is_snippet: false, 589 is_snippet: false,
714 }, 590 },
591 fix_trigger_range: 0..8,
715 }, 592 },
716 ), 593 ),
717 }, 594 },