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