Skip to content

Commit c92bdc4

Browse files
committed
Split filter_map into manual_filter_map
1 parent 7a86608 commit c92bdc4

File tree

7 files changed

+198
-23
lines changed

7 files changed

+198
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,7 @@ Released 2018-09-13
20352035
[`macro_use_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#macro_use_imports
20362036
[`main_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#main_recursion
20372037
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
2038+
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
20382039
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
20392040
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
20402041
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
745745
&methods::ITER_NTH,
746746
&methods::ITER_NTH_ZERO,
747747
&methods::ITER_SKIP_NEXT,
748+
&methods::MANUAL_FILTER_MAP,
748749
&methods::MANUAL_SATURATING_ARITHMETIC,
749750
&methods::MAP_COLLECT_RESULT_UNIT,
750751
&methods::MAP_FLATTEN,
@@ -1526,6 +1527,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
15261527
LintId::of(&methods::ITER_NTH),
15271528
LintId::of(&methods::ITER_NTH_ZERO),
15281529
LintId::of(&methods::ITER_SKIP_NEXT),
1530+
LintId::of(&methods::MANUAL_FILTER_MAP),
15291531
LintId::of(&methods::MANUAL_SATURATING_ARITHMETIC),
15301532
LintId::of(&methods::MAP_COLLECT_RESULT_UNIT),
15311533
LintId::of(&methods::NEW_RET_NO_SELF),
@@ -1823,6 +1825,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
18231825
LintId::of(&methods::FILTER_NEXT),
18241826
LintId::of(&methods::FLAT_MAP_IDENTITY),
18251827
LintId::of(&methods::INSPECT_FOR_EACH),
1828+
LintId::of(&methods::MANUAL_FILTER_MAP),
18261829
LintId::of(&methods::OPTION_AS_REF_DEREF),
18271830
LintId::of(&methods::SEARCH_IS_SOME),
18281831
LintId::of(&methods::SKIP_WHILE_NEXT),

clippy_lints/src/methods/mod.rs

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use if_chain::if_chain;
1515
use rustc_ast::ast;
1616
use rustc_errors::Applicability;
1717
use rustc_hir as hir;
18-
use rustc_hir::{TraitItem, TraitItemKind};
18+
use rustc_hir::def::Res;
19+
use rustc_hir::{Expr, ExprKind, PatKind, QPath, TraitItem, TraitItemKind, UnOp};
1920
use rustc_lint::{LateContext, LateLintPass, Lint, LintContext};
2021
use rustc_middle::lint::in_external_macro;
2122
use rustc_middle::ty::{self, TraitRef, Ty, TyS};
@@ -450,6 +451,32 @@ declare_clippy_lint! {
450451
"using combinations of `filter`, `map`, `filter_map` and `flat_map` which can usually be written as a single method call"
451452
}
452453

454+
declare_clippy_lint! {
455+
/// **What it does:** Checks for usage of `_.filter(_).map(_)` that can be written more simply
456+
/// as `filter_map(_)`.
457+
///
458+
/// **Why is this bad?** Redundant code in the `filter` and `map` operations is poor style and
459+
/// less performant.
460+
///
461+
/// **Known problems:** None.
462+
///
463+
/// **Example:**
464+
/// Bad:
465+
/// ```rust
466+
/// (0_i32..10)
467+
/// .filter(|n| n.checked_add(1).is_some())
468+
/// .map(|n| n.checked_add(1).unwrap());
469+
/// ```
470+
///
471+
/// Good:
472+
/// ```rust
473+
/// (0_i32..10).filter_map(|n| n.checked_add(1));
474+
/// ```
475+
pub MANUAL_FILTER_MAP,
476+
complexity,
477+
"using `_.filter(_).map(_)` in a way that can be written more simply as `filter_map(_)`"
478+
}
479+
453480
declare_clippy_lint! {
454481
/// **What it does:** Checks for usage of `_.filter_map(_).next()`.
455482
///
@@ -1473,6 +1500,7 @@ impl_lint_pass!(Methods => [
14731500
FILTER_NEXT,
14741501
SKIP_WHILE_NEXT,
14751502
FILTER_MAP,
1503+
MANUAL_FILTER_MAP,
14761504
FILTER_MAP_NEXT,
14771505
FLAT_MAP_IDENTITY,
14781506
FIND_MAP,
@@ -1540,7 +1568,7 @@ impl<'tcx> LateLintPass<'tcx> for Methods {
15401568
["next", "filter"] => lint_filter_next(cx, expr, arg_lists[1]),
15411569
["next", "skip_while"] => lint_skip_while_next(cx, expr, arg_lists[1]),
15421570
["next", "iter"] => lint_iter_next(cx, expr, arg_lists[1]),
1543-
["map", "filter"] => lint_filter_map(cx, expr, arg_lists[1], arg_lists[0]),
1571+
["map", "filter"] => lint_filter_map(cx, expr),
15441572
["map", "filter_map"] => lint_filter_map_map(cx, expr, arg_lists[1], arg_lists[0]),
15451573
["next", "filter_map"] => lint_filter_map_next(cx, expr, arg_lists[1], self.msrv.as_ref()),
15461574
["map", "find"] => lint_find_map(cx, expr, arg_lists[1], arg_lists[0]),
@@ -2989,17 +3017,72 @@ fn lint_skip_while_next<'tcx>(
29893017
}
29903018

29913019
/// lint use of `filter().map()` for `Iterators`
2992-
fn lint_filter_map<'tcx>(
2993-
cx: &LateContext<'tcx>,
2994-
expr: &'tcx hir::Expr<'_>,
2995-
_filter_args: &'tcx [hir::Expr<'_>],
2996-
_map_args: &'tcx [hir::Expr<'_>],
2997-
) {
2998-
// lint if caller of `.filter().map()` is an Iterator
2999-
if match_trait_method(cx, expr, &paths::ITERATOR) {
3000-
let msg = "called `filter(..).map(..)` on an `Iterator`";
3001-
let hint = "this is more succinctly expressed by calling `.filter_map(..)` instead";
3002-
span_lint_and_help(cx, FILTER_MAP, expr.span, msg, None, hint);
3020+
fn lint_filter_map<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
3021+
if_chain! {
3022+
if let ExprKind::MethodCall(_, _, [map_recv, map_arg], map_span) = expr.kind;
3023+
if let ExprKind::MethodCall(_, _, [_, filter_arg], filter_span) = map_recv.kind;
3024+
if match_trait_method(cx, expr, &paths::ITERATOR);
3025+
3026+
// filter(|x| ...is_some())...
3027+
if let ExprKind::Closure(_, _, filter_body_id, ..) = filter_arg.kind;
3028+
let filter_body = cx.tcx.hir().body(filter_body_id);
3029+
if let [filter_param] = filter_body.params;
3030+
// optional ref pattern: `filter(|&x| ..)`
3031+
let (filter_pat, is_filter_param_ref) = if let PatKind::Ref(ref_pat, _) = filter_param.pat.kind {
3032+
(ref_pat, true)
3033+
} else {
3034+
(filter_param.pat, false)
3035+
};
3036+
// closure ends with is_some() or is_ok()
3037+
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
3038+
if let ExprKind::MethodCall(path, _, [filter_arg], _) = filter_body.value.kind;
3039+
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).ty_adt_def();
3040+
if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::option_type, opt_ty.did) {
3041+
Some(false)
3042+
} else if cx.tcx.is_diagnostic_item(sym::result_type, opt_ty.did) {
3043+
Some(true)
3044+
} else {
3045+
None
3046+
};
3047+
if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" };
3048+
3049+
// ...map(|x| ...unwrap())
3050+
if let ExprKind::Closure(_, _, map_body_id, ..) = map_arg.kind;
3051+
let map_body = cx.tcx.hir().body(map_body_id);
3052+
if let [map_param] = map_body.params;
3053+
if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind;
3054+
// closure ends with expect() or unwrap()
3055+
if let ExprKind::MethodCall(seg, _, [map_arg, ..], _) = map_body.value.kind;
3056+
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);
3057+
3058+
let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
3059+
// in `filter(|x| ..)`, replace `*x` with `x`
3060+
let a_path = if_chain! {
3061+
if !is_filter_param_ref;
3062+
if let ExprKind::Unary(UnOp::UnDeref, expr_path) = a.kind;
3063+
then { expr_path } else { a }
3064+
};
3065+
// let the filter closure arg and the map closure arg be equal
3066+
if_chain! {
3067+
if let ExprKind::Path(QPath::Resolved(None, a_path)) = a_path.kind;
3068+
if let ExprKind::Path(QPath::Resolved(None, b_path)) = b.kind;
3069+
if a_path.res == Res::Local(filter_param_id);
3070+
if b_path.res == Res::Local(map_param_id);
3071+
if TyS::same_type(cx.typeck_results().expr_ty_adjusted(a), cx.typeck_results().expr_ty_adjusted(b));
3072+
then {
3073+
return true;
3074+
}
3075+
}
3076+
false
3077+
};
3078+
if SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg);
3079+
then {
3080+
let span = filter_span.to(map_span);
3081+
let msg = "`filter(..).map(..)` can be simplified as `filter_map(..)`";
3082+
let to_opt = if is_result { ".ok()" } else { "" };
3083+
let sugg = format!("filter_map(|{}| {}{})", map_param_ident, snippet(cx, map_arg.span, ".."), to_opt);
3084+
span_lint_and_sugg(cx, MANUAL_FILTER_MAP, span, msg, "try", sugg, Applicability::MachineApplicable);
3085+
}
30033086
}
30043087
}
30053088

tests/ui/filter_methods.stderr

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
error: called `filter(..).map(..)` on an `Iterator`
2-
--> $DIR/filter_methods.rs:6:21
3-
|
4-
LL | let _: Vec<_> = vec![5; 6].into_iter().filter(|&x| x == 0).map(|x| x * 2).collect();
5-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6-
|
7-
= note: `-D clippy::filter-map` implied by `-D warnings`
8-
= help: this is more succinctly expressed by calling `.filter_map(..)` instead
9-
101
error: called `filter(..).flat_map(..)` on an `Iterator`
112
--> $DIR/filter_methods.rs:8:21
123
|
@@ -17,6 +8,7 @@ LL | | .filter(|&x| x == 0)
178
LL | | .flat_map(|x| x.checked_mul(2))
189
| |_______________________________________^
1910
|
11+
= note: `-D clippy::filter-map` implied by `-D warnings`
2012
= help: this is more succinctly expressed by calling `.flat_map(..)` and filtering by returning `iter::empty()`
2113

2214
error: called `filter_map(..).flat_map(..)` on an `Iterator`
@@ -43,5 +35,5 @@ LL | | .map(|x| x.checked_mul(2))
4335
|
4436
= help: this is more succinctly expressed by only calling `.filter_map(..)` instead
4537

46-
error: aborting due to 4 previous errors
38+
error: aborting due to 3 previous errors
4739

tests/ui/manual_filter_map.fixed

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// run-rustfix
2+
#![allow(dead_code)]
3+
#![warn(clippy::manual_filter_map)]
4+
#![allow(clippy::redundant_closure)] // FIXME suggestion may have redundant closure
5+
6+
fn main() {
7+
// is_some(), unwrap()
8+
let _ = (0..).filter_map(|a| to_opt(a));
9+
10+
// ref pattern, expect()
11+
let _ = (0..).filter_map(|a| to_opt(a));
12+
13+
// is_ok(), unwrap_or()
14+
let _ = (0..).filter_map(|a| to_res(a).ok());
15+
}
16+
17+
fn no_lint() {
18+
// no shared code
19+
let _ = (0..).filter(|n| *n > 1).map(|n| n + 1);
20+
21+
// very close but different since filter() provides a reference
22+
let _ = (0..).filter(|n| to_opt(n).is_some()).map(|a| to_opt(a).unwrap());
23+
24+
// similar but different
25+
let _ = (0..).filter(|n| to_opt(n).is_some()).map(|n| to_res(n).unwrap());
26+
let _ = (0..)
27+
.filter(|n| to_opt(n).map(|n| n + 1).is_some())
28+
.map(|a| to_opt(a).unwrap());
29+
}
30+
31+
fn to_opt<T>(_: T) -> Option<T> {
32+
unimplemented!()
33+
}
34+
35+
fn to_res<T>(_: T) -> Result<T, ()> {
36+
unimplemented!()
37+
}

tests/ui/manual_filter_map.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// run-rustfix
2+
#![allow(dead_code)]
3+
#![warn(clippy::manual_filter_map)]
4+
#![allow(clippy::redundant_closure)] // FIXME suggestion may have redundant closure
5+
6+
fn main() {
7+
// is_some(), unwrap()
8+
let _ = (0..).filter(|n| to_opt(*n).is_some()).map(|a| to_opt(a).unwrap());
9+
10+
// ref pattern, expect()
11+
let _ = (0..).filter(|&n| to_opt(n).is_some()).map(|a| to_opt(a).expect("hi"));
12+
13+
// is_ok(), unwrap_or()
14+
let _ = (0..).filter(|&n| to_res(n).is_ok()).map(|a| to_res(a).unwrap_or(1));
15+
}
16+
17+
fn no_lint() {
18+
// no shared code
19+
let _ = (0..).filter(|n| *n > 1).map(|n| n + 1);
20+
21+
// very close but different since filter() provides a reference
22+
let _ = (0..).filter(|n| to_opt(n).is_some()).map(|a| to_opt(a).unwrap());
23+
24+
// similar but different
25+
let _ = (0..).filter(|n| to_opt(n).is_some()).map(|n| to_res(n).unwrap());
26+
let _ = (0..)
27+
.filter(|n| to_opt(n).map(|n| n + 1).is_some())
28+
.map(|a| to_opt(a).unwrap());
29+
}
30+
31+
fn to_opt<T>(_: T) -> Option<T> {
32+
unimplemented!()
33+
}
34+
35+
fn to_res<T>(_: T) -> Result<T, ()> {
36+
unimplemented!()
37+
}

tests/ui/manual_filter_map.stderr

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: `filter(..).map(..)` can be simplified as `filter_map(..)`
2+
--> $DIR/manual_filter_map.rs:8:19
3+
|
4+
LL | let _ = (0..).filter(|n| to_opt(*n).is_some()).map(|a| to_opt(a).unwrap());
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `filter_map(|a| to_opt(a))`
6+
|
7+
= note: `-D clippy::manual-filter-map` implied by `-D warnings`
8+
9+
error: `filter(..).map(..)` can be simplified as `filter_map(..)`
10+
--> $DIR/manual_filter_map.rs:11:19
11+
|
12+
LL | let _ = (0..).filter(|&n| to_opt(n).is_some()).map(|a| to_opt(a).expect("hi"));
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `filter_map(|a| to_opt(a))`
14+
15+
error: `filter(..).map(..)` can be simplified as `filter_map(..)`
16+
--> $DIR/manual_filter_map.rs:14:19
17+
|
18+
LL | let _ = (0..).filter(|&n| to_res(n).is_ok()).map(|a| to_res(a).unwrap_or(1));
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `filter_map(|a| to_res(a).ok())`
20+
21+
error: aborting due to 3 previous errors
22+

0 commit comments

Comments
 (0)