aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide')
-rw-r--r--crates/ra_ide/src/diagnostics.rs184
-rw-r--r--crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs171
-rw-r--r--crates/ra_ide/src/lib.rs10
3 files changed, 211 insertions, 154 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs
index 73c0b8275..1046d7ab3 100644
--- a/crates/ra_ide/src/diagnostics.rs
+++ b/crates/ra_ide/src/diagnostics.rs
@@ -6,22 +6,21 @@
6 6
7use std::cell::RefCell; 7use std::cell::RefCell;
8 8
9use hir::{ 9use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
10 diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder},
11 HasSource, HirDisplay, Semantics, VariantDef,
12};
13use itertools::Itertools; 10use itertools::Itertools;
14use ra_db::SourceDatabase; 11use ra_db::SourceDatabase;
15use ra_ide_db::RootDatabase; 12use ra_ide_db::RootDatabase;
16use ra_prof::profile; 13use ra_prof::profile;
17use ra_syntax::{ 14use ra_syntax::{
18 algo, 15 ast::{self, AstNode},
19 ast::{self, edit::IndentLevel, make, AstNode},
20 SyntaxNode, TextRange, T, 16 SyntaxNode, TextRange, T,
21}; 17};
22use ra_text_edit::{TextEdit, TextEditBuilder}; 18use ra_text_edit::{TextEdit, TextEditBuilder};
23 19
24use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; 20use crate::{Diagnostic, FileId, Fix, SourceFileEdit};
21
22mod diagnostics_with_fix;
23use diagnostics_with_fix::DiagnosticWithFix;
25 24
26#[derive(Debug, Copy, Clone)] 25#[derive(Debug, Copy, Clone)]
27pub enum Severity { 26pub enum Severity {
@@ -54,73 +53,16 @@ pub(crate) fn diagnostics(
54 let res = RefCell::new(res); 53 let res = RefCell::new(res);
55 let mut sink = DiagnosticSinkBuilder::new() 54 let mut sink = DiagnosticSinkBuilder::new()
56 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 55 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
57 let original_file = d.source().file_id.original_file(db); 56 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
58 let fix = Fix::new(
59 "Create module",
60 FileSystemEdit::CreateFile { anchor: original_file, dst: d.candidate.clone() }
61 .into(),
62 );
63 res.borrow_mut().push(Diagnostic {
64 range: sema.diagnostics_range(d).range,
65 message: d.message(),
66 severity: Severity::Error,
67 fix: Some(fix),
68 })
69 }) 57 })
70 .on::<hir::diagnostics::MissingFields, _>(|d| { 58 .on::<hir::diagnostics::MissingFields, _>(|d| {
71 // Note that although we could add a diagnostics to 59 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
72 // fill the missing tuple field, e.g :
73 // `struct A(usize);`
74 // `let a = A { 0: () }`
75 // but it is uncommon usage and it should not be encouraged.
76 let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
77 None
78 } else {
79 let mut field_list = d.ast(db);
80 for f in d.missed_fields.iter() {
81 let field = make::record_expr_field(
82 make::name_ref(&f.to_string()),
83 Some(make::expr_unit()),
84 );
85 field_list = field_list.append_field(&field);
86 }
87
88 let edit = {
89 let mut builder = TextEditBuilder::default();
90 algo::diff(&d.ast(db).syntax(), &field_list.syntax())
91 .into_text_edit(&mut builder);
92 builder.finish()
93 };
94 Some(Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into()))
95 };
96
97 res.borrow_mut().push(Diagnostic {
98 range: sema.diagnostics_range(d).range,
99 message: d.message(),
100 severity: Severity::Error,
101 fix,
102 })
103 }) 60 })
104 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { 61 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
105 let node = d.ast(db); 62 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
106 let replacement = format!("Ok({})", node.syntax());
107 let edit = TextEdit::replace(node.syntax().text_range(), replacement);
108 let source_change = SourceFileEdit { file_id, edit }.into();
109 let fix = Fix::new("Wrap with ok", source_change);
110 res.borrow_mut().push(Diagnostic {
111 range: sema.diagnostics_range(d).range,
112 message: d.message(),
113 severity: Severity::Error,
114 fix: Some(fix),
115 })
116 }) 63 })
117 .on::<hir::diagnostics::NoSuchField, _>(|d| { 64 .on::<hir::diagnostics::NoSuchField, _>(|d| {
118 res.borrow_mut().push(Diagnostic { 65 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
119 range: sema.diagnostics_range(d).range,
120 message: d.message(),
121 severity: Severity::Error,
122 fix: missing_struct_field_fix(&sema, file_id, d),
123 })
124 }) 66 })
125 // Only collect experimental diagnostics when they're enabled. 67 // Only collect experimental diagnostics when they're enabled.
126 .filter(|diag| !diag.is_experimental() || enable_experimental) 68 .filter(|diag| !diag.is_experimental() || enable_experimental)
@@ -128,7 +70,7 @@ pub(crate) fn diagnostics(
128 .build(|d| { 70 .build(|d| {
129 res.borrow_mut().push(Diagnostic { 71 res.borrow_mut().push(Diagnostic {
130 message: d.message(), 72 message: d.message(),
131 range: sema.diagnostics_range(d).range, 73 range: sema.diagnostics_display_range(d).range,
132 severity: Severity::Error, 74 severity: Severity::Error,
133 fix: None, 75 fix: None,
134 }) 76 })
@@ -141,77 +83,12 @@ pub(crate) fn diagnostics(
141 res.into_inner() 83 res.into_inner()
142} 84}
143 85
144fn missing_struct_field_fix( 86fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
145 sema: &Semantics<RootDatabase>, 87 Diagnostic {
146 usage_file_id: FileId, 88 range: sema.diagnostics_display_range(d).range,
147 d: &hir::diagnostics::NoSuchField, 89 message: d.message(),
148) -> Option<Fix> { 90 severity: Severity::Error,
149 let record_expr = sema.ast(d); 91 fix: d.fix(&sema),
150
151 let record_lit = ast::RecordExpr::cast(record_expr.syntax().parent()?.parent()?)?;
152 let def_id = sema.resolve_variant(record_lit)?;
153 let module;
154 let def_file_id;
155 let record_fields = match VariantDef::from(def_id) {
156 VariantDef::Struct(s) => {
157 module = s.module(sema.db);
158 let source = s.source(sema.db);
159 def_file_id = source.file_id;
160 let fields = source.value.field_list()?;
161 record_field_list(fields)?
162 }
163 VariantDef::Union(u) => {
164 module = u.module(sema.db);
165 let source = u.source(sema.db);
166 def_file_id = source.file_id;
167 source.value.record_field_list()?
168 }
169 VariantDef::EnumVariant(e) => {
170 module = e.module(sema.db);
171 let source = e.source(sema.db);
172 def_file_id = source.file_id;
173 let fields = source.value.field_list()?;
174 record_field_list(fields)?
175 }
176 };
177 let def_file_id = def_file_id.original_file(sema.db);
178
179 let new_field_type = sema.type_of_expr(&record_expr.expr()?)?;
180 if new_field_type.is_unknown() {
181 return None;
182 }
183 let new_field = make::record_field(
184 record_expr.field_name()?,
185 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
186 );
187
188 let last_field = record_fields.fields().last()?;
189 let last_field_syntax = last_field.syntax();
190 let indent = IndentLevel::from_node(last_field_syntax);
191
192 let mut new_field = new_field.to_string();
193 if usage_file_id != def_file_id {
194 new_field = format!("pub(crate) {}", new_field);
195 }
196 new_field = format!("\n{}{}", indent, new_field);
197
198 let needs_comma = !last_field_syntax.to_string().ends_with(',');
199 if needs_comma {
200 new_field = format!(",{}", new_field);
201 }
202
203 let source_change = SourceFileEdit {
204 file_id: def_file_id,
205 edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
206 };
207 let fix = Fix::new("Create field", source_change.into());
208 return Some(fix);
209
210 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
211 match field_def_list {
212 ast::FieldList::RecordFieldList(it) => Some(it),
213 ast::FieldList::TupleFieldList(_) => None,
214 }
215 } 92 }
216} 93}
217 94
@@ -222,24 +99,25 @@ fn check_unnecessary_braces_in_use_statement(
222) -> Option<()> { 99) -> Option<()> {
223 let use_tree_list = ast::UseTreeList::cast(node.clone())?; 100 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
224 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { 101 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
225 let range = use_tree_list.syntax().text_range(); 102 let use_range = use_tree_list.syntax().text_range();
226 let edit = 103 let edit =
227 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) 104 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
228 .unwrap_or_else(|| { 105 .unwrap_or_else(|| {
229 let to_replace = single_use_tree.syntax().text().to_string(); 106 let to_replace = single_use_tree.syntax().text().to_string();
230 let mut edit_builder = TextEditBuilder::default(); 107 let mut edit_builder = TextEditBuilder::default();
231 edit_builder.delete(range); 108 edit_builder.delete(use_range);
232 edit_builder.insert(range.start(), to_replace); 109 edit_builder.insert(use_range.start(), to_replace);
233 edit_builder.finish() 110 edit_builder.finish()
234 }); 111 });
235 112
236 acc.push(Diagnostic { 113 acc.push(Diagnostic {
237 range, 114 range: use_range,
238 message: "Unnecessary braces in use statement".to_string(), 115 message: "Unnecessary braces in use statement".to_string(),
239 severity: Severity::WeakWarning, 116 severity: Severity::WeakWarning,
240 fix: Some(Fix::new( 117 fix: Some(Fix::new(
241 "Remove unnecessary braces", 118 "Remove unnecessary braces",
242 SourceFileEdit { file_id, edit }.into(), 119 SourceFileEdit { file_id, edit }.into(),
120 use_range,
243 )), 121 )),
244 }); 122 });
245 } 123 }
@@ -254,8 +132,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
254 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { 132 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
255 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); 133 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
256 let end = use_tree_list_node.text_range().end(); 134 let end = use_tree_list_node.text_range().end();
257 let range = TextRange::new(start, end); 135 return Some(TextEdit::delete(TextRange::new(start, end)));
258 return Some(TextEdit::delete(range));
259 } 136 }
260 None 137 None
261} 138}
@@ -278,13 +155,15 @@ fn check_struct_shorthand_initialization(
278 edit_builder.insert(record_field.syntax().text_range().start(), field_name); 155 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
279 let edit = edit_builder.finish(); 156 let edit = edit_builder.finish();
280 157
158 let field_range = record_field.syntax().text_range();
281 acc.push(Diagnostic { 159 acc.push(Diagnostic {
282 range: record_field.syntax().text_range(), 160 range: field_range,
283 message: "Shorthand struct initialization".to_string(), 161 message: "Shorthand struct initialization".to_string(),
284 severity: Severity::WeakWarning, 162 severity: Severity::WeakWarning,
285 fix: Some(Fix::new( 163 fix: Some(Fix::new(
286 "Use struct shorthand initialization", 164 "Use struct shorthand initialization",
287 SourceFileEdit { file_id, edit }.into(), 165 SourceFileEdit { file_id, edit }.into(),
166 field_range,
288 )), 167 )),
289 }); 168 });
290 } 169 }
@@ -304,7 +183,7 @@ mod tests {
304 /// Takes a multi-file input fixture with annotated cursor positions, 183 /// Takes a multi-file input fixture with annotated cursor positions,
305 /// and checks that: 184 /// and checks that:
306 /// * a diagnostic is produced 185 /// * a diagnostic is produced
307 /// * this diagnostic touches the input cursor position 186 /// * this diagnostic fix trigger range touches the input cursor position
308 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied 187 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
309 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { 188 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
310 let after = trim_indent(ra_fixture_after); 189 let after = trim_indent(ra_fixture_after);
@@ -322,10 +201,10 @@ mod tests {
322 201
323 assert_eq_text!(&after, &actual); 202 assert_eq_text!(&after, &actual);
324 assert!( 203 assert!(
325 diagnostic.range.start() <= file_position.offset 204 fix.fix_trigger_range.start() <= file_position.offset
326 && diagnostic.range.end() >= file_position.offset, 205 && fix.fix_trigger_range.end() >= file_position.offset,
327 "diagnostic range {:?} does not touch cursor position {:?}", 206 "diagnostic fix range {:?} does not touch cursor position {:?}",
328 diagnostic.range, 207 fix.fix_trigger_range,
329 file_position.offset 208 file_position.offset
330 ); 209 );
331 } 210 }
@@ -642,6 +521,7 @@ fn test_fn() {
642 ], 521 ],
643 is_snippet: false, 522 is_snippet: false,
644 }, 523 },
524 fix_trigger_range: 0..8,
645 }, 525 },
646 ), 526 ),
647 }, 527 },
diff --git a/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs
new file mode 100644
index 000000000..f7c73773f
--- /dev/null
+++ b/crates/ra_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.
3use crate::Fix;
4use ast::{edit::IndentLevel, make};
5use hir::{
6 db::AstDatabase,
7 diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule},
8 HasSource, HirDisplay, Semantics, VariantDef,
9};
10use ra_db::FileId;
11use ra_ide_db::{
12 source_change::{FileSystemEdit, SourceFileEdit},
13 RootDatabase,
14};
15use ra_syntax::{algo, ast, AstNode};
16use ra_text_edit::{TextEdit, TextEditBuilder};
17
18/// A [Diagnostic] that potentially has a fix available.
19///
20/// [Diagnostic]: hir::diagnostics::Diagnostic
21pub trait DiagnosticWithFix: Diagnostic {
22 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>;
23}
24
25impl 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
41impl 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
52impl 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 = TextEditBuilder::default();
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
86impl 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
98fn 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}
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 0fede0d87..89fcb6f17 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -112,13 +112,19 @@ pub struct Diagnostic {
112pub struct Fix { 112pub struct Fix {
113 pub label: String, 113 pub label: String,
114 pub source_change: SourceChange, 114 pub source_change: SourceChange,
115 /// Allows to trigger the fix only when the caret is in the range given
116 pub fix_trigger_range: TextRange,
115} 117}
116 118
117impl Fix { 119impl Fix {
118 pub fn new(label: impl Into<String>, source_change: SourceChange) -> Self { 120 pub fn new(
121 label: impl Into<String>,
122 source_change: SourceChange,
123 fix_trigger_range: TextRange,
124 ) -> Self {
119 let label = label.into(); 125 let label = label.into();
120 assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.')); 126 assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.'));
121 Self { label, source_change } 127 Self { label, source_change, fix_trigger_range }
122 } 128 }
123} 129}
124 130