diff options
author | Aleksey Kladov <[email protected]> | 2021-06-13 18:32:54 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2021-06-13 18:32:54 +0100 |
commit | de1fc70ccd3bf7a0850e036a12cf866a80d46458 (patch) | |
tree | d93cc6be7678b8be01a5162e80bb3f8df84e8c50 /crates/ide | |
parent | 24262f9ff6ae9ea326fa35d238704d18e99d52a1 (diff) |
internal: refactor find_map diagnostic
Diffstat (limited to 'crates/ide')
-rw-r--r-- | crates/ide/src/diagnostics.rs | 88 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes.rs | 1 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes/replace_with_find_map.rs | 84 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs | 182 |
4 files changed, 184 insertions, 171 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index e3974e1ae..ec5318594 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs | |||
@@ -13,6 +13,7 @@ mod missing_ok_or_some_in_tail_expr; | |||
13 | mod missing_unsafe; | 13 | mod missing_unsafe; |
14 | mod no_such_field; | 14 | mod no_such_field; |
15 | mod remove_this_semicolon; | 15 | mod remove_this_semicolon; |
16 | mod replace_filter_map_next_with_find_map; | ||
16 | mod unimplemented_builtin_macro; | 17 | mod unimplemented_builtin_macro; |
17 | mod unresolved_extern_crate; | 18 | mod unresolved_extern_crate; |
18 | mod unresolved_import; | 19 | mod unresolved_import; |
@@ -167,9 +168,6 @@ pub(crate) fn diagnostics( | |||
167 | .on::<hir::diagnostics::IncorrectCase, _>(|d| { | 168 | .on::<hir::diagnostics::IncorrectCase, _>(|d| { |
168 | res.borrow_mut().push(warning_with_fix(d, &sema, resolve)); | 169 | res.borrow_mut().push(warning_with_fix(d, &sema, resolve)); |
169 | }) | 170 | }) |
170 | .on::<hir::diagnostics::ReplaceFilterMapNextWithFindMap, _>(|d| { | ||
171 | res.borrow_mut().push(warning_with_fix(d, &sema, resolve)); | ||
172 | }) | ||
173 | .on::<UnlinkedFile, _>(|d| { | 171 | .on::<UnlinkedFile, _>(|d| { |
174 | // Limit diagnostic to the first few characters in the file. This matches how VS Code | 172 | // Limit diagnostic to the first few characters in the file. This matches how VS Code |
175 | // renders it with the full span, but on other editors, and is less invasive. | 173 | // renders it with the full span, but on other editors, and is less invasive. |
@@ -225,6 +223,7 @@ pub(crate) fn diagnostics( | |||
225 | AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d), | 223 | AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d), |
226 | AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d), | 224 | AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d), |
227 | AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d), | 225 | AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d), |
226 | AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), | ||
228 | AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), | 227 | AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), |
229 | AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), | 228 | AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), |
230 | AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d), | 229 | AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d), |
@@ -672,89 +671,6 @@ mod foo; | |||
672 | ); | 671 | ); |
673 | } | 672 | } |
674 | 673 | ||
675 | // Register the required standard library types to make the tests work | ||
676 | fn add_filter_map_with_find_next_boilerplate(body: &str) -> String { | ||
677 | let prefix = r#" | ||
678 | //- /main.rs crate:main deps:core | ||
679 | use core::iter::Iterator; | ||
680 | use core::option::Option::{self, Some, None}; | ||
681 | "#; | ||
682 | let suffix = r#" | ||
683 | //- /core/lib.rs crate:core | ||
684 | pub mod option { | ||
685 | pub enum Option<T> { Some(T), None } | ||
686 | } | ||
687 | pub mod iter { | ||
688 | pub trait Iterator { | ||
689 | type Item; | ||
690 | fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap } | ||
691 | fn next(&mut self) -> Option<Self::Item>; | ||
692 | } | ||
693 | pub struct FilterMap {} | ||
694 | impl Iterator for FilterMap { | ||
695 | type Item = i32; | ||
696 | fn next(&mut self) -> i32 { 7 } | ||
697 | } | ||
698 | } | ||
699 | "#; | ||
700 | format!("{}{}{}", prefix, body, suffix) | ||
701 | } | ||
702 | |||
703 | #[test] | ||
704 | fn replace_filter_map_next_with_find_map2() { | ||
705 | check_diagnostics(&add_filter_map_with_find_next_boilerplate( | ||
706 | r#" | ||
707 | fn foo() { | ||
708 | let m = [1, 2, 3].iter().filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); | ||
709 | //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ replace filter_map(..).next() with find_map(..) | ||
710 | } | ||
711 | "#, | ||
712 | )); | ||
713 | } | ||
714 | |||
715 | #[test] | ||
716 | fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() { | ||
717 | check_diagnostics(&add_filter_map_with_find_next_boilerplate( | ||
718 | r#" | ||
719 | fn foo() { | ||
720 | let m = [1, 2, 3] | ||
721 | .iter() | ||
722 | .filter_map(|x| if *x == 2 { Some (4) } else { None }) | ||
723 | .len(); | ||
724 | } | ||
725 | "#, | ||
726 | )); | ||
727 | } | ||
728 | |||
729 | #[test] | ||
730 | fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() { | ||
731 | check_diagnostics(&add_filter_map_with_find_next_boilerplate( | ||
732 | r#" | ||
733 | fn foo() { | ||
734 | let m = [1, 2, 3] | ||
735 | .iter() | ||
736 | .filter_map(|x| if *x == 2 { Some (4) } else { None }) | ||
737 | .map(|x| x + 2) | ||
738 | .len(); | ||
739 | } | ||
740 | "#, | ||
741 | )); | ||
742 | } | ||
743 | |||
744 | #[test] | ||
745 | fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() { | ||
746 | check_diagnostics(&add_filter_map_with_find_next_boilerplate( | ||
747 | r#" | ||
748 | fn foo() { | ||
749 | let m = [1, 2, 3] | ||
750 | .iter() | ||
751 | .filter_map(|x| if *x == 2 { Some (4) } else { None }); | ||
752 | let n = m.next(); | ||
753 | } | ||
754 | "#, | ||
755 | )); | ||
756 | } | ||
757 | |||
758 | #[test] | 674 | #[test] |
759 | fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() { | 675 | fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() { |
760 | check_diagnostics( | 676 | check_diagnostics( |
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 350575b3a..e4bd90c3f 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs | |||
@@ -1,7 +1,6 @@ | |||
1 | //! Provides a way to attach fixes to the diagnostics. | 1 | //! Provides a way to attach fixes to the diagnostics. |
2 | //! The same module also has all curret custom fixes for the diagnostics implemented. | 2 | //! The same module also has all curret custom fixes for the diagnostics implemented. |
3 | mod change_case; | 3 | mod change_case; |
4 | mod replace_with_find_map; | ||
5 | 4 | ||
6 | use hir::{diagnostics::Diagnostic, Semantics}; | 5 | use hir::{diagnostics::Diagnostic, Semantics}; |
7 | use ide_assists::AssistResolveStrategy; | 6 | use ide_assists::AssistResolveStrategy; |
diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs deleted file mode 100644 index 444bf563b..000000000 --- a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs +++ /dev/null | |||
@@ -1,84 +0,0 @@ | |||
1 | use hir::{db::AstDatabase, diagnostics::ReplaceFilterMapNextWithFindMap, Semantics}; | ||
2 | use ide_assists::{Assist, AssistResolveStrategy}; | ||
3 | use ide_db::{source_change::SourceChange, RootDatabase}; | ||
4 | use syntax::{ | ||
5 | ast::{self, ArgListOwner}, | ||
6 | AstNode, TextRange, | ||
7 | }; | ||
8 | use text_edit::TextEdit; | ||
9 | |||
10 | use crate::diagnostics::{fix, DiagnosticWithFixes}; | ||
11 | |||
12 | impl DiagnosticWithFixes for ReplaceFilterMapNextWithFindMap { | ||
13 | fn fixes( | ||
14 | &self, | ||
15 | sema: &Semantics<RootDatabase>, | ||
16 | _resolve: &AssistResolveStrategy, | ||
17 | ) -> Option<Vec<Assist>> { | ||
18 | let root = sema.db.parse_or_expand(self.file)?; | ||
19 | let next_expr = self.next_expr.to_node(&root); | ||
20 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | ||
21 | |||
22 | let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?; | ||
23 | let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range(); | ||
24 | let filter_map_args = filter_map_call.arg_list()?; | ||
25 | |||
26 | let range_to_replace = | ||
27 | TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end()); | ||
28 | let replacement = format!("find_map{}", filter_map_args.syntax().text()); | ||
29 | let trigger_range = next_expr.syntax().text_range(); | ||
30 | |||
31 | let edit = TextEdit::replace(range_to_replace, replacement); | ||
32 | |||
33 | let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); | ||
34 | |||
35 | Some(vec![fix( | ||
36 | "replace_with_find_map", | ||
37 | "Replace filter_map(..).next() with find_map()", | ||
38 | source_change, | ||
39 | trigger_range, | ||
40 | )]) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | #[cfg(test)] | ||
45 | mod tests { | ||
46 | use crate::diagnostics::tests::check_fix; | ||
47 | |||
48 | #[test] | ||
49 | fn replace_with_wind_map() { | ||
50 | check_fix( | ||
51 | r#" | ||
52 | //- /main.rs crate:main deps:core | ||
53 | use core::iter::Iterator; | ||
54 | use core::option::Option::{self, Some, None}; | ||
55 | fn foo() { | ||
56 | let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); | ||
57 | } | ||
58 | //- /core/lib.rs crate:core | ||
59 | pub mod option { | ||
60 | pub enum Option<T> { Some(T), None } | ||
61 | } | ||
62 | pub mod iter { | ||
63 | pub trait Iterator { | ||
64 | type Item; | ||
65 | fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap } | ||
66 | fn next(&mut self) -> Option<Self::Item>; | ||
67 | } | ||
68 | pub struct FilterMap {} | ||
69 | impl Iterator for FilterMap { | ||
70 | type Item = i32; | ||
71 | fn next(&mut self) -> i32 { 7 } | ||
72 | } | ||
73 | } | ||
74 | "#, | ||
75 | r#" | ||
76 | use core::iter::Iterator; | ||
77 | use core::option::Option::{self, Some, None}; | ||
78 | fn foo() { | ||
79 | let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); | ||
80 | } | ||
81 | "#, | ||
82 | ) | ||
83 | } | ||
84 | } | ||
diff --git a/crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs b/crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs new file mode 100644 index 000000000..f3b011495 --- /dev/null +++ b/crates/ide/src/diagnostics/replace_filter_map_next_with_find_map.rs | |||
@@ -0,0 +1,182 @@ | |||
1 | use hir::{db::AstDatabase, InFile}; | ||
2 | use ide_db::source_change::SourceChange; | ||
3 | use syntax::{ | ||
4 | ast::{self, ArgListOwner}, | ||
5 | AstNode, TextRange, | ||
6 | }; | ||
7 | use text_edit::TextEdit; | ||
8 | |||
9 | use crate::{ | ||
10 | diagnostics::{fix, Diagnostic, DiagnosticsContext}, | ||
11 | Assist, Severity, | ||
12 | }; | ||
13 | |||
14 | // Diagnostic: replace-filter-map-next-with-find-map | ||
15 | // | ||
16 | // This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`. | ||
17 | pub(super) fn replace_filter_map_next_with_find_map( | ||
18 | ctx: &DiagnosticsContext<'_>, | ||
19 | d: &hir::ReplaceFilterMapNextWithFindMap, | ||
20 | ) -> Diagnostic { | ||
21 | Diagnostic::new( | ||
22 | "replace-filter-map-next-with-find-map", | ||
23 | "replace filter_map(..).next() with find_map(..)", | ||
24 | ctx.sema.diagnostics_display_range(InFile::new(d.file, d.next_expr.clone().into())).range, | ||
25 | ) | ||
26 | .severity(Severity::WeakWarning) | ||
27 | .with_fixes(fixes(ctx, d)) | ||
28 | } | ||
29 | |||
30 | fn fixes( | ||
31 | ctx: &DiagnosticsContext<'_>, | ||
32 | d: &hir::ReplaceFilterMapNextWithFindMap, | ||
33 | ) -> Option<Vec<Assist>> { | ||
34 | let root = ctx.sema.db.parse_or_expand(d.file)?; | ||
35 | let next_expr = d.next_expr.to_node(&root); | ||
36 | let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?; | ||
37 | |||
38 | let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?; | ||
39 | let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range(); | ||
40 | let filter_map_args = filter_map_call.arg_list()?; | ||
41 | |||
42 | let range_to_replace = | ||
43 | TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end()); | ||
44 | let replacement = format!("find_map{}", filter_map_args.syntax().text()); | ||
45 | let trigger_range = next_expr.syntax().text_range(); | ||
46 | |||
47 | let edit = TextEdit::replace(range_to_replace, replacement); | ||
48 | |||
49 | let source_change = SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit); | ||
50 | |||
51 | Some(vec![fix( | ||
52 | "replace_with_find_map", | ||
53 | "Replace filter_map(..).next() with find_map()", | ||
54 | source_change, | ||
55 | trigger_range, | ||
56 | )]) | ||
57 | } | ||
58 | |||
59 | #[cfg(test)] | ||
60 | mod tests { | ||
61 | use crate::diagnostics::tests::check_fix; | ||
62 | |||
63 | // Register the required standard library types to make the tests work | ||
64 | #[track_caller] | ||
65 | fn check_diagnostics(ra_fixture: &str) { | ||
66 | let prefix = r#" | ||
67 | //- /main.rs crate:main deps:core | ||
68 | use core::iter::Iterator; | ||
69 | use core::option::Option::{self, Some, None}; | ||
70 | "#; | ||
71 | let suffix = r#" | ||
72 | //- /core/lib.rs crate:core | ||
73 | pub mod option { | ||
74 | pub enum Option<T> { Some(T), None } | ||
75 | } | ||
76 | pub mod iter { | ||
77 | pub trait Iterator { | ||
78 | type Item; | ||
79 | fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap } | ||
80 | fn next(&mut self) -> Option<Self::Item>; | ||
81 | } | ||
82 | pub struct FilterMap {} | ||
83 | impl Iterator for FilterMap { | ||
84 | type Item = i32; | ||
85 | fn next(&mut self) -> i32 { 7 } | ||
86 | } | ||
87 | } | ||
88 | "#; | ||
89 | crate::diagnostics::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix)) | ||
90 | } | ||
91 | |||
92 | #[test] | ||
93 | fn replace_filter_map_next_with_find_map2() { | ||
94 | check_diagnostics( | ||
95 | r#" | ||
96 | fn foo() { | ||
97 | let m = [1, 2, 3].iter().filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); | ||
98 | } //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ replace filter_map(..).next() with find_map(..) | ||
99 | "#, | ||
100 | ); | ||
101 | } | ||
102 | |||
103 | #[test] | ||
104 | fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() { | ||
105 | check_diagnostics( | ||
106 | r#" | ||
107 | fn foo() { | ||
108 | let m = [1, 2, 3] | ||
109 | .iter() | ||
110 | .filter_map(|x| if *x == 2 { Some (4) } else { None }) | ||
111 | .len(); | ||
112 | } | ||
113 | "#, | ||
114 | ); | ||
115 | } | ||
116 | |||
117 | #[test] | ||
118 | fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() { | ||
119 | check_diagnostics( | ||
120 | r#" | ||
121 | fn foo() { | ||
122 | let m = [1, 2, 3] | ||
123 | .iter() | ||
124 | .filter_map(|x| if *x == 2 { Some (4) } else { None }) | ||
125 | .map(|x| x + 2) | ||
126 | .len(); | ||
127 | } | ||
128 | "#, | ||
129 | ); | ||
130 | } | ||
131 | |||
132 | #[test] | ||
133 | fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() { | ||
134 | check_diagnostics( | ||
135 | r#" | ||
136 | fn foo() { | ||
137 | let m = [1, 2, 3] | ||
138 | .iter() | ||
139 | .filter_map(|x| if *x == 2 { Some (4) } else { None }); | ||
140 | let n = m.next(); | ||
141 | } | ||
142 | "#, | ||
143 | ); | ||
144 | } | ||
145 | |||
146 | #[test] | ||
147 | fn replace_with_wind_map() { | ||
148 | check_fix( | ||
149 | r#" | ||
150 | //- /main.rs crate:main deps:core | ||
151 | use core::iter::Iterator; | ||
152 | use core::option::Option::{self, Some, None}; | ||
153 | fn foo() { | ||
154 | let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); | ||
155 | } | ||
156 | //- /core/lib.rs crate:core | ||
157 | pub mod option { | ||
158 | pub enum Option<T> { Some(T), None } | ||
159 | } | ||
160 | pub mod iter { | ||
161 | pub trait Iterator { | ||
162 | type Item; | ||
163 | fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap } | ||
164 | fn next(&mut self) -> Option<Self::Item>; | ||
165 | } | ||
166 | pub struct FilterMap {} | ||
167 | impl Iterator for FilterMap { | ||
168 | type Item = i32; | ||
169 | fn next(&mut self) -> i32 { 7 } | ||
170 | } | ||
171 | } | ||
172 | "#, | ||
173 | r#" | ||
174 | use core::iter::Iterator; | ||
175 | use core::option::Option::{self, Some, None}; | ||
176 | fn foo() { | ||
177 | let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); | ||
178 | } | ||
179 | "#, | ||
180 | ) | ||
181 | } | ||
182 | } | ||