diff options
Diffstat (limited to 'crates/ide/src/diagnostics')
| -rw-r--r-- | crates/ide/src/diagnostics/diagnostics_with_fix.rs | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/crates/ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ide/src/diagnostics/diagnostics_with_fix.rs new file mode 100644 index 000000000..85b46c995 --- /dev/null +++ b/crates/ide/src/diagnostics/diagnostics_with_fix.rs | |||
| @@ -0,0 +1,171 @@ | |||
| 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 crate::Fix; | ||
| 4 | use ast::{edit::IndentLevel, make}; | ||
| 5 | use base_db::FileId; | ||
| 6 | use hir::{ | ||
| 7 | db::AstDatabase, | ||
| 8 | diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | ||
| 9 | HasSource, HirDisplay, Semantics, VariantDef, | ||
| 10 | }; | ||
| 11 | use ide_db::{ | ||
| 12 | source_change::{FileSystemEdit, SourceFileEdit}, | ||
| 13 | RootDatabase, | ||
| 14 | }; | ||
| 15 | use syntax::{algo, ast, AstNode}; | ||
| 16 | use text_edit::TextEdit; | ||
| 17 | |||
| 18 | /// A [Diagnostic] that potentially has a fix available. | ||
| 19 | /// | ||
| 20 | /// [Diagnostic]: hir::diagnostics::Diagnostic | ||
| 21 | pub trait DiagnosticWithFix: Diagnostic { | ||
| 22 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>; | ||
| 23 | } | ||
| 24 | |||
| 25 | impl DiagnosticWithFix for UnresolvedModule { | ||
| 26 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
| 27 | let root = sema.db.parse_or_expand(self.file)?; | ||
| 28 | let unresolved_module = self.decl.to_node(&root); | ||
| 29 | Some(Fix::new( | ||
| 30 | "Create module", | ||
| 31 | FileSystemEdit::CreateFile { | ||
| 32 | anchor: self.file.original_file(sema.db), | ||
| 33 | dst: self.candidate.clone(), | ||
| 34 | } | ||
| 35 | .into(), | ||
| 36 | unresolved_module.syntax().text_range(), | ||
| 37 | )) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | impl DiagnosticWithFix for NoSuchField { | ||
| 42 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
| 43 | let root = sema.db.parse_or_expand(self.file)?; | ||
| 44 | missing_record_expr_field_fix( | ||
| 45 | &sema, | ||
| 46 | self.file.original_file(sema.db), | ||
| 47 | &self.field.to_node(&root), | ||
| 48 | ) | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | impl DiagnosticWithFix for MissingFields { | ||
| 53 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
| 54 | // Note that although we could add a diagnostics to | ||
| 55 | // fill the missing tuple field, e.g : | ||
| 56 | // `struct A(usize);` | ||
| 57 | // `let a = A { 0: () }` | ||
| 58 | // but it is uncommon usage and it should not be encouraged. | ||
| 59 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
| 60 | return None; | ||
| 61 | } | ||
| 62 | |||
| 63 | let root = sema.db.parse_or_expand(self.file)?; | ||
| 64 | let old_field_list = self.field_list_parent.to_node(&root).record_expr_field_list()?; | ||
| 65 | let mut new_field_list = old_field_list.clone(); | ||
| 66 | for f in self.missed_fields.iter() { | ||
| 67 | let field = | ||
| 68 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
| 69 | new_field_list = new_field_list.append_field(&field); | ||
| 70 | } | ||
| 71 | |||
| 72 | let edit = { | ||
| 73 | let mut builder = TextEdit::builder(); | ||
| 74 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
| 75 | .into_text_edit(&mut builder); | ||
| 76 | builder.finish() | ||
| 77 | }; | ||
| 78 | Some(Fix::new( | ||
| 79 | "Fill struct fields", | ||
| 80 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), | ||
| 81 | sema.original_range(&old_field_list.syntax()).range, | ||
| 82 | )) | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | impl DiagnosticWithFix for MissingOkInTailExpr { | ||
| 87 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
| 88 | let root = sema.db.parse_or_expand(self.file)?; | ||
| 89 | let tail_expr = self.expr.to_node(&root); | ||
| 90 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
| 91 | let edit = TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax())); | ||
| 92 | let source_change = | ||
| 93 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); | ||
| 94 | Some(Fix::new("Wrap with ok", source_change, tail_expr_range)) | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | fn missing_record_expr_field_fix( | ||
| 99 | sema: &Semantics<RootDatabase>, | ||
| 100 | usage_file_id: FileId, | ||
| 101 | record_expr_field: &ast::RecordExprField, | ||
| 102 | ) -> Option<Fix> { | ||
| 103 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
| 104 | let def_id = sema.resolve_variant(record_lit)?; | ||
| 105 | let module; | ||
| 106 | let def_file_id; | ||
| 107 | let record_fields = match VariantDef::from(def_id) { | ||
| 108 | VariantDef::Struct(s) => { | ||
| 109 | module = s.module(sema.db); | ||
| 110 | let source = s.source(sema.db); | ||
| 111 | def_file_id = source.file_id; | ||
| 112 | let fields = source.value.field_list()?; | ||
| 113 | record_field_list(fields)? | ||
| 114 | } | ||
| 115 | VariantDef::Union(u) => { | ||
| 116 | module = u.module(sema.db); | ||
| 117 | let source = u.source(sema.db); | ||
| 118 | def_file_id = source.file_id; | ||
| 119 | source.value.record_field_list()? | ||
| 120 | } | ||
| 121 | VariantDef::EnumVariant(e) => { | ||
| 122 | module = e.module(sema.db); | ||
| 123 | let source = e.source(sema.db); | ||
| 124 | def_file_id = source.file_id; | ||
| 125 | let fields = source.value.field_list()?; | ||
| 126 | record_field_list(fields)? | ||
| 127 | } | ||
| 128 | }; | ||
| 129 | let def_file_id = def_file_id.original_file(sema.db); | ||
| 130 | |||
| 131 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
| 132 | if new_field_type.is_unknown() { | ||
| 133 | return None; | ||
| 134 | } | ||
| 135 | let new_field = make::record_field( | ||
| 136 | record_expr_field.field_name()?, | ||
| 137 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
| 138 | ); | ||
| 139 | |||
| 140 | let last_field = record_fields.fields().last()?; | ||
| 141 | let last_field_syntax = last_field.syntax(); | ||
| 142 | let indent = IndentLevel::from_node(last_field_syntax); | ||
| 143 | |||
| 144 | let mut new_field = new_field.to_string(); | ||
| 145 | if usage_file_id != def_file_id { | ||
| 146 | new_field = format!("pub(crate) {}", new_field); | ||
| 147 | } | ||
| 148 | new_field = format!("\n{}{}", indent, new_field); | ||
| 149 | |||
| 150 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
| 151 | if needs_comma { | ||
| 152 | new_field = format!(",{}", new_field); | ||
| 153 | } | ||
| 154 | |||
| 155 | let source_change = SourceFileEdit { | ||
| 156 | file_id: def_file_id, | ||
| 157 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
| 158 | }; | ||
| 159 | return Some(Fix::new( | ||
| 160 | "Create field", | ||
| 161 | source_change.into(), | ||
| 162 | record_expr_field.syntax().text_range(), | ||
| 163 | )); | ||
| 164 | |||
| 165 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
| 166 | match field_def_list { | ||
| 167 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
| 168 | ast::FieldList::TupleFieldList(_) => None, | ||
| 169 | } | ||
| 170 | } | ||
| 171 | } | ||
