ra_ap_ide_diagnostics/handlers/
replace_filter_map_next_with_find_map.rs

1use hir::{InFile, db::ExpandDatabase};
2use ide_db::source_change::SourceChange;
3use ide_db::text_edit::TextEdit;
4use syntax::{
5    AstNode, TextRange,
6    ast::{self, HasArgList},
7};
8
9use crate::{Assist, Diagnostic, DiagnosticCode, DiagnosticsContext, fix};
10
11// Diagnostic: replace-filter-map-next-with-find-map
12//
13// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`.
14pub(crate) fn replace_filter_map_next_with_find_map(
15    ctx: &DiagnosticsContext<'_>,
16    d: &hir::ReplaceFilterMapNextWithFindMap,
17) -> Diagnostic {
18    Diagnostic::new_with_syntax_node_ptr(
19        ctx,
20        DiagnosticCode::Clippy("filter_map_next"),
21        "replace filter_map(..).next() with find_map(..)",
22        InFile::new(d.file, d.next_expr.into()),
23    )
24    .stable()
25    .with_fixes(fixes(ctx, d))
26}
27
28fn fixes(
29    ctx: &DiagnosticsContext<'_>,
30    d: &hir::ReplaceFilterMapNextWithFindMap,
31) -> Option<Vec<Assist>> {
32    let root = ctx.sema.db.parse_or_expand(d.file);
33    let next_expr = d.next_expr.to_node(&root);
34    let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
35
36    let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
37    let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
38    let filter_map_args = filter_map_call.arg_list()?;
39
40    let range_to_replace =
41        TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
42    let replacement = format!("find_map{}", filter_map_args.syntax().text());
43    let trigger_range = next_expr.syntax().text_range();
44
45    let edit = TextEdit::replace(range_to_replace, replacement);
46
47    let source_change =
48        SourceChange::from_text_edit(d.file.original_file(ctx.sema.db).file_id(ctx.sema.db), edit);
49
50    Some(vec![fix(
51        "replace_with_find_map",
52        "Replace filter_map(..).next() with find_map()",
53        source_change,
54        trigger_range,
55    )])
56}
57
58#[cfg(test)]
59mod tests {
60    use crate::{
61        DiagnosticsConfig,
62        tests::{check_diagnostics_with_config, check_fix},
63    };
64
65    #[track_caller]
66    pub(crate) fn check_diagnostics(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
67        let mut config = DiagnosticsConfig::test_sample();
68        config.disabled.insert("inactive-code".to_owned());
69        config.disabled.insert("E0599".to_owned());
70        check_diagnostics_with_config(config, ra_fixture)
71    }
72
73    #[test]
74    fn replace_filter_map_next_with_find_map2() {
75        check_diagnostics(
76            r#"
77//- minicore: iterators
78fn foo() {
79    let _m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
80}          //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 weak: replace filter_map(..).next() with find_map(..)
81"#,
82        );
83    }
84
85    #[test]
86    fn replace_filter_map_next_dont_work_for_not_sized_issues_16596() {
87        check_diagnostics(
88            r#"
89//- minicore: iterators, dispatch_from_dyn
90fn foo() {
91    let mut j = [0].into_iter();
92    let i: &mut dyn Iterator<Item = i32>  = &mut j;
93    let dummy_fn = |v| (v > 0).then_some(v + 1);
94    let _res = i.filter_map(dummy_fn).next();
95}
96"#,
97        );
98    }
99
100    #[test]
101    fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() {
102        check_diagnostics(
103            r#"
104//- minicore: iterators
105fn foo() {
106    let m = core::iter::repeat(())
107        .filter_map(|()| Some(92))
108        .count();
109}
110"#,
111        );
112    }
113
114    #[test]
115    fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() {
116        check_diagnostics(
117            r#"
118//- minicore: iterators
119fn foo() {
120    let m = core::iter::repeat(())
121        .filter_map(|()| Some(92))
122        .map(|x| x + 2)
123        .next();
124}
125"#,
126        );
127    }
128
129    #[test]
130    fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() {
131        check_diagnostics(
132            r#"
133//- minicore: iterators
134fn foo() {
135    let mut m = core::iter::repeat(())
136        .filter_map(|()| Some(92));
137    let _n = m.next();
138}
139"#,
140        );
141    }
142
143    #[test]
144    fn replace_with_find_map() {
145        check_fix(
146            r#"
147//- minicore: iterators
148fn foo() {
149    let m = core::iter::repeat(()).$0filter_map(|()| Some(92)).next();
150}
151"#,
152            r#"
153fn foo() {
154    let m = core::iter::repeat(()).find_map(|()| Some(92));
155}
156"#,
157        )
158    }
159
160    #[test]
161    fn respect_lint_attributes_for_clippy_equivalent() {
162        check_diagnostics(
163            r#"
164//- minicore: iterators
165
166fn foo() {
167    #[allow(clippy::filter_map_next)]
168    let _m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
169}
170
171#[deny(clippy::filter_map_next)]
172fn foo() {
173    let _m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
174}          //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 error: replace filter_map(..).next() with find_map(..)
175
176fn foo() {
177    let _m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
178}          //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 weak: replace filter_map(..).next() with find_map(..)
179
180#[warn(clippy::filter_map_next)]
181fn foo() {
182    let _m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
183}          //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 warn: replace filter_map(..).next() with find_map(..)
184
185"#,
186        );
187    }
188}