diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-08-12 14:44:13 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-08-12 14:44:13 +0100 |
commit | 5b8fdfe23100b88e4fd8e210ccf6b852f5c9bf2a (patch) | |
tree | e321c900fe4997ec5ffe20cbb09946502745849c | |
parent | 11de7ac2fb6514484076217acb8d93eb36468681 (diff) | |
parent | db12ccee96bf37367b39ad99638d06da7123c088 (diff) |
Merge #5553
5553: Add fix ranges for diagnostics r=matklad a=SomeoneToIgnore
A follow-up of https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/Less.20red.20in.20the.20code
Now diagnostics can apply fixes in a range that's different from the range used to highlight the diagnostics.
Previous logic did not consider the fix range, having both ranges equal, which could cause a lot of red noise in the editor.
Now, the fix range gets used with the fix, the diagnostics range is used for everything else which allows to improve the error highlighting.
before:
<img width="191" alt="image" src="https://user-images.githubusercontent.com/2690773/88590727-df9a6a00-d063-11ea-97ed-9809c1c5e6e6.png">
after:
<img width="222" alt="image" src="https://user-images.githubusercontent.com/2690773/88590734-e1fcc400-d063-11ea-9b7c-25701cbd5352.png">
`MissingFields` and `MissingPatFields` diagnostics now have the fix range as `ast::RecordFieldList` of the expression with an error (as it was before this PR), and the diagnostics range as a `ast::Path` of the expression, if it's present (do you have any example of `ast::Expr::RecordLit` that has no path btw?).
The rest of the diagnostics have both ranges equal, same as it was before this PR.
Co-authored-by: Kirill Bulatov <[email protected]>
-rw-r--r-- | crates/ra_assists/src/handlers/fix_visibility.rs | 2 | ||||
-rw-r--r-- | crates/ra_hir/src/diagnostics.rs | 4 | ||||
-rw-r--r-- | crates/ra_hir/src/semantics.rs | 18 | ||||
-rw-r--r-- | crates/ra_hir_def/src/diagnostics.rs | 2 | ||||
-rw-r--r-- | crates/ra_hir_expand/src/diagnostics.rs | 25 | ||||
-rw-r--r-- | crates/ra_hir_ty/src/diagnostics.rs | 128 | ||||
-rw-r--r-- | crates/ra_hir_ty/src/diagnostics/expr.rs | 14 | ||||
-rw-r--r-- | crates/ra_hir_ty/src/diagnostics/match_check.rs | 8 | ||||
-rw-r--r-- | crates/ra_ide/src/diagnostics.rs | 184 | ||||
-rw-r--r-- | crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs | 171 | ||||
-rw-r--r-- | crates/ra_ide/src/lib.rs | 10 | ||||
-rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 9 |
12 files changed, 288 insertions, 287 deletions
diff --git a/crates/ra_assists/src/handlers/fix_visibility.rs b/crates/ra_assists/src/handlers/fix_visibility.rs index 1aefa79cc..a19dbf33f 100644 --- a/crates/ra_assists/src/handlers/fix_visibility.rs +++ b/crates/ra_assists/src/handlers/fix_visibility.rs | |||
@@ -121,7 +121,7 @@ fn add_vis_to_referenced_record_field(acc: &mut Assists, ctx: &AssistContext) -> | |||
121 | Some(cap) => match current_visibility { | 121 | Some(cap) => match current_visibility { |
122 | Some(current_visibility) => builder.replace_snippet( | 122 | Some(current_visibility) => builder.replace_snippet( |
123 | cap, | 123 | cap, |
124 | dbg!(current_visibility.syntax()).text_range(), | 124 | current_visibility.syntax().text_range(), |
125 | format!("$0{}", missing_visibility), | 125 | format!("$0{}", missing_visibility), |
126 | ), | 126 | ), |
127 | None => builder.insert_snippet(cap, offset, format!("$0{} ", missing_visibility)), | 127 | None => builder.insert_snippet(cap, offset, format!("$0{} ", missing_visibility)), |
diff --git a/crates/ra_hir/src/diagnostics.rs b/crates/ra_hir/src/diagnostics.rs index 266b513dc..363164b9b 100644 --- a/crates/ra_hir/src/diagnostics.rs +++ b/crates/ra_hir/src/diagnostics.rs | |||
@@ -1,8 +1,6 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! FIXME: write short doc here |
2 | pub use hir_def::diagnostics::UnresolvedModule; | 2 | pub use hir_def::diagnostics::UnresolvedModule; |
3 | pub use hir_expand::diagnostics::{ | 3 | pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder}; |
4 | AstDiagnostic, Diagnostic, DiagnosticSink, DiagnosticSinkBuilder, | ||
5 | }; | ||
6 | pub use hir_ty::diagnostics::{ | 4 | pub use hir_ty::diagnostics::{ |
7 | MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkInTailExpr, NoSuchField, | 5 | MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkInTailExpr, NoSuchField, |
8 | }; | 6 | }; |
diff --git a/crates/ra_hir/src/semantics.rs b/crates/ra_hir/src/semantics.rs index 872f5fa4c..36b688ccb 100644 --- a/crates/ra_hir/src/semantics.rs +++ b/crates/ra_hir/src/semantics.rs | |||
@@ -8,7 +8,7 @@ use hir_def::{ | |||
8 | resolver::{self, HasResolver, Resolver}, | 8 | resolver::{self, HasResolver, Resolver}, |
9 | AsMacroCall, FunctionId, TraitId, VariantId, | 9 | AsMacroCall, FunctionId, TraitId, VariantId, |
10 | }; | 10 | }; |
11 | use hir_expand::{diagnostics::AstDiagnostic, hygiene::Hygiene, name::AsName, ExpansionInfo}; | 11 | use hir_expand::{hygiene::Hygiene, name::AsName, ExpansionInfo}; |
12 | use hir_ty::associated_type_shorthand_candidates; | 12 | use hir_ty::associated_type_shorthand_candidates; |
13 | use itertools::Itertools; | 13 | use itertools::Itertools; |
14 | use ra_db::{FileId, FileRange}; | 14 | use ra_db::{FileId, FileRange}; |
@@ -110,13 +110,6 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> { | |||
110 | self.imp.parse(file_id) | 110 | self.imp.parse(file_id) |
111 | } | 111 | } |
112 | 112 | ||
113 | pub fn ast<T: AstDiagnostic + Diagnostic>(&self, d: &T) -> <T as AstDiagnostic>::AST { | ||
114 | let file_id = d.source().file_id; | ||
115 | let root = self.db.parse_or_expand(file_id).unwrap(); | ||
116 | self.imp.cache(root, file_id); | ||
117 | d.ast(self.db.upcast()) | ||
118 | } | ||
119 | |||
120 | pub fn expand(&self, macro_call: &ast::MacroCall) -> Option<SyntaxNode> { | 113 | pub fn expand(&self, macro_call: &ast::MacroCall) -> Option<SyntaxNode> { |
121 | self.imp.expand(macro_call) | 114 | self.imp.expand(macro_call) |
122 | } | 115 | } |
@@ -146,8 +139,8 @@ impl<'db, DB: HirDatabase> Semantics<'db, DB> { | |||
146 | self.imp.original_range(node) | 139 | self.imp.original_range(node) |
147 | } | 140 | } |
148 | 141 | ||
149 | pub fn diagnostics_range(&self, diagnostics: &dyn Diagnostic) -> FileRange { | 142 | pub fn diagnostics_display_range(&self, diagnostics: &dyn Diagnostic) -> FileRange { |
150 | self.imp.diagnostics_range(diagnostics) | 143 | self.imp.diagnostics_display_range(diagnostics) |
151 | } | 144 | } |
152 | 145 | ||
153 | pub fn ancestors_with_macros(&self, node: SyntaxNode) -> impl Iterator<Item = SyntaxNode> + '_ { | 146 | pub fn ancestors_with_macros(&self, node: SyntaxNode) -> impl Iterator<Item = SyntaxNode> + '_ { |
@@ -389,10 +382,11 @@ impl<'db> SemanticsImpl<'db> { | |||
389 | original_range(self.db, node.as_ref()) | 382 | original_range(self.db, node.as_ref()) |
390 | } | 383 | } |
391 | 384 | ||
392 | fn diagnostics_range(&self, diagnostics: &dyn Diagnostic) -> FileRange { | 385 | fn diagnostics_display_range(&self, diagnostics: &dyn Diagnostic) -> FileRange { |
393 | let src = diagnostics.source(); | 386 | let src = diagnostics.display_source(); |
394 | let root = self.db.parse_or_expand(src.file_id).unwrap(); | 387 | let root = self.db.parse_or_expand(src.file_id).unwrap(); |
395 | let node = src.value.to_node(&root); | 388 | let node = src.value.to_node(&root); |
389 | self.cache(root, src.file_id); | ||
396 | original_range(self.db, src.with_value(&node)) | 390 | original_range(self.db, src.with_value(&node)) |
397 | } | 391 | } |
398 | 392 | ||
diff --git a/crates/ra_hir_def/src/diagnostics.rs b/crates/ra_hir_def/src/diagnostics.rs index 30db48f86..71d177070 100644 --- a/crates/ra_hir_def/src/diagnostics.rs +++ b/crates/ra_hir_def/src/diagnostics.rs | |||
@@ -18,7 +18,7 @@ impl Diagnostic for UnresolvedModule { | |||
18 | fn message(&self) -> String { | 18 | fn message(&self) -> String { |
19 | "unresolved module".to_string() | 19 | "unresolved module".to_string() |
20 | } | 20 | } |
21 | fn source(&self) -> InFile<SyntaxNodePtr> { | 21 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
22 | InFile::new(self.file, self.decl.clone().into()) | 22 | InFile::new(self.file, self.decl.clone().into()) |
23 | } | 23 | } |
24 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 24 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
diff --git a/crates/ra_hir_expand/src/diagnostics.rs b/crates/ra_hir_expand/src/diagnostics.rs index 84ba97b14..b138500e7 100644 --- a/crates/ra_hir_expand/src/diagnostics.rs +++ b/crates/ra_hir_expand/src/diagnostics.rs | |||
@@ -16,35 +16,20 @@ | |||
16 | 16 | ||
17 | use std::{any::Any, fmt}; | 17 | use std::{any::Any, fmt}; |
18 | 18 | ||
19 | use ra_syntax::{SyntaxNode, SyntaxNodePtr}; | 19 | use ra_syntax::SyntaxNodePtr; |
20 | 20 | ||
21 | use crate::{db::AstDatabase, InFile}; | 21 | use crate::InFile; |
22 | 22 | ||
23 | pub trait Diagnostic: Any + Send + Sync + fmt::Debug + 'static { | 23 | pub trait Diagnostic: Any + Send + Sync + fmt::Debug + 'static { |
24 | fn message(&self) -> String; | 24 | fn message(&self) -> String; |
25 | fn source(&self) -> InFile<SyntaxNodePtr>; | 25 | /// Used in highlighting and related purposes |
26 | fn display_source(&self) -> InFile<SyntaxNodePtr>; | ||
26 | fn as_any(&self) -> &(dyn Any + Send + 'static); | 27 | fn as_any(&self) -> &(dyn Any + Send + 'static); |
27 | fn is_experimental(&self) -> bool { | 28 | fn is_experimental(&self) -> bool { |
28 | false | 29 | false |
29 | } | 30 | } |
30 | } | 31 | } |
31 | 32 | ||
32 | pub trait AstDiagnostic { | ||
33 | type AST; | ||
34 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST; | ||
35 | } | ||
36 | |||
37 | impl dyn Diagnostic { | ||
38 | pub fn syntax_node(&self, db: &impl AstDatabase) -> SyntaxNode { | ||
39 | let node = db.parse_or_expand(self.source().file_id).unwrap(); | ||
40 | self.source().value.to_node(&node) | ||
41 | } | ||
42 | |||
43 | pub fn downcast_ref<D: Diagnostic>(&self) -> Option<&D> { | ||
44 | self.as_any().downcast_ref() | ||
45 | } | ||
46 | } | ||
47 | |||
48 | pub struct DiagnosticSink<'a> { | 33 | pub struct DiagnosticSink<'a> { |
49 | callbacks: Vec<Box<dyn FnMut(&dyn Diagnostic) -> Result<(), ()> + 'a>>, | 34 | callbacks: Vec<Box<dyn FnMut(&dyn Diagnostic) -> Result<(), ()> + 'a>>, |
50 | filters: Vec<Box<dyn FnMut(&dyn Diagnostic) -> bool + 'a>>, | 35 | filters: Vec<Box<dyn FnMut(&dyn Diagnostic) -> bool + 'a>>, |
@@ -89,7 +74,7 @@ impl<'a> DiagnosticSinkBuilder<'a> { | |||
89 | } | 74 | } |
90 | 75 | ||
91 | pub fn on<D: Diagnostic, F: FnMut(&D) + 'a>(mut self, mut cb: F) -> Self { | 76 | pub fn on<D: Diagnostic, F: FnMut(&D) + 'a>(mut self, mut cb: F) -> Self { |
92 | let cb = move |diag: &dyn Diagnostic| match diag.downcast_ref::<D>() { | 77 | let cb = move |diag: &dyn Diagnostic| match diag.as_any().downcast_ref::<D>() { |
93 | Some(d) => { | 78 | Some(d) => { |
94 | cb(d); | 79 | cb(d); |
95 | Ok(()) | 80 | Ok(()) |
diff --git a/crates/ra_hir_ty/src/diagnostics.rs b/crates/ra_hir_ty/src/diagnostics.rs index 977c0525b..7ab7f79db 100644 --- a/crates/ra_hir_ty/src/diagnostics.rs +++ b/crates/ra_hir_ty/src/diagnostics.rs | |||
@@ -6,10 +6,10 @@ mod unsafe_check; | |||
6 | use std::any::Any; | 6 | use std::any::Any; |
7 | 7 | ||
8 | use hir_def::DefWithBodyId; | 8 | use hir_def::DefWithBodyId; |
9 | use hir_expand::diagnostics::{AstDiagnostic, Diagnostic, DiagnosticSink}; | 9 | use hir_expand::diagnostics::{Diagnostic, DiagnosticSink}; |
10 | use hir_expand::{db::AstDatabase, name::Name, HirFileId, InFile}; | 10 | use hir_expand::{name::Name, HirFileId, InFile}; |
11 | use ra_prof::profile; | 11 | use ra_prof::profile; |
12 | use ra_syntax::{ast, AstNode, AstPtr, SyntaxNodePtr}; | 12 | use ra_syntax::{ast, AstPtr, SyntaxNodePtr}; |
13 | use stdx::format_to; | 13 | use stdx::format_to; |
14 | 14 | ||
15 | use crate::db::HirDatabase; | 15 | use crate::db::HirDatabase; |
@@ -37,7 +37,7 @@ impl Diagnostic for NoSuchField { | |||
37 | "no such field".to_string() | 37 | "no such field".to_string() |
38 | } | 38 | } |
39 | 39 | ||
40 | fn source(&self) -> InFile<SyntaxNodePtr> { | 40 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
41 | InFile::new(self.file, self.field.clone().into()) | 41 | InFile::new(self.file, self.field.clone().into()) |
42 | } | 42 | } |
43 | 43 | ||
@@ -46,20 +46,11 @@ impl Diagnostic for NoSuchField { | |||
46 | } | 46 | } |
47 | } | 47 | } |
48 | 48 | ||
49 | impl AstDiagnostic for NoSuchField { | ||
50 | type AST = ast::RecordExprField; | ||
51 | |||
52 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
53 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | ||
54 | let node = self.source().value.to_node(&root); | ||
55 | ast::RecordExprField::cast(node).unwrap() | ||
56 | } | ||
57 | } | ||
58 | |||
59 | #[derive(Debug)] | 49 | #[derive(Debug)] |
60 | pub struct MissingFields { | 50 | pub struct MissingFields { |
61 | pub file: HirFileId, | 51 | pub file: HirFileId, |
62 | pub field_list: AstPtr<ast::RecordExprFieldList>, | 52 | pub field_list_parent: AstPtr<ast::RecordExpr>, |
53 | pub field_list_parent_path: Option<AstPtr<ast::Path>>, | ||
63 | pub missed_fields: Vec<Name>, | 54 | pub missed_fields: Vec<Name>, |
64 | } | 55 | } |
65 | 56 | ||
@@ -71,28 +62,28 @@ impl Diagnostic for MissingFields { | |||
71 | } | 62 | } |
72 | buf | 63 | buf |
73 | } | 64 | } |
74 | fn source(&self) -> InFile<SyntaxNodePtr> { | 65 | |
75 | InFile { file_id: self.file, value: self.field_list.clone().into() } | 66 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
67 | InFile { | ||
68 | file_id: self.file, | ||
69 | value: self | ||
70 | .field_list_parent_path | ||
71 | .clone() | ||
72 | .map(SyntaxNodePtr::from) | ||
73 | .unwrap_or_else(|| self.field_list_parent.clone().into()), | ||
74 | } | ||
76 | } | 75 | } |
76 | |||
77 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 77 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
78 | self | 78 | self |
79 | } | 79 | } |
80 | } | 80 | } |
81 | 81 | ||
82 | impl AstDiagnostic for MissingFields { | ||
83 | type AST = ast::RecordExprFieldList; | ||
84 | |||
85 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
86 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | ||
87 | let node = self.source().value.to_node(&root); | ||
88 | ast::RecordExprFieldList::cast(node).unwrap() | ||
89 | } | ||
90 | } | ||
91 | |||
92 | #[derive(Debug)] | 82 | #[derive(Debug)] |
93 | pub struct MissingPatFields { | 83 | pub struct MissingPatFields { |
94 | pub file: HirFileId, | 84 | pub file: HirFileId, |
95 | pub field_list: AstPtr<ast::RecordPatFieldList>, | 85 | pub field_list_parent: AstPtr<ast::RecordPat>, |
86 | pub field_list_parent_path: Option<AstPtr<ast::Path>>, | ||
96 | pub missed_fields: Vec<Name>, | 87 | pub missed_fields: Vec<Name>, |
97 | } | 88 | } |
98 | 89 | ||
@@ -104,8 +95,15 @@ impl Diagnostic for MissingPatFields { | |||
104 | } | 95 | } |
105 | buf | 96 | buf |
106 | } | 97 | } |
107 | fn source(&self) -> InFile<SyntaxNodePtr> { | 98 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
108 | InFile { file_id: self.file, value: self.field_list.clone().into() } | 99 | InFile { |
100 | file_id: self.file, | ||
101 | value: self | ||
102 | .field_list_parent_path | ||
103 | .clone() | ||
104 | .map(SyntaxNodePtr::from) | ||
105 | .unwrap_or_else(|| self.field_list_parent.clone().into()), | ||
106 | } | ||
109 | } | 107 | } |
110 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 108 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
111 | self | 109 | self |
@@ -123,7 +121,7 @@ impl Diagnostic for MissingMatchArms { | |||
123 | fn message(&self) -> String { | 121 | fn message(&self) -> String { |
124 | String::from("Missing match arm") | 122 | String::from("Missing match arm") |
125 | } | 123 | } |
126 | fn source(&self) -> InFile<SyntaxNodePtr> { | 124 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
127 | InFile { file_id: self.file, value: self.match_expr.clone().into() } | 125 | InFile { file_id: self.file, value: self.match_expr.clone().into() } |
128 | } | 126 | } |
129 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 127 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
@@ -141,7 +139,7 @@ impl Diagnostic for MissingOkInTailExpr { | |||
141 | fn message(&self) -> String { | 139 | fn message(&self) -> String { |
142 | "wrap return expression in Ok".to_string() | 140 | "wrap return expression in Ok".to_string() |
143 | } | 141 | } |
144 | fn source(&self) -> InFile<SyntaxNodePtr> { | 142 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
145 | InFile { file_id: self.file, value: self.expr.clone().into() } | 143 | InFile { file_id: self.file, value: self.expr.clone().into() } |
146 | } | 144 | } |
147 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 145 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
@@ -149,16 +147,6 @@ impl Diagnostic for MissingOkInTailExpr { | |||
149 | } | 147 | } |
150 | } | 148 | } |
151 | 149 | ||
152 | impl AstDiagnostic for MissingOkInTailExpr { | ||
153 | type AST = ast::Expr; | ||
154 | |||
155 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
156 | let root = db.parse_or_expand(self.file).unwrap(); | ||
157 | let node = self.source().value.to_node(&root); | ||
158 | ast::Expr::cast(node).unwrap() | ||
159 | } | ||
160 | } | ||
161 | |||
162 | #[derive(Debug)] | 150 | #[derive(Debug)] |
163 | pub struct BreakOutsideOfLoop { | 151 | pub struct BreakOutsideOfLoop { |
164 | pub file: HirFileId, | 152 | pub file: HirFileId, |
@@ -169,7 +157,7 @@ impl Diagnostic for BreakOutsideOfLoop { | |||
169 | fn message(&self) -> String { | 157 | fn message(&self) -> String { |
170 | "break outside of loop".to_string() | 158 | "break outside of loop".to_string() |
171 | } | 159 | } |
172 | fn source(&self) -> InFile<SyntaxNodePtr> { | 160 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
173 | InFile { file_id: self.file, value: self.expr.clone().into() } | 161 | InFile { file_id: self.file, value: self.expr.clone().into() } |
174 | } | 162 | } |
175 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 163 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
@@ -177,16 +165,6 @@ impl Diagnostic for BreakOutsideOfLoop { | |||
177 | } | 165 | } |
178 | } | 166 | } |
179 | 167 | ||
180 | impl AstDiagnostic for BreakOutsideOfLoop { | ||
181 | type AST = ast::Expr; | ||
182 | |||
183 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
184 | let root = db.parse_or_expand(self.file).unwrap(); | ||
185 | let node = self.source().value.to_node(&root); | ||
186 | ast::Expr::cast(node).unwrap() | ||
187 | } | ||
188 | } | ||
189 | |||
190 | #[derive(Debug)] | 168 | #[derive(Debug)] |
191 | pub struct MissingUnsafe { | 169 | pub struct MissingUnsafe { |
192 | pub file: HirFileId, | 170 | pub file: HirFileId, |
@@ -197,7 +175,7 @@ impl Diagnostic for MissingUnsafe { | |||
197 | fn message(&self) -> String { | 175 | fn message(&self) -> String { |
198 | format!("This operation is unsafe and requires an unsafe function or block") | 176 | format!("This operation is unsafe and requires an unsafe function or block") |
199 | } | 177 | } |
200 | fn source(&self) -> InFile<SyntaxNodePtr> { | 178 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
201 | InFile { file_id: self.file, value: self.expr.clone().into() } | 179 | InFile { file_id: self.file, value: self.expr.clone().into() } |
202 | } | 180 | } |
203 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 181 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
@@ -205,16 +183,6 @@ impl Diagnostic for MissingUnsafe { | |||
205 | } | 183 | } |
206 | } | 184 | } |
207 | 185 | ||
208 | impl AstDiagnostic for MissingUnsafe { | ||
209 | type AST = ast::Expr; | ||
210 | |||
211 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
212 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | ||
213 | let node = self.source().value.to_node(&root); | ||
214 | ast::Expr::cast(node).unwrap() | ||
215 | } | ||
216 | } | ||
217 | |||
218 | #[derive(Debug)] | 186 | #[derive(Debug)] |
219 | pub struct MismatchedArgCount { | 187 | pub struct MismatchedArgCount { |
220 | pub file: HirFileId, | 188 | pub file: HirFileId, |
@@ -228,7 +196,7 @@ impl Diagnostic for MismatchedArgCount { | |||
228 | let s = if self.expected == 1 { "" } else { "s" }; | 196 | let s = if self.expected == 1 { "" } else { "s" }; |
229 | format!("Expected {} argument{}, found {}", self.expected, s, self.found) | 197 | format!("Expected {} argument{}, found {}", self.expected, s, self.found) |
230 | } | 198 | } |
231 | fn source(&self) -> InFile<SyntaxNodePtr> { | 199 | fn display_source(&self) -> InFile<SyntaxNodePtr> { |
232 | InFile { file_id: self.file, value: self.call_expr.clone().into() } | 200 | InFile { file_id: self.file, value: self.call_expr.clone().into() } |
233 | } | 201 | } |
234 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | 202 | fn as_any(&self) -> &(dyn Any + Send + 'static) { |
@@ -239,19 +207,13 @@ impl Diagnostic for MismatchedArgCount { | |||
239 | } | 207 | } |
240 | } | 208 | } |
241 | 209 | ||
242 | impl AstDiagnostic for MismatchedArgCount { | ||
243 | type AST = ast::CallExpr; | ||
244 | fn ast(&self, db: &dyn AstDatabase) -> Self::AST { | ||
245 | let root = db.parse_or_expand(self.source().file_id).unwrap(); | ||
246 | let node = self.source().value.to_node(&root); | ||
247 | ast::CallExpr::cast(node).unwrap() | ||
248 | } | ||
249 | } | ||
250 | |||
251 | #[cfg(test)] | 210 | #[cfg(test)] |
252 | mod tests { | 211 | mod tests { |
253 | use hir_def::{db::DefDatabase, AssocItemId, ModuleDefId}; | 212 | use hir_def::{db::DefDatabase, AssocItemId, ModuleDefId}; |
254 | use hir_expand::diagnostics::{Diagnostic, DiagnosticSinkBuilder}; | 213 | use hir_expand::{ |
214 | db::AstDatabase, | ||
215 | diagnostics::{Diagnostic, DiagnosticSinkBuilder}, | ||
216 | }; | ||
255 | use ra_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; | 217 | use ra_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; |
256 | use ra_syntax::{TextRange, TextSize}; | 218 | use ra_syntax::{TextRange, TextSize}; |
257 | use rustc_hash::FxHashMap; | 219 | use rustc_hash::FxHashMap; |
@@ -296,9 +258,11 @@ mod tests { | |||
296 | 258 | ||
297 | let mut actual: FxHashMap<FileId, Vec<(TextRange, String)>> = FxHashMap::default(); | 259 | let mut actual: FxHashMap<FileId, Vec<(TextRange, String)>> = FxHashMap::default(); |
298 | db.diagnostics(|d| { | 260 | db.diagnostics(|d| { |
299 | // FXIME: macros... | 261 | let src = d.display_source(); |
300 | let file_id = d.source().file_id.original_file(&db); | 262 | let root = db.parse_or_expand(src.file_id).unwrap(); |
301 | let range = d.syntax_node(&db).text_range(); | 263 | // FIXME: macros... |
264 | let file_id = src.file_id.original_file(&db); | ||
265 | let range = src.value.to_node(&root).text_range(); | ||
302 | let message = d.message().to_owned(); | 266 | let message = d.message().to_owned(); |
303 | actual.entry(file_id).or_default().push((range, message)); | 267 | actual.entry(file_id).or_default().push((range, message)); |
304 | }); | 268 | }); |
@@ -326,8 +290,8 @@ struct S { foo: i32, bar: () } | |||
326 | impl S { | 290 | impl S { |
327 | fn new() -> S { | 291 | fn new() -> S { |
328 | S { | 292 | S { |
329 | //^... Missing structure fields: | 293 | //^ Missing structure fields: |
330 | //| - bar | 294 | //| - bar |
331 | foo: 92, | 295 | foo: 92, |
332 | baz: 62, | 296 | baz: 62, |
333 | //^^^^^^^ no such field | 297 | //^^^^^^^ no such field |
@@ -448,8 +412,8 @@ impl Foo { | |||
448 | struct S { foo: i32, bar: () } | 412 | struct S { foo: i32, bar: () } |
449 | fn baz(s: S) { | 413 | fn baz(s: S) { |
450 | let S { foo: _ } = s; | 414 | let S { foo: _ } = s; |
451 | //^^^^^^^^^^ Missing structure fields: | 415 | //^ Missing structure fields: |
452 | // | - bar | 416 | //| - bar |
453 | } | 417 | } |
454 | "#, | 418 | "#, |
455 | ); | 419 | ); |
diff --git a/crates/ra_hir_ty/src/diagnostics/expr.rs b/crates/ra_hir_ty/src/diagnostics/expr.rs index 95bbf2d95..51adcecaf 100644 --- a/crates/ra_hir_ty/src/diagnostics/expr.rs +++ b/crates/ra_hir_ty/src/diagnostics/expr.rs | |||
@@ -100,8 +100,8 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
100 | 100 | ||
101 | if let Ok(source_ptr) = source_map.expr_syntax(id) { | 101 | if let Ok(source_ptr) = source_map.expr_syntax(id) { |
102 | let root = source_ptr.file_syntax(db.upcast()); | 102 | let root = source_ptr.file_syntax(db.upcast()); |
103 | if let ast::Expr::RecordExpr(record_lit) = &source_ptr.value.to_node(&root) { | 103 | if let ast::Expr::RecordExpr(record_expr) = &source_ptr.value.to_node(&root) { |
104 | if let Some(field_list) = record_lit.record_expr_field_list() { | 104 | if let Some(_) = record_expr.record_expr_field_list() { |
105 | let variant_data = variant_data(db.upcast(), variant_def); | 105 | let variant_data = variant_data(db.upcast(), variant_def); |
106 | let missed_fields = missed_fields | 106 | let missed_fields = missed_fields |
107 | .into_iter() | 107 | .into_iter() |
@@ -109,7 +109,8 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
109 | .collect(); | 109 | .collect(); |
110 | self.sink.push(MissingFields { | 110 | self.sink.push(MissingFields { |
111 | file: source_ptr.file_id, | 111 | file: source_ptr.file_id, |
112 | field_list: AstPtr::new(&field_list), | 112 | field_list_parent: AstPtr::new(&record_expr), |
113 | field_list_parent_path: record_expr.path().map(|path| AstPtr::new(&path)), | ||
113 | missed_fields, | 114 | missed_fields, |
114 | }) | 115 | }) |
115 | } | 116 | } |
@@ -131,7 +132,7 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
131 | if let Some(expr) = source_ptr.value.as_ref().left() { | 132 | if let Some(expr) = source_ptr.value.as_ref().left() { |
132 | let root = source_ptr.file_syntax(db.upcast()); | 133 | let root = source_ptr.file_syntax(db.upcast()); |
133 | if let ast::Pat::RecordPat(record_pat) = expr.to_node(&root) { | 134 | if let ast::Pat::RecordPat(record_pat) = expr.to_node(&root) { |
134 | if let Some(field_list) = record_pat.record_pat_field_list() { | 135 | if let Some(_) = record_pat.record_pat_field_list() { |
135 | let variant_data = variant_data(db.upcast(), variant_def); | 136 | let variant_data = variant_data(db.upcast(), variant_def); |
136 | let missed_fields = missed_fields | 137 | let missed_fields = missed_fields |
137 | .into_iter() | 138 | .into_iter() |
@@ -139,7 +140,10 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
139 | .collect(); | 140 | .collect(); |
140 | self.sink.push(MissingPatFields { | 141 | self.sink.push(MissingPatFields { |
141 | file: source_ptr.file_id, | 142 | file: source_ptr.file_id, |
142 | field_list: AstPtr::new(&field_list), | 143 | field_list_parent: AstPtr::new(&record_pat), |
144 | field_list_parent_path: record_pat | ||
145 | .path() | ||
146 | .map(|path| AstPtr::new(&path)), | ||
143 | missed_fields, | 147 | missed_fields, |
144 | }) | 148 | }) |
145 | } | 149 | } |
diff --git a/crates/ra_hir_ty/src/diagnostics/match_check.rs b/crates/ra_hir_ty/src/diagnostics/match_check.rs index 507edcb7d..deca244db 100644 --- a/crates/ra_hir_ty/src/diagnostics/match_check.rs +++ b/crates/ra_hir_ty/src/diagnostics/match_check.rs | |||
@@ -1161,15 +1161,15 @@ fn main() { | |||
1161 | //^ Missing match arm | 1161 | //^ Missing match arm |
1162 | match a { | 1162 | match a { |
1163 | Either::A { } => (), | 1163 | Either::A { } => (), |
1164 | //^^^ Missing structure fields: | 1164 | //^^^^^^^^^ Missing structure fields: |
1165 | // | - foo | 1165 | // | - foo |
1166 | Either::B => (), | 1166 | Either::B => (), |
1167 | } | 1167 | } |
1168 | match a { | 1168 | match a { |
1169 | //^ Missing match arm | 1169 | //^ Missing match arm |
1170 | Either::A { } => (), | 1170 | Either::A { } => (), |
1171 | } //^^^ Missing structure fields: | 1171 | } //^^^^^^^^^ Missing structure fields: |
1172 | // | - foo | 1172 | // | - foo |
1173 | 1173 | ||
1174 | match a { | 1174 | match a { |
1175 | Either::A { foo: true } => (), | 1175 | Either::A { foo: true } => (), |
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 | ||
7 | use std::cell::RefCell; | 7 | use std::cell::RefCell; |
8 | 8 | ||
9 | use hir::{ | 9 | use hir::{diagnostics::DiagnosticSinkBuilder, Semantics}; |
10 | diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder}, | ||
11 | HasSource, HirDisplay, Semantics, VariantDef, | ||
12 | }; | ||
13 | use itertools::Itertools; | 10 | use itertools::Itertools; |
14 | use ra_db::SourceDatabase; | 11 | use ra_db::SourceDatabase; |
15 | use ra_ide_db::RootDatabase; | 12 | use ra_ide_db::RootDatabase; |
16 | use ra_prof::profile; | 13 | use ra_prof::profile; |
17 | use ra_syntax::{ | 14 | use 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 | }; |
22 | use ra_text_edit::{TextEdit, TextEditBuilder}; | 18 | use ra_text_edit::{TextEdit, TextEditBuilder}; |
23 | 19 | ||
24 | use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; | 20 | use crate::{Diagnostic, FileId, Fix, SourceFileEdit}; |
21 | |||
22 | mod diagnostics_with_fix; | ||
23 | use diagnostics_with_fix::DiagnosticWithFix; | ||
25 | 24 | ||
26 | #[derive(Debug, Copy, Clone)] | 25 | #[derive(Debug, Copy, Clone)] |
27 | pub enum Severity { | 26 | pub 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 | ||
144 | fn missing_struct_field_fix( | 86 | fn 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. | ||
3 | use crate::Fix; | ||
4 | use ast::{edit::IndentLevel, make}; | ||
5 | use hir::{ | ||
6 | db::AstDatabase, | ||
7 | diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | ||
8 | HasSource, HirDisplay, Semantics, VariantDef, | ||
9 | }; | ||
10 | use ra_db::FileId; | ||
11 | use ra_ide_db::{ | ||
12 | source_change::{FileSystemEdit, SourceFileEdit}, | ||
13 | RootDatabase, | ||
14 | }; | ||
15 | use ra_syntax::{algo, ast, AstNode}; | ||
16 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
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 = 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 | |||
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 | } | ||
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 { | |||
112 | pub struct Fix { | 112 | pub 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 | ||
117 | impl Fix { | 119 | impl 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 | ||
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index c2afcf192..785dd2a26 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs | |||
@@ -773,12 +773,11 @@ fn handle_fixes( | |||
773 | 773 | ||
774 | let diagnostics = snap.analysis.diagnostics(file_id, snap.config.experimental_diagnostics)?; | 774 | let diagnostics = snap.analysis.diagnostics(file_id, snap.config.experimental_diagnostics)?; |
775 | 775 | ||
776 | let fixes_from_diagnostics = diagnostics | 776 | for fix in diagnostics |
777 | .into_iter() | 777 | .into_iter() |
778 | .filter_map(|d| Some((d.range, d.fix?))) | 778 | .filter_map(|d| d.fix) |
779 | .filter(|(diag_range, _fix)| diag_range.intersect(range).is_some()) | 779 | .filter(|fix| fix.fix_trigger_range.intersect(range).is_some()) |
780 | .map(|(_range, fix)| fix); | 780 | { |
781 | for fix in fixes_from_diagnostics { | ||
782 | let title = fix.label; | 781 | let title = fix.label; |
783 | let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?; | 782 | let edit = to_proto::snippet_workspace_edit(&snap, fix.source_change)?; |
784 | let action = lsp_ext::CodeAction { | 783 | let action = lsp_ext::CodeAction { |