1 //! Provides a way to attach fixes to the diagnostics.
2 //! The same module also has all curret custom fixes for the diagnostics implemented.
6 Diagnostic, IncorrectCase, MissingFields, MissingOkOrSomeInTailExpr, NoSuchField,
7 RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnresolvedModule,
9 HasSource, HirDisplay, InFile, Semantics, VariantDef,
11 use ide_assists::AssistResolveStrategy;
13 base_db::{AnchoredPathBuf, FileId},
14 source_change::{FileSystemEdit, SourceChange},
19 ast::{self, edit::IndentLevel, make, ArgListOwner},
22 use text_edit::TextEdit;
25 diagnostics::{fix, unresolved_fix},
26 references::rename::rename_with_semantics,
30 /// A [Diagnostic] that potentially has a fix available.
32 /// [Diagnostic]: hir::diagnostics::Diagnostic
33 pub(crate) trait DiagnosticWithFix: Diagnostic {
34 /// `resolve` determines if the diagnostic should fill in the `edit` field
37 /// If `resolve` is false, the edit will be computed later, on demand, and
41 sema: &Semantics<RootDatabase>,
42 _resolve: &AssistResolveStrategy,
46 impl DiagnosticWithFix for UnresolvedModule {
49 sema: &Semantics<RootDatabase>,
50 _resolve: &AssistResolveStrategy,
52 let root = sema.db.parse_or_expand(self.file)?;
53 let unresolved_module = self.decl.to_node(&root);
57 FileSystemEdit::CreateFile {
58 dst: AnchoredPathBuf {
59 anchor: self.file.original_file(sema.db),
60 path: self.candidate.clone(),
62 initial_contents: "".to_string(),
65 unresolved_module.syntax().text_range(),
70 impl DiagnosticWithFix for NoSuchField {
73 sema: &Semantics<RootDatabase>,
74 _resolve: &AssistResolveStrategy,
76 let root = sema.db.parse_or_expand(self.file)?;
77 missing_record_expr_field_fix(
79 self.file.original_file(sema.db),
80 &self.field.to_node(&root),
85 impl DiagnosticWithFix for MissingFields {
88 sema: &Semantics<RootDatabase>,
89 _resolve: &AssistResolveStrategy,
91 // Note that although we could add a diagnostics to
92 // fill the missing tuple field, e.g :
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()) {
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() {
106 make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit()))
108 new_field_list.add_field(field);
112 let mut builder = TextEdit::builder();
113 algo::diff(&old_field_list.syntax(), &new_field_list.syntax())
114 .into_text_edit(&mut builder);
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,
126 impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
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))
143 impl DiagnosticWithFix for RemoveThisSemicolon {
146 sema: &Semantics<RootDatabase>,
147 _resolve: &AssistResolveStrategy,
148 ) -> Option<Assist> {
149 let root = sema.db.parse_or_expand(self.file)?;
156 .and_then(ast::ExprStmt::cast)
157 .and_then(|expr| expr.semicolon_token())?
160 let edit = TextEdit::delete(semicolon);
161 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
163 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
167 impl DiagnosticWithFix for IncorrectCase {
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);
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() };
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());
191 impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
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())?;
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()?;
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();
210 let edit = TextEdit::replace(range_to_replace, replacement);
212 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
215 "replace_with_find_map",
216 "Replace filter_map(..).next() with find_map()",
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)?;
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)?
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()?
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)?
254 let def_file_id = def_file_id.original_file(sema.db);
256 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
257 if new_field_type.is_unknown() {
260 let new_field = make::record_field(
262 make::name(&record_expr_field.field_name()?.text()),
263 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
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);
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);
274 new_field = format!("\n{}{}", indent, new_field);
276 let needs_comma = !last_field_syntax.to_string().ends_with(',');
278 new_field = format!(",{}", new_field);
281 let source_change = SourceChange::from_text_edit(
283 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
289 record_expr_field.syntax().text_range(),
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,