diff options
author | Igor Aleksanov <[email protected]> | 2020-08-12 15:26:43 +0100 |
---|---|---|
committer | Igor Aleksanov <[email protected]> | 2020-08-12 15:26:43 +0100 |
commit | b50bb800a5b5e01b6cb4de10330fd5b61d6cd0db (patch) | |
tree | adb19b05996e8a2829f5a6eb0ed7017404aaf7da /crates/ra_ide/src/diagnostics.rs | |
parent | 13f736d4a13bdf5af2cdd6a4832a41470431a70b (diff) | |
parent | 6be5ab02008b442c85c201968b97f24f13c4692e (diff) |
Merge branch 'master' into add-disable-diagnostics
Diffstat (limited to 'crates/ra_ide/src/diagnostics.rs')
-rw-r--r-- | crates/ra_ide/src/diagnostics.rs | 189 |
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 | ||
7 | use std::cell::RefCell; | 7 | use std::cell::RefCell; |
8 | 8 | ||
9 | use hir::{ | 9 | use hir::{diagnostics::DiagnosticSinkBuilder, Semantics}; |
10 | diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder}, | ||
11 | HasSource, HirDisplay, Semantics, VariantDef, | ||
12 | }; | ||
13 | use itertools::Itertools; | 10 | use itertools::Itertools; |
14 | use ra_db::SourceDatabase; | 11 | use ra_db::SourceDatabase; |
15 | use ra_ide_db::RootDatabase; | 12 | use ra_ide_db::RootDatabase; |
16 | use ra_prof::profile; | 13 | use ra_prof::profile; |
17 | use ra_syntax::{ | 14 | use 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 | }; |
22 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 18 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
23 | 19 | ||
24 | use crate::{AnalysisConfig, Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; | 20 | use crate::{AnalysisConfig, Diagnostic, FileId, Fix, SourceFileEdit}; |
21 | |||
22 | mod diagnostics_with_fix; | ||
23 | use diagnostics_with_fix::DiagnosticWithFix; | ||
25 | 24 | ||
26 | #[derive(Debug, Copy, Clone)] | 25 | #[derive(Debug, Copy, Clone)] |
27 | pub enum Severity { | 26 | pub 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 | ||
160 | fn missing_struct_field_fix( | 98 | fn 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 | }, |