diff options
Diffstat (limited to 'crates/ra_ide/src')
-rw-r--r-- | crates/ra_ide/src/diagnostics.rs | 188 | ||||
-rw-r--r-- | crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs | 163 |
2 files changed, 160 insertions, 191 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs index ca1a7c1aa..165ff5249 100644 --- a/crates/ra_ide/src/diagnostics.rs +++ b/crates/ra_ide/src/diagnostics.rs | |||
@@ -7,22 +7,20 @@ | |||
7 | use std::cell::RefCell; | 7 | use std::cell::RefCell; |
8 | 8 | ||
9 | use hir::{ | 9 | use hir::{ |
10 | db::AstDatabase, | 10 | diagnostics::{Diagnostic as HirDiagnostics, DiagnosticSinkBuilder}, |
11 | diagnostics::{Diagnostic as _, DiagnosticSinkBuilder}, | 11 | Semantics, |
12 | HasSource, HirDisplay, Semantics, VariantDef, | ||
13 | }; | 12 | }; |
14 | use itertools::Itertools; | 13 | use itertools::Itertools; |
15 | use ra_db::{SourceDatabase, Upcast}; | 14 | use ra_db::SourceDatabase; |
16 | use ra_ide_db::RootDatabase; | 15 | use ra_ide_db::RootDatabase; |
17 | use ra_prof::profile; | 16 | use ra_prof::profile; |
18 | use ra_syntax::{ | 17 | use ra_syntax::{ |
19 | algo, | 18 | ast::{self, AstNode}, |
20 | ast::{self, edit::IndentLevel, make, AstNode}, | ||
21 | SyntaxNode, TextRange, T, | 19 | SyntaxNode, TextRange, T, |
22 | }; | 20 | }; |
23 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 21 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
24 | 22 | ||
25 | use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; | 23 | use crate::{Diagnostic, FileId, Fix, SourceFileEdit}; |
26 | 24 | ||
27 | mod diagnostics_with_fix; | 25 | mod diagnostics_with_fix; |
28 | use diagnostics_with_fix::DiagnosticWithFix; | 26 | use diagnostics_with_fix::DiagnosticWithFix; |
@@ -58,96 +56,16 @@ pub(crate) fn diagnostics( | |||
58 | let res = RefCell::new(res); | 56 | let res = RefCell::new(res); |
59 | let mut sink = DiagnosticSinkBuilder::new() | 57 | let mut sink = DiagnosticSinkBuilder::new() |
60 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { | 58 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { |
61 | let fix = Fix::new( | 59 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
62 | "Create module", | ||
63 | FileSystemEdit::CreateFile { | ||
64 | anchor: d.file.original_file(db), | ||
65 | dst: d.candidate.clone(), | ||
66 | } | ||
67 | .into(), | ||
68 | ); | ||
69 | let fix = diagnostic_fix_source(&sema, d) | ||
70 | .map(|unresolved_module| unresolved_module.syntax().text_range()) | ||
71 | .map(|fix_range| (fix, fix_range)); | ||
72 | |||
73 | res.borrow_mut().push(Diagnostic { | ||
74 | range: sema.diagnostics_presentation_range(d).range, | ||
75 | message: d.message(), | ||
76 | severity: Severity::Error, | ||
77 | fix, | ||
78 | }); | ||
79 | Some(()) | ||
80 | }) | 60 | }) |
81 | .on::<hir::diagnostics::MissingFields, _>(|d| { | 61 | .on::<hir::diagnostics::MissingFields, _>(|d| { |
82 | // Note that although we could add a diagnostics to | 62 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
83 | // fill the missing tuple field, e.g : | ||
84 | // `struct A(usize);` | ||
85 | // `let a = A { 0: () }` | ||
86 | // but it is uncommon usage and it should not be encouraged. | ||
87 | let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
88 | None | ||
89 | } else { | ||
90 | diagnostic_fix_source(&sema, d) | ||
91 | .and_then(|record_expr| record_expr.record_expr_field_list()) | ||
92 | .map(|old_field_list| { | ||
93 | let mut new_field_list = old_field_list.clone(); | ||
94 | for f in d.missed_fields.iter() { | ||
95 | let field = make::record_expr_field( | ||
96 | make::name_ref(&f.to_string()), | ||
97 | Some(make::expr_unit()), | ||
98 | ); | ||
99 | new_field_list = new_field_list.append_field(&field); | ||
100 | } | ||
101 | |||
102 | let edit = { | ||
103 | let mut builder = TextEditBuilder::default(); | ||
104 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
105 | .into_text_edit(&mut builder); | ||
106 | builder.finish() | ||
107 | }; | ||
108 | ( | ||
109 | Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into()), | ||
110 | sema.original_range(&old_field_list.syntax()).range, | ||
111 | // old_field_list.syntax().text_range(), | ||
112 | ) | ||
113 | }) | ||
114 | }; | ||
115 | |||
116 | res.borrow_mut().push(Diagnostic { | ||
117 | range: sema.diagnostics_presentation_range(d).range, | ||
118 | message: d.message(), | ||
119 | severity: Severity::Error, | ||
120 | fix, | ||
121 | }); | ||
122 | Some(()) | ||
123 | }) | 63 | }) |
124 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | 64 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { |
125 | let fix = diagnostic_fix_source(&sema, d).map(|tail_expr| { | 65 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
126 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
127 | let edit = | ||
128 | TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax())); | ||
129 | let source_change = SourceFileEdit { file_id, edit }.into(); | ||
130 | (Fix::new("Wrap with ok", source_change), tail_expr_range) | ||
131 | }); | ||
132 | |||
133 | res.borrow_mut().push(Diagnostic { | ||
134 | range: sema.diagnostics_presentation_range(d).range, | ||
135 | message: d.message(), | ||
136 | severity: Severity::Error, | ||
137 | fix, | ||
138 | }); | ||
139 | Some(()) | ||
140 | }) | 66 | }) |
141 | .on::<hir::diagnostics::NoSuchField, _>(|d| { | 67 | .on::<hir::diagnostics::NoSuchField, _>(|d| { |
142 | res.borrow_mut().push(Diagnostic { | 68 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
143 | range: sema.diagnostics_presentation_range(d).range, | ||
144 | message: d.message(), | ||
145 | severity: Severity::Error, | ||
146 | fix: missing_struct_field_fix(&sema, file_id, d).and_then(|fix| { | ||
147 | Some((fix, diagnostic_fix_source(&sema, d)?.syntax().text_range())) | ||
148 | }), | ||
149 | }); | ||
150 | Some(()) | ||
151 | }) | 69 | }) |
152 | // Only collect experimental diagnostics when they're enabled. | 70 | // Only collect experimental diagnostics when they're enabled. |
153 | .filter(|diag| !diag.is_experimental() || enable_experimental) | 71 | .filter(|diag| !diag.is_experimental() || enable_experimental) |
@@ -168,87 +86,15 @@ pub(crate) fn diagnostics( | |||
168 | res.into_inner() | 86 | res.into_inner() |
169 | } | 87 | } |
170 | 88 | ||
171 | fn diagnostic_fix_source<T: DiagnosticWithFix + hir::diagnostics::Diagnostic>( | 89 | fn diagnostic_with_fix<D: HirDiagnostics + DiagnosticWithFix>( |
90 | d: &D, | ||
172 | sema: &Semantics<RootDatabase>, | 91 | sema: &Semantics<RootDatabase>, |
173 | d: &T, | 92 | ) -> Diagnostic { |
174 | ) -> Option<<T as DiagnosticWithFix>::AST> { | 93 | Diagnostic { |
175 | let file_id = d.presentation().file_id; | 94 | range: sema.diagnostics_presentation_range(d).range, |
176 | let root = sema.db.parse_or_expand(file_id)?; | 95 | message: d.message(), |
177 | sema.cache(root, file_id); | 96 | severity: Severity::Error, |
178 | d.fix_source(sema.db.upcast()) | 97 | fix: d.fix(&sema), |
179 | } | ||
180 | |||
181 | fn missing_struct_field_fix( | ||
182 | sema: &Semantics<RootDatabase>, | ||
183 | usage_file_id: FileId, | ||
184 | d: &hir::diagnostics::NoSuchField, | ||
185 | ) -> Option<Fix> { | ||
186 | let record_expr_field = diagnostic_fix_source(&sema, d)?; | ||
187 | |||
188 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
189 | let def_id = sema.resolve_variant(record_lit)?; | ||
190 | let module; | ||
191 | let def_file_id; | ||
192 | let record_fields = match VariantDef::from(def_id) { | ||
193 | VariantDef::Struct(s) => { | ||
194 | module = s.module(sema.db); | ||
195 | let source = s.source(sema.db); | ||
196 | def_file_id = source.file_id; | ||
197 | let fields = source.value.field_list()?; | ||
198 | record_field_list(fields)? | ||
199 | } | ||
200 | VariantDef::Union(u) => { | ||
201 | module = u.module(sema.db); | ||
202 | let source = u.source(sema.db); | ||
203 | def_file_id = source.file_id; | ||
204 | source.value.record_field_list()? | ||
205 | } | ||
206 | VariantDef::EnumVariant(e) => { | ||
207 | module = e.module(sema.db); | ||
208 | let source = e.source(sema.db); | ||
209 | def_file_id = source.file_id; | ||
210 | let fields = source.value.field_list()?; | ||
211 | record_field_list(fields)? | ||
212 | } | ||
213 | }; | ||
214 | let def_file_id = def_file_id.original_file(sema.db); | ||
215 | |||
216 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
217 | if new_field_type.is_unknown() { | ||
218 | return None; | ||
219 | } | ||
220 | let new_field = make::record_field( | ||
221 | record_expr_field.field_name()?, | ||
222 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
223 | ); | ||
224 | |||
225 | let last_field = record_fields.fields().last()?; | ||
226 | let last_field_syntax = last_field.syntax(); | ||
227 | let indent = IndentLevel::from_node(last_field_syntax); | ||
228 | |||
229 | let mut new_field = new_field.to_string(); | ||
230 | if usage_file_id != def_file_id { | ||
231 | new_field = format!("pub(crate) {}", new_field); | ||
232 | } | ||
233 | new_field = format!("\n{}{}", indent, new_field); | ||
234 | |||
235 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
236 | if needs_comma { | ||
237 | new_field = format!(",{}", new_field); | ||
238 | } | ||
239 | |||
240 | let source_change = SourceFileEdit { | ||
241 | file_id: def_file_id, | ||
242 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
243 | }; | ||
244 | let fix = Fix::new("Create field", source_change.into()); | ||
245 | return Some(fix); | ||
246 | |||
247 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
248 | match field_def_list { | ||
249 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
250 | ast::FieldList::TupleFieldList(_) => None, | ||
251 | } | ||
252 | } | 98 | } |
253 | } | 99 | } |
254 | 100 | ||
diff --git a/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs index 8578a90ec..56d454ac6 100644 --- a/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs +++ b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs | |||
@@ -1,46 +1,169 @@ | |||
1 | use crate::Fix; | ||
2 | use ast::{edit::IndentLevel, make}; | ||
1 | use hir::{ | 3 | use hir::{ |
2 | db::AstDatabase, | 4 | db::AstDatabase, |
3 | diagnostics::{MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | 5 | diagnostics::{MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, |
6 | HasSource, HirDisplay, Semantics, VariantDef, | ||
4 | }; | 7 | }; |
5 | use ra_syntax::ast; | 8 | use ra_db::FileId; |
9 | use ra_ide_db::{ | ||
10 | source_change::{FileSystemEdit, SourceFileEdit}, | ||
11 | RootDatabase, | ||
12 | }; | ||
13 | use ra_syntax::{algo, ast, AstNode, TextRange}; | ||
14 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
6 | 15 | ||
7 | // TODO kb | 16 | // TODO kb |
8 | pub trait DiagnosticWithFix { | 17 | pub trait DiagnosticWithFix { |
9 | type AST; | 18 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<(Fix, TextRange)>; |
10 | fn fix_source(&self, db: &dyn AstDatabase) -> Option<Self::AST>; | ||
11 | } | 19 | } |
12 | 20 | ||
13 | impl DiagnosticWithFix for UnresolvedModule { | 21 | impl DiagnosticWithFix for UnresolvedModule { |
14 | type AST = ast::Module; | 22 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<(Fix, TextRange)> { |
15 | fn fix_source(&self, db: &dyn AstDatabase) -> Option<Self::AST> { | 23 | let fix = Fix::new( |
16 | let root = db.parse_or_expand(self.file)?; | 24 | "Create module", |
17 | Some(self.decl.to_node(&root)) | 25 | FileSystemEdit::CreateFile { |
26 | anchor: self.file.original_file(sema.db), | ||
27 | dst: self.candidate.clone(), | ||
28 | } | ||
29 | .into(), | ||
30 | ); | ||
31 | |||
32 | let root = sema.db.parse_or_expand(self.file)?; | ||
33 | let unresolved_module = self.decl.to_node(&root); | ||
34 | Some((fix, unresolved_module.syntax().text_range())) | ||
18 | } | 35 | } |
19 | } | 36 | } |
20 | 37 | ||
21 | impl DiagnosticWithFix for NoSuchField { | 38 | impl DiagnosticWithFix for NoSuchField { |
22 | type AST = ast::RecordExprField; | 39 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<(Fix, TextRange)> { |
23 | 40 | let root = sema.db.parse_or_expand(self.file)?; | |
24 | fn fix_source(&self, db: &dyn AstDatabase) -> Option<Self::AST> { | 41 | let record_expr_field = self.field.to_node(&root); |
25 | let root = db.parse_or_expand(self.file)?; | 42 | let fix = |
26 | Some(self.field.to_node(&root)) | 43 | missing_struct_field_fix(&sema, self.file.original_file(sema.db), &record_expr_field)?; |
44 | Some((fix, record_expr_field.syntax().text_range())) | ||
27 | } | 45 | } |
28 | } | 46 | } |
29 | 47 | ||
30 | impl DiagnosticWithFix for MissingFields { | 48 | impl DiagnosticWithFix for MissingFields { |
31 | type AST = ast::RecordExpr; | 49 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<(Fix, TextRange)> { |
50 | // Note that although we could add a diagnostics to | ||
51 | // fill the missing tuple field, e.g : | ||
52 | // `struct A(usize);` | ||
53 | // `let a = A { 0: () }` | ||
54 | // but it is uncommon usage and it should not be encouraged. | ||
55 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
56 | None | ||
57 | } else { | ||
58 | let root = sema.db.parse_or_expand(self.file)?; | ||
59 | let old_field_list = self.field_list_parent.to_node(&root).record_expr_field_list()?; | ||
60 | let mut new_field_list = old_field_list.clone(); | ||
61 | for f in self.missed_fields.iter() { | ||
62 | let field = make::record_expr_field( | ||
63 | make::name_ref(&f.to_string()), | ||
64 | Some(make::expr_unit()), | ||
65 | ); | ||
66 | new_field_list = new_field_list.append_field(&field); | ||
67 | } | ||
32 | 68 | ||
33 | fn fix_source(&self, db: &dyn AstDatabase) -> Option<Self::AST> { | 69 | let edit = { |
34 | let root = db.parse_or_expand(self.file)?; | 70 | let mut builder = TextEditBuilder::default(); |
35 | Some(self.field_list_parent.to_node(&root)) | 71 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) |
72 | .into_text_edit(&mut builder); | ||
73 | builder.finish() | ||
74 | }; | ||
75 | Some(( | ||
76 | Fix::new( | ||
77 | "Fill struct fields", | ||
78 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), | ||
79 | ), | ||
80 | sema.original_range(&old_field_list.syntax()).range, | ||
81 | // old_field_list.syntax().text_range(), | ||
82 | )) | ||
83 | } | ||
36 | } | 84 | } |
37 | } | 85 | } |
38 | 86 | ||
39 | impl DiagnosticWithFix for MissingOkInTailExpr { | 87 | impl DiagnosticWithFix for MissingOkInTailExpr { |
40 | type AST = ast::Expr; | 88 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<(Fix, TextRange)> { |
89 | let root = sema.db.parse_or_expand(self.file)?; | ||
90 | let tail_expr = self.expr.to_node(&root); | ||
91 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
92 | let edit = TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax())); | ||
93 | let source_change = | ||
94 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); | ||
95 | Some((Fix::new("Wrap with ok", source_change), tail_expr_range)) | ||
96 | } | ||
97 | } | ||
98 | |||
99 | fn missing_struct_field_fix( | ||
100 | sema: &Semantics<RootDatabase>, | ||
101 | usage_file_id: FileId, | ||
102 | record_expr_field: &ast::RecordExprField, | ||
103 | ) -> Option<Fix> { | ||
104 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
105 | let def_id = sema.resolve_variant(record_lit)?; | ||
106 | let module; | ||
107 | let def_file_id; | ||
108 | let record_fields = match VariantDef::from(def_id) { | ||
109 | VariantDef::Struct(s) => { | ||
110 | module = s.module(sema.db); | ||
111 | let source = s.source(sema.db); | ||
112 | def_file_id = source.file_id; | ||
113 | let fields = source.value.field_list()?; | ||
114 | record_field_list(fields)? | ||
115 | } | ||
116 | VariantDef::Union(u) => { | ||
117 | module = u.module(sema.db); | ||
118 | let source = u.source(sema.db); | ||
119 | def_file_id = source.file_id; | ||
120 | source.value.record_field_list()? | ||
121 | } | ||
122 | VariantDef::EnumVariant(e) => { | ||
123 | module = e.module(sema.db); | ||
124 | let source = e.source(sema.db); | ||
125 | def_file_id = source.file_id; | ||
126 | let fields = source.value.field_list()?; | ||
127 | record_field_list(fields)? | ||
128 | } | ||
129 | }; | ||
130 | let def_file_id = def_file_id.original_file(sema.db); | ||
131 | |||
132 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
133 | if new_field_type.is_unknown() { | ||
134 | return None; | ||
135 | } | ||
136 | let new_field = make::record_field( | ||
137 | record_expr_field.field_name()?, | ||
138 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
139 | ); | ||
140 | |||
141 | let last_field = record_fields.fields().last()?; | ||
142 | let last_field_syntax = last_field.syntax(); | ||
143 | let indent = IndentLevel::from_node(last_field_syntax); | ||
144 | |||
145 | let mut new_field = new_field.to_string(); | ||
146 | if usage_file_id != def_file_id { | ||
147 | new_field = format!("pub(crate) {}", new_field); | ||
148 | } | ||
149 | new_field = format!("\n{}{}", indent, new_field); | ||
150 | |||
151 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
152 | if needs_comma { | ||
153 | new_field = format!(",{}", new_field); | ||
154 | } | ||
155 | |||
156 | let source_change = SourceFileEdit { | ||
157 | file_id: def_file_id, | ||
158 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
159 | }; | ||
160 | let fix = Fix::new("Create field", source_change.into()); | ||
161 | return Some(fix); | ||
41 | 162 | ||
42 | fn fix_source(&self, db: &dyn AstDatabase) -> Option<Self::AST> { | 163 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { |
43 | let root = db.parse_or_expand(self.file)?; | 164 | match field_def_list { |
44 | Some(self.expr.to_node(&root)) | 165 | ast::FieldList::RecordFieldList(it) => Some(it), |
166 | ast::FieldList::TupleFieldList(_) => None, | ||
167 | } | ||
45 | } | 168 | } |
46 | } | 169 | } |