]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/diagnostics/fixes.rs
internal: use mutable syntax trees when filling fields
[rust.git] / crates / ide / src / diagnostics / fixes.rs
1 //! Provides a way to attach fixes to the diagnostics.
2 //! The same module also has all curret custom fixes for the diagnostics implemented.
3 use hir::{
4     db::AstDatabase,
5     diagnostics::{
6         Diagnostic, IncorrectCase, MissingFields, MissingOkOrSomeInTailExpr, NoSuchField,
7         RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnresolvedModule,
8     },
9     HasSource, HirDisplay, InFile, Semantics, VariantDef,
10 };
11 use ide_assists::AssistResolveStrategy;
12 use ide_db::{
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
24 use crate::{
25     diagnostics::{fix, unresolved_fix},
26     references::rename::rename_with_semantics,
27     Assist, FilePosition,
28 };
29
30 /// A [Diagnostic] that potentially has a fix available.
31 ///
32 /// [Diagnostic]: hir::diagnostics::Diagnostic
33 pub(crate) trait DiagnosticWithFix: Diagnostic {
34     /// `resolve` determines if the diagnostic should fill in the `edit` field
35     /// of the assist.
36     ///
37     /// If `resolve` is false, the edit will be computed later, on demand, and
38     /// can be omitted.
39     fn fix(
40         &self,
41         sema: &Semantics<RootDatabase>,
42         _resolve: &AssistResolveStrategy,
43     ) -> Option<Assist>;
44 }
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 new_field_list = old_field_list.clone_for_update();
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                     .clone_for_update();
108             new_field_list.add_field(field);
109         }
110
111         let edit = {
112             let mut builder = TextEdit::builder();
113             algo::diff(&old_field_list.syntax(), &new_field_list.syntax())
114                 .into_text_edit(&mut builder);
115             builder.finish()
116         };
117         Some(fix(
118             "fill_missing_fields",
119             "Fill struct fields",
120             SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
121             sema.original_range(&field_list_parent.syntax()).range,
122         ))
123     }
124 }
125
126 impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
127     fn fix(
128         &self,
129         sema: &Semantics<RootDatabase>,
130         _resolve: &AssistResolveStrategy,
131     ) -> Option<Assist> {
132         let root = sema.db.parse_or_expand(self.file)?;
133         let tail_expr = self.expr.to_node(&root);
134         let tail_expr_range = tail_expr.syntax().text_range();
135         let replacement = format!("{}({})", self.required, tail_expr.syntax());
136         let edit = TextEdit::replace(tail_expr_range, replacement);
137         let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
138         let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
139         Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
140     }
141 }
142
143 impl DiagnosticWithFix for RemoveThisSemicolon {
144     fn fix(
145         &self,
146         sema: &Semantics<RootDatabase>,
147         _resolve: &AssistResolveStrategy,
148     ) -> Option<Assist> {
149         let root = sema.db.parse_or_expand(self.file)?;
150
151         let semicolon = self
152             .expr
153             .to_node(&root)
154             .syntax()
155             .parent()
156             .and_then(ast::ExprStmt::cast)
157             .and_then(|expr| expr.semicolon_token())?
158             .text_range();
159
160         let edit = TextEdit::delete(semicolon);
161         let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
162
163         Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
164     }
165 }
166
167 impl DiagnosticWithFix for IncorrectCase {
168     fn fix(
169         &self,
170         sema: &Semantics<RootDatabase>,
171         resolve: &AssistResolveStrategy,
172     ) -> Option<Assist> {
173         let root = sema.db.parse_or_expand(self.file)?;
174         let name_node = self.ident.to_node(&root);
175
176         let name_node = InFile::new(self.file, name_node.syntax());
177         let frange = name_node.original_file_range(sema.db);
178         let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
179
180         let label = format!("Rename to {}", self.suggested_text);
181         let mut res = unresolved_fix("change_case", &label, frange.range);
182         if resolve.should_resolve(&res.id) {
183             let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
184             res.source_change = Some(source_change.ok().unwrap_or_default());
185         }
186
187         Some(res)
188     }
189 }
190
191 impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
192     fn fix(
193         &self,
194         sema: &Semantics<RootDatabase>,
195         _resolve: &AssistResolveStrategy,
196     ) -> Option<Assist> {
197         let root = sema.db.parse_or_expand(self.file)?;
198         let next_expr = self.next_expr.to_node(&root);
199         let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
200
201         let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
202         let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
203         let filter_map_args = filter_map_call.arg_list()?;
204
205         let range_to_replace =
206             TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
207         let replacement = format!("find_map{}", filter_map_args.syntax().text());
208         let trigger_range = next_expr.syntax().text_range();
209
210         let edit = TextEdit::replace(range_to_replace, replacement);
211
212         let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
213
214         Some(fix(
215             "replace_with_find_map",
216             "Replace filter_map(..).next() with find_map()",
217             source_change,
218             trigger_range,
219         ))
220     }
221 }
222
223 fn missing_record_expr_field_fix(
224     sema: &Semantics<RootDatabase>,
225     usage_file_id: FileId,
226     record_expr_field: &ast::RecordExprField,
227 ) -> Option<Assist> {
228     let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
229     let def_id = sema.resolve_variant(record_lit)?;
230     let module;
231     let def_file_id;
232     let record_fields = match def_id {
233         VariantDef::Struct(s) => {
234             module = s.module(sema.db);
235             let source = s.source(sema.db)?;
236             def_file_id = source.file_id;
237             let fields = source.value.field_list()?;
238             record_field_list(fields)?
239         }
240         VariantDef::Union(u) => {
241             module = u.module(sema.db);
242             let source = u.source(sema.db)?;
243             def_file_id = source.file_id;
244             source.value.record_field_list()?
245         }
246         VariantDef::Variant(e) => {
247             module = e.module(sema.db);
248             let source = e.source(sema.db)?;
249             def_file_id = source.file_id;
250             let fields = source.value.field_list()?;
251             record_field_list(fields)?
252         }
253     };
254     let def_file_id = def_file_id.original_file(sema.db);
255
256     let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
257     if new_field_type.is_unknown() {
258         return None;
259     }
260     let new_field = make::record_field(
261         None,
262         make::name(&record_expr_field.field_name()?.text()),
263         make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
264     );
265
266     let last_field = record_fields.fields().last()?;
267     let last_field_syntax = last_field.syntax();
268     let indent = IndentLevel::from_node(last_field_syntax);
269
270     let mut new_field = new_field.to_string();
271     if usage_file_id != def_file_id {
272         new_field = format!("pub(crate) {}", new_field);
273     }
274     new_field = format!("\n{}{}", indent, new_field);
275
276     let needs_comma = !last_field_syntax.to_string().ends_with(',');
277     if needs_comma {
278         new_field = format!(",{}", new_field);
279     }
280
281     let source_change = SourceChange::from_text_edit(
282         def_file_id,
283         TextEdit::insert(last_field_syntax.text_range().end(), new_field),
284     );
285     return Some(fix(
286         "create_field",
287         "Create field",
288         source_change,
289         record_expr_field.syntax().text_range(),
290     ));
291
292     fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
293         match field_def_list {
294             ast::FieldList::RecordFieldList(it) => Some(it),
295             ast::FieldList::TupleFieldList(_) => None,
296         }
297     }
298 }