diff options
Diffstat (limited to 'crates/ide/src/diagnostics/fixes.rs')
-rw-r--r-- | crates/ide/src/diagnostics/fixes.rs | 288 |
1 files changed, 11 insertions, 277 deletions
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 15821500f..92b3f5a2d 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs | |||
@@ -1,31 +1,18 @@ | |||
1 | //! Provides a way to attach fixes to the diagnostics. | 1 | //! Provides a way to attach fixes to the diagnostics. |
2 | //! The same module also has all curret custom fixes for the diagnostics implemented. | 2 | //! The same module also has all curret custom fixes for the diagnostics implemented. |
3 | use hir::{ | 3 | mod change_case; |
4 | db::AstDatabase, | 4 | mod create_field; |
5 | diagnostics::{ | 5 | mod fill_missing_fields; |
6 | Diagnostic, IncorrectCase, MissingFields, MissingOkOrSomeInTailExpr, NoSuchField, | 6 | mod remove_semicolon; |
7 | RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnresolvedModule, | 7 | mod replace_with_find_map; |
8 | }, | 8 | mod unresolved_module; |
9 | HasSource, HirDisplay, InFile, Semantics, VariantDef, | 9 | mod wrap_tail_expr; |
10 | }; | 10 | |
11 | use hir::{diagnostics::Diagnostic, Semantics}; | ||
11 | use ide_assists::AssistResolveStrategy; | 12 | use ide_assists::AssistResolveStrategy; |
12 | use ide_db::{ | 13 | use ide_db::RootDatabase; |
13 | base_db::{AnchoredPathBuf, FileId}, | ||
14 | source_change::{FileSystemEdit, SourceChange}, | ||
15 | RootDatabase, | ||
16 | }; | ||
17 | use syntax::{ | ||
18 | algo, | ||
19 | ast::{self, edit::IndentLevel, make, ArgListOwner}, | ||
20 | AstNode, TextRange, | ||
21 | }; | ||
22 | use text_edit::TextEdit; | ||
23 | 14 | ||
24 | use crate::{ | 15 | use crate::Assist; |
25 | diagnostics::{fix, unresolved_fix}, | ||
26 | references::rename::rename_with_semantics, | ||
27 | Assist, FilePosition, | ||
28 | }; | ||
29 | 16 | ||
30 | /// A [Diagnostic] that potentially has a fix available. | 17 | /// A [Diagnostic] that potentially has a fix available. |
31 | /// | 18 | /// |
@@ -42,256 +29,3 @@ pub(crate) trait DiagnosticWithFix: Diagnostic { | |||
42 | _resolve: &AssistResolveStrategy, | 29 | _resolve: &AssistResolveStrategy, |
43 | ) -> Option<Assist>; | 30 | ) -> Option<Assist>; |
44 | } | 31 | } |
45 | |||
46 | impl DiagnosticWithFix for UnresolvedModule { | ||
47 | fn fix( | ||
48 | &self, | ||
49 | sema: &Semantics<RootDatabase>, | ||
50 | _resolve: &AssistResolveStrategy, | ||
51 | ) -> Option<Assist> { | ||
52 | let root = sema.db.parse_or_expand(self.file)?; | ||
53 | let unresolved_module = self.decl.to_node(&root); | ||
54 | Some(fix( | ||
55 | "create_module", | ||
56 | "Create module", | ||
57 | FileSystemEdit::CreateFile { | ||
58 | dst: AnchoredPathBuf { | ||
59 | anchor: self.file.original_file(sema.db), | ||
60 | path: self.candidate.clone(), | ||
61 | }, | ||
62 | initial_contents: "".to_string(), | ||
63 | } | ||
64 | .into(), | ||
65 | unresolved_module.syntax().text_range(), | ||
66 | )) | ||
67 | } | ||
68 | } | ||
69 | |||
70 | impl DiagnosticWithFix for NoSuchField { | ||
71 | fn fix( | ||
72 | &self, | ||
73 | sema: &Semantics<RootDatabase>, | ||
74 | _resolve: &AssistResolveStrategy, | ||
75 | ) -> Option<Assist> { | ||
76 | let root = sema.db.parse_or_expand(self.file)?; | ||
77 | missing_record_expr_field_fix( | ||
78 | &sema, | ||
79 | self.file.original_file(sema.db), | ||
80 | &self.field.to_node(&root), | ||
81 | ) | ||
82 | } | ||
83 | } | ||
84 | |||
85 | impl DiagnosticWithFix for MissingFields { | ||
86 | fn fix( | ||
87 | &self, | ||
88 | sema: &Semantics<RootDatabase>, | ||
89 | _resolve: &AssistResolveStrategy, | ||
90 | ) -> Option<Assist> { | ||
91 | // Note that although we could add a diagnostics to | ||
92 | // fill the missing tuple field, e.g : | ||
93 | // `struct A(usize);` | ||
94 | // `let a = A { 0: () }` | ||
95 | // but it is uncommon usage and it should not be encouraged. | ||
96 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
97 | return None; | ||
98 | } | ||
99 | |||
100 | let root = sema.db.parse_or_expand(self.file)?; | ||
101 | let field_list_parent = self.field_list_parent.to_node(&root); | ||
102 | let old_field_list = field_list_parent.record_expr_field_list()?; | ||
103 | let mut new_field_list = old_field_list.clone(); | ||
104 | for f in self.missed_fields.iter() { | ||
105 | let field = | ||
106 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
107 | new_field_list = new_field_list.append_field(&field); | ||
108 | } | ||
109 | |||
110 | let edit = { | ||
111 | let mut builder = TextEdit::builder(); | ||
112 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
113 | .into_text_edit(&mut builder); | ||
114 | builder.finish() | ||
115 | }; | ||
116 | Some(fix( | ||
117 | "fill_missing_fields", | ||
118 | "Fill struct fields", | ||
119 | SourceChange::from_text_edit(self.file.original_file(sema.db), edit), | ||
120 | sema.original_range(&field_list_parent.syntax()).range, | ||
121 | )) | ||
122 | } | ||
123 | } | ||
124 | |||
125 | impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { | ||
126 | fn fix( | ||
127 | &self, | ||
128 | sema: &Semantics<RootDatabase>, | ||
129 | _resolve: &AssistResolveStrategy, | ||
130 | ) -> Option<Assist> { | ||
131 | let root = sema.db.parse_or_expand(self.file)?; | ||
132 | let tail_expr = self.expr.to_node(&root); | ||
133 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
134 | let replacement = format!("{}({})", self.required, tail_expr.syntax()); | ||
135 | let edit = TextEdit::replace(tail_expr_range, replacement); | ||
136 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
137 | let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; | ||
138 | Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | impl DiagnosticWithFix for RemoveThisSemicolon { | ||
143 | fn fix( | ||
144 | &self, | ||
145 | sema: &Semantics<RootDatabase>, | ||
146 | _resolve: &AssistResolveStrategy, | ||
147 | ) -> Option<Assist> { | ||
148 | let root = sema.db.parse_or_expand(self.file)?; | ||
149 | |||
150 | let semicolon = self | ||
151 | .expr | ||
152 | .to_node(&root) | ||
153 | .syntax() | ||
154 | .parent() | ||
155 | .and_then(ast::ExprStmt::cast) | ||
156 | .and_then(|expr| expr.semicolon_token())? | ||
157 | .text_range(); | ||
158 | |||
159 | let edit = TextEdit::delete(semicolon); | ||
160 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
161 | |||
162 | Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | impl DiagnosticWithFix for IncorrectCase { | ||
167 | fn fix( | ||
168 | &self, | ||
169 | sema: &Semantics<RootDatabase>, | ||
170 | resolve: &AssistResolveStrategy, | ||
171 | ) -> Option<Assist> { | ||
172 | let root = sema.db.parse_or_expand(self.file)?; | ||
173 | let name_node = self.ident.to_node(&root); | ||
174 | |||
175 | let name_node = InFile::new(self.file, name_node.syntax()); | ||
176 | let frange = name_node.original_file_range(sema.db); | ||
177 | let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; | ||
178 | |||
179 | let label = format!("Rename to {}", self.suggested_text); | ||
180 | let mut res = unresolved_fix("change_case", &label, frange.range); | ||
181 | if resolve.should_resolve(&res.id) { | ||
182 | let source_change = rename_with_semantics(sema, file_position, &self.suggested_text); | ||
183 | res.source_change = Some(source_change.ok().unwrap_or_default()); | ||
184 | } | ||
185 | |||
186 | Some(res) | ||
187 | } | ||
188 | } | ||
189 | |||
190 | impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { | ||
191 | fn fix( | ||
192 | &self, | ||
193 | sema: &Semantics<RootDatabase>, | ||
194 | _resolve: &AssistResolveStrategy, | ||
195 | ) -> Option<Assist> { | ||
196 | let root = sema.db.parse_or_expand(self.file)?; | ||
197 | let next_expr = self.next_expr.to_node(&root); | ||
198 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | ||
199 | |||
200 | let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?; | ||
201 | let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range(); | ||
202 | let filter_map_args = filter_map_call.arg_list()?; | ||
203 | |||
204 | let range_to_replace = | ||
205 | TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end()); | ||
206 | let replacement = format!("find_map{}", filter_map_args.syntax().text()); | ||
207 | let trigger_range = next_expr.syntax().text_range(); | ||
208 | |||
209 | let edit = TextEdit::replace(range_to_replace, replacement); | ||
210 | |||
211 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
212 | |||
213 | Some(fix( | ||
214 | "replace_with_find_map", | ||
215 | "Replace filter_map(..).next() with find_map()", | ||
216 | source_change, | ||
217 | trigger_range, | ||
218 | )) | ||
219 | } | ||
220 | } | ||
221 | |||
222 | fn missing_record_expr_field_fix( | ||
223 | sema: &Semantics<RootDatabase>, | ||
224 | usage_file_id: FileId, | ||
225 | record_expr_field: &ast::RecordExprField, | ||
226 | ) -> Option<Assist> { | ||
227 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
228 | let def_id = sema.resolve_variant(record_lit)?; | ||
229 | let module; | ||
230 | let def_file_id; | ||
231 | let record_fields = match def_id { | ||
232 | VariantDef::Struct(s) => { | ||
233 | module = s.module(sema.db); | ||
234 | let source = s.source(sema.db)?; | ||
235 | def_file_id = source.file_id; | ||
236 | let fields = source.value.field_list()?; | ||
237 | record_field_list(fields)? | ||
238 | } | ||
239 | VariantDef::Union(u) => { | ||
240 | module = u.module(sema.db); | ||
241 | let source = u.source(sema.db)?; | ||
242 | def_file_id = source.file_id; | ||
243 | source.value.record_field_list()? | ||
244 | } | ||
245 | VariantDef::Variant(e) => { | ||
246 | module = e.module(sema.db); | ||
247 | let source = e.source(sema.db)?; | ||
248 | def_file_id = source.file_id; | ||
249 | let fields = source.value.field_list()?; | ||
250 | record_field_list(fields)? | ||
251 | } | ||
252 | }; | ||
253 | let def_file_id = def_file_id.original_file(sema.db); | ||
254 | |||
255 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
256 | if new_field_type.is_unknown() { | ||
257 | return None; | ||
258 | } | ||
259 | let new_field = make::record_field( | ||
260 | None, | ||
261 | make::name(&record_expr_field.field_name()?.text()), | ||
262 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
263 | ); | ||
264 | |||
265 | let last_field = record_fields.fields().last()?; | ||
266 | let last_field_syntax = last_field.syntax(); | ||
267 | let indent = IndentLevel::from_node(last_field_syntax); | ||
268 | |||
269 | let mut new_field = new_field.to_string(); | ||
270 | if usage_file_id != def_file_id { | ||
271 | new_field = format!("pub(crate) {}", new_field); | ||
272 | } | ||
273 | new_field = format!("\n{}{}", indent, new_field); | ||
274 | |||
275 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
276 | if needs_comma { | ||
277 | new_field = format!(",{}", new_field); | ||
278 | } | ||
279 | |||
280 | let source_change = SourceChange::from_text_edit( | ||
281 | def_file_id, | ||
282 | TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
283 | ); | ||
284 | return Some(fix( | ||
285 | "create_field", | ||
286 | "Create field", | ||
287 | source_change, | ||
288 | record_expr_field.syntax().text_range(), | ||
289 | )); | ||
290 | |||
291 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
292 | match field_def_list { | ||
293 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
294 | ast::FieldList::TupleFieldList(_) => None, | ||
295 | } | ||
296 | } | ||
297 | } | ||