aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/diagnostics/fixes.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2021-05-17 10:04:17 +0100
committerAleksey Kladov <[email protected]>2021-05-17 10:04:17 +0100
commitfa7fc0e5cb5343e2c59220fb91370f005c13be3a (patch)
tree061cc578b86de2cd72f8f8a57e2979e5c988f7fc /crates/ide/src/diagnostics/fixes.rs
parentdb8fbb99ce6e60c072250bada004de9645431a43 (diff)
internal: scalable module structure for fixits
Diffstat (limited to 'crates/ide/src/diagnostics/fixes.rs')
-rw-r--r--crates/ide/src/diagnostics/fixes.rs245
1 files changed, 9 insertions, 236 deletions
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 5330449f9..92b3f5a2d 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -1,32 +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.
3mod change_case;
4mod create_field;
3mod fill_missing_fields; 5mod fill_missing_fields;
6mod remove_semicolon;
7mod replace_with_find_map;
8mod unresolved_module;
9mod wrap_tail_expr;
4 10
5use hir::{ 11use hir::{diagnostics::Diagnostic, Semantics};
6 db::AstDatabase,
7 diagnostics::{
8 Diagnostic, IncorrectCase, MissingOkOrSomeInTailExpr, NoSuchField, RemoveThisSemicolon,
9 ReplaceFilterMapNextWithFindMap, UnresolvedModule,
10 },
11 HasSource, HirDisplay, InFile, Semantics, VariantDef,
12};
13use ide_assists::AssistResolveStrategy; 12use ide_assists::AssistResolveStrategy;
14use ide_db::{ 13use ide_db::RootDatabase;
15 base_db::{AnchoredPathBuf, FileId},
16 source_change::{FileSystemEdit, SourceChange},
17 RootDatabase,
18};
19use syntax::{
20 ast::{self, edit::IndentLevel, make, ArgListOwner},
21 AstNode, TextRange,
22};
23use text_edit::TextEdit;
24 14
25use crate::{ 15use crate::Assist;
26 diagnostics::{fix, unresolved_fix},
27 references::rename::rename_with_semantics,
28 Assist, FilePosition,
29};
30 16
31/// A [Diagnostic] that potentially has a fix available. 17/// A [Diagnostic] that potentially has a fix available.
32/// 18///
@@ -43,216 +29,3 @@ pub(crate) trait DiagnosticWithFix: Diagnostic {
43 _resolve: &AssistResolveStrategy, 29 _resolve: &AssistResolveStrategy,
44 ) -> Option<Assist>; 30 ) -> Option<Assist>;
45} 31}
46
47impl DiagnosticWithFix for UnresolvedModule {
48 fn fix(
49 &self,
50 sema: &Semantics<RootDatabase>,
51 _resolve: &AssistResolveStrategy,
52 ) -> Option<Assist> {
53 let root = sema.db.parse_or_expand(self.file)?;
54 let unresolved_module = self.decl.to_node(&root);
55 Some(fix(
56 "create_module",
57 "Create module",
58 FileSystemEdit::CreateFile {
59 dst: AnchoredPathBuf {
60 anchor: self.file.original_file(sema.db),
61 path: self.candidate.clone(),
62 },
63 initial_contents: "".to_string(),
64 }
65 .into(),
66 unresolved_module.syntax().text_range(),
67 ))
68 }
69}
70
71impl DiagnosticWithFix for NoSuchField {
72 fn fix(
73 &self,
74 sema: &Semantics<RootDatabase>,
75 _resolve: &AssistResolveStrategy,
76 ) -> Option<Assist> {
77 let root = sema.db.parse_or_expand(self.file)?;
78 missing_record_expr_field_fix(
79 &sema,
80 self.file.original_file(sema.db),
81 &self.field.to_node(&root),
82 )
83 }
84}
85
86impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
87 fn fix(
88 &self,
89 sema: &Semantics<RootDatabase>,
90 _resolve: &AssistResolveStrategy,
91 ) -> Option<Assist> {
92 let root = sema.db.parse_or_expand(self.file)?;
93 let tail_expr = self.expr.to_node(&root);
94 let tail_expr_range = tail_expr.syntax().text_range();
95 let replacement = format!("{}({})", self.required, tail_expr.syntax());
96 let edit = TextEdit::replace(tail_expr_range, replacement);
97 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
98 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
99 Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
100 }
101}
102
103impl DiagnosticWithFix for RemoveThisSemicolon {
104 fn fix(
105 &self,
106 sema: &Semantics<RootDatabase>,
107 _resolve: &AssistResolveStrategy,
108 ) -> Option<Assist> {
109 let root = sema.db.parse_or_expand(self.file)?;
110
111 let semicolon = self
112 .expr
113 .to_node(&root)
114 .syntax()
115 .parent()
116 .and_then(ast::ExprStmt::cast)
117 .and_then(|expr| expr.semicolon_token())?
118 .text_range();
119
120 let edit = TextEdit::delete(semicolon);
121 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
122
123 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
124 }
125}
126
127impl DiagnosticWithFix for IncorrectCase {
128 fn fix(
129 &self,
130 sema: &Semantics<RootDatabase>,
131 resolve: &AssistResolveStrategy,
132 ) -> Option<Assist> {
133 let root = sema.db.parse_or_expand(self.file)?;
134 let name_node = self.ident.to_node(&root);
135
136 let name_node = InFile::new(self.file, name_node.syntax());
137 let frange = name_node.original_file_range(sema.db);
138 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
139
140 let label = format!("Rename to {}", self.suggested_text);
141 let mut res = unresolved_fix("change_case", &label, frange.range);
142 if resolve.should_resolve(&res.id) {
143 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
144 res.source_change = Some(source_change.ok().unwrap_or_default());
145 }
146
147 Some(res)
148 }
149}
150
151impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
152 fn fix(
153 &self,
154 sema: &Semantics<RootDatabase>,
155 _resolve: &AssistResolveStrategy,
156 ) -> Option<Assist> {
157 let root = sema.db.parse_or_expand(self.file)?;
158 let next_expr = self.next_expr.to_node(&root);
159 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
160
161 let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
162 let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
163 let filter_map_args = filter_map_call.arg_list()?;
164
165 let range_to_replace =
166 TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
167 let replacement = format!("find_map{}", filter_map_args.syntax().text());
168 let trigger_range = next_expr.syntax().text_range();
169
170 let edit = TextEdit::replace(range_to_replace, replacement);
171
172 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
173
174 Some(fix(
175 "replace_with_find_map",
176 "Replace filter_map(..).next() with find_map()",
177 source_change,
178 trigger_range,
179 ))
180 }
181}
182
183fn missing_record_expr_field_fix(
184 sema: &Semantics<RootDatabase>,
185 usage_file_id: FileId,
186 record_expr_field: &ast::RecordExprField,
187) -> Option<Assist> {
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 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::Variant(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 None,
222 make::name(&record_expr_field.field_name()?.text()),
223 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
224 );
225
226 let last_field = record_fields.fields().last()?;
227 let last_field_syntax = last_field.syntax();
228 let indent = IndentLevel::from_node(last_field_syntax);
229
230 let mut new_field = new_field.to_string();
231 if usage_file_id != def_file_id {
232 new_field = format!("pub(crate) {}", new_field);
233 }
234 new_field = format!("\n{}{}", indent, new_field);
235
236 let needs_comma = !last_field_syntax.to_string().ends_with(',');
237 if needs_comma {
238 new_field = format!(",{}", new_field);
239 }
240
241 let source_change = SourceChange::from_text_edit(
242 def_file_id,
243 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
244 );
245 return Some(fix(
246 "create_field",
247 "Create field",
248 source_change,
249 record_expr_field.syntax().text_range(),
250 ));
251
252 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
253 match field_def_list {
254 ast::FieldList::RecordFieldList(it) => Some(it),
255 ast::FieldList::TupleFieldList(_) => None,
256 }
257 }
258}