Skip to content

Commit

Permalink
# This is a combination of 6 commits.
Browse files Browse the repository at this point in the history
# This is the 1st commit message:

Split filter_map into manual_filter_map

# The commit message rust-lang#2 will be skipped:

# fixup! Split filter_map into manual_filter_map

# The commit message rust-lang#3 will be skipped:

# fixup! Split filter_map into manual_filter_map

# The commit message rust-lang#4 will be skipped:

# fixup! Split filter_map into manual_filter_map

# The commit message rust-lang#5 will be skipped:

# fixup! Split filter_map into manual_filter_map

# The commit message rust-lang#6 will be skipped:

# fixup! Split filter_map into manual_filter_map
  • Loading branch information
camsteffen committed Jan 15, 2021
1 parent d93889a commit aaf6bcf
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2033,6 +2033,7 @@ Released 2018-09-13
[`macro_use_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#macro_use_imports
[`main_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#main_recursion
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
Expand Down
3 changes: 3 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
&methods::ITER_NTH,
&methods::ITER_NTH_ZERO,
&methods::ITER_SKIP_NEXT,
&methods::MANUAL_FILTER_MAP,
&methods::MANUAL_SATURATING_ARITHMETIC,
&methods::MAP_COLLECT_RESULT_UNIT,
&methods::MAP_FLATTEN,
Expand Down Expand Up @@ -1514,6 +1515,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&methods::ITER_NTH),
LintId::of(&methods::ITER_NTH_ZERO),
LintId::of(&methods::ITER_SKIP_NEXT),
LintId::of(&methods::MANUAL_FILTER_MAP),
LintId::of(&methods::MANUAL_SATURATING_ARITHMETIC),
LintId::of(&methods::MAP_COLLECT_RESULT_UNIT),
LintId::of(&methods::NEW_RET_NO_SELF),
Expand Down Expand Up @@ -1807,6 +1809,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&methods::CLONE_ON_COPY),
LintId::of(&methods::FILTER_NEXT),
LintId::of(&methods::FLAT_MAP_IDENTITY),
LintId::of(&methods::MANUAL_FILTER_MAP),
LintId::of(&methods::OPTION_AS_REF_DEREF),
LintId::of(&methods::SEARCH_IS_SOME),
LintId::of(&methods::SKIP_WHILE_NEXT),
Expand Down
102 changes: 94 additions & 8 deletions clippy_lints/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use if_chain::if_chain;
use rustc_ast::ast;
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_hir::{TraitItem, TraitItemKind};
use rustc_hir::def::Res;
use rustc_hir::{Expr, ExprKind, PatKind, QPath, TraitItem, TraitItemKind, UnOp};
use rustc_lint::{LateContext, LateLintPass, Lint, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_middle::ty::{self, TraitRef, Ty, TyS};
Expand Down Expand Up @@ -449,6 +450,32 @@ declare_clippy_lint! {
"using combinations of `filter`, `map`, `filter_map` and `flat_map` which can usually be written as a single method call"
}

declare_clippy_lint! {
/// **What it does:** Checks for usage of `_.filter(_).map(_)` that can be written more simply
/// as `filter_map(_)`.
///
/// **Why is this bad?** Redundant code in the `filter` and `map` operations is poor style and
/// less performant.
///
/// **Known problems:** None.
///
/// **Example:**
/// Bad:
/// ```rust
/// (0..10)
/// .filter(|n| n.checked_add(1).is_some())
/// .map(|n| n.checked_add(1).unwrap());
/// ```
///
/// Good:
/// ```rust
/// (0..10).filter_map(|n| n.checked_add(1));
/// ```
pub MANUAL_FILTER_MAP,
complexity,
"using `_.filter(_).map(_)` in a way that can be written more simply as `filter_map(_)`"
}

declare_clippy_lint! {
/// **What it does:** Checks for usage of `_.filter_map(_).next()`.
///
Expand Down Expand Up @@ -1442,6 +1469,7 @@ impl_lint_pass!(Methods => [
FILTER_NEXT,
SKIP_WHILE_NEXT,
FILTER_MAP,
MANUAL_FILTER_MAP,
FILTER_MAP_NEXT,
FLAT_MAP_IDENTITY,
FIND_MAP,
Expand Down Expand Up @@ -2958,14 +2986,72 @@ fn lint_skip_while_next<'tcx>(
fn lint_filter_map<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx hir::Expr<'_>,
_filter_args: &'tcx [hir::Expr<'_>],
_map_args: &'tcx [hir::Expr<'_>],
filter_args: &'tcx [hir::Expr<'_>],
map_args: &'tcx [hir::Expr<'_>],
) {
// lint if caller of `.filter().map()` is an Iterator
if match_trait_method(cx, expr, &paths::ITERATOR) {
let msg = "called `filter(..).map(..)` on an `Iterator`";
let hint = "this is more succinctly expressed by calling `.filter_map(..)` instead";
span_lint_and_help(cx, FILTER_MAP, expr.span, msg, None, hint);
if_chain! {
if match_trait_method(cx, expr, &paths::ITERATOR);
if let [_, filter_arg] = filter_args;

// filter(|x| ...is_some())...
if let ExprKind::Closure(_, _, filter_body_id, ..) = filter_arg.kind;
let filter_body = cx.tcx.hir().body(filter_body_id);
if let [filter_param] = filter_body.params;
// optional ref pattern: `filter(|&x| ..)`
let (filter_pat, is_filter_param_ref) = if let PatKind::Ref(ref_pat, _) = filter_param.pat.kind {
(ref_pat, true)
} else {
(filter_param.pat, false)
};
// closure ends with is_some() or is_ok()
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
if let ExprKind::MethodCall(_, _, [filter_arg], filter_span) = filter_body.value.kind;
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(filter_body.value.hir_id);
if let Some(is_result) = if match_def_path(cx, def_id, &paths::OPTION_IS_SOME) {
Some(false)
} else if match_def_path(cx, def_id, &paths::RESULT_IS_OK) {
Some(true)
} else {
None
};

// ...map(|x| ...unwrap())
if let [_, map_arg] = map_args;
if let ExprKind::Closure(_, _, map_body_id, ..) = map_arg.kind;
let map_body = cx.tcx.hir().body(map_body_id);
if let [map_param] = map_body.params;
if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind;
// closure ends with expect() or unwrap()
if let ExprKind::MethodCall(seg, _, [map_arg, ..], map_span) = map_body.value.kind;
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);

let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
// in `filter(|x| ..)`, replace `*x` with `x`
let a_path = if_chain! {
if !is_filter_param_ref;
if let ExprKind::Unary(UnOp::UnDeref, expr_path) = a.kind;
then { expr_path } else { a }
};
// let the filter closure arg and the map closure arg be equal
if_chain! {
if let ExprKind::Path(QPath::Resolved(None, a_path)) = a_path.kind;
if let ExprKind::Path(QPath::Resolved(None, b_path)) = b.kind;
if a_path.res == Res::Local(filter_param_id);
if b_path.res == Res::Local(map_param_id);
if TyS::same_type(cx.typeck_results().expr_ty_adjusted(a), cx.typeck_results().expr_ty_adjusted(b));
then {
return true;
}
}
false
};
if SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg);
then {
let msg = "`filter(..).map(..)` can be simplified as `filter_map(..)`";
let to_opt = if is_result { ".ok()" } else { "" };
let sugg = format!(".filter_map(|{}| {}{})", map_param_ident, snippet(cx, map_arg.span, ".."), to_opt);
span_lint_and_sugg(cx, MANUAL_FILTER_MAP, span, msg, "try", sugg, Applicability::MachineApplicable);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/utils/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub const MUTEX_GUARD: [&str; 4] = ["std", "sync", "mutex", "MutexGuard"];
pub const OPEN_OPTIONS: [&str; 3] = ["std", "fs", "OpenOptions"];
pub const OPS_MODULE: [&str; 2] = ["core", "ops"];
pub const OPTION: [&str; 3] = ["core", "option", "Option"];
pub const OPTION_IS_SOME: [&str; 4] = ["core", "option", "Option", "is_some"];
pub const OPTION_NONE: [&str; 4] = ["core", "option", "Option", "None"];
pub const OPTION_SOME: [&str; 4] = ["core", "option", "Option", "Some"];
pub const ORD: [&str; 3] = ["core", "cmp", "Ord"];
Expand Down Expand Up @@ -129,6 +130,7 @@ pub const REGEX_SET_NEW: [&str; 5] = ["regex", "re_set", "unicode", "RegexSet",
pub const REPEAT: [&str; 3] = ["core", "iter", "repeat"];
pub const RESULT: [&str; 3] = ["core", "result", "Result"];
pub const RESULT_ERR: [&str; 4] = ["core", "result", "Result", "Err"];
pub const RESULT_IS_OK: [&str; 4] = ["core", "result", "Result", "is_ok"];
pub const RESULT_OK: [&str; 4] = ["core", "result", "Result", "Ok"];
pub const RWLOCK_READ_GUARD: [&str; 4] = ["std", "sync", "rwlock", "RwLockReadGuard"];
pub const RWLOCK_WRITE_GUARD: [&str; 4] = ["std", "sync", "rwlock", "RwLockWriteGuard"];
Expand Down
12 changes: 2 additions & 10 deletions tests/ui/filter_methods.stderr
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
error: called `filter(..).map(..)` on an `Iterator`
--> $DIR/filter_methods.rs:6:21
|
LL | let _: Vec<_> = vec![5; 6].into_iter().filter(|&x| x == 0).map(|x| x * 2).collect();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D clippy::filter-map` implied by `-D warnings`
= help: this is more succinctly expressed by calling `.filter_map(..)` instead

error: called `filter(..).flat_map(..)` on an `Iterator`
--> $DIR/filter_methods.rs:8:21
|
Expand All @@ -17,6 +8,7 @@ LL | | .filter(|&x| x == 0)
LL | | .flat_map(|x| x.checked_mul(2))
| |_______________________________________^
|
= note: `-D clippy::filter-map` implied by `-D warnings`
= help: this is more succinctly expressed by calling `.flat_map(..)` and filtering by returning `iter::empty()`

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

error: aborting due to 4 previous errors
error: aborting due to 3 previous errors

43 changes: 43 additions & 0 deletions tests/ui/manual_filter_map.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// run-rustfix
#![allow(dead_code)]
#![warn(clippy::manual_filter_map)]
#![allow(clippy::redundant_closure)] // FIXME suggestion may have redundant closure

use std::iter;

fn main() {
// is_some(), unwrap()
let _ = iter::once(1).filter_map(|a| to_opt(a));

// ref pattern, expect()
let _ = iter::once(1).filter_map(|a| to_opt(a));

// is_ok(), unwrap_or()
let _ = iter::once(1).filter_map(|a| to_res(a).ok());
}

fn no_lint() {
// no shared code
let _ = iter::once(1).filter(|n| *n > 1).map(|n| n + 1);

// very close but different since filter() provides a reference
let _ = iter::once(1)
.filter(|n| to_opt(n).is_some())
.map(|a| to_opt(a).unwrap());

// similar but different
let _ = iter::once(1)
.filter(|n| to_opt(n).is_some())
.map(|n| to_res(n).unwrap());
let _ = iter::once(1)
.filter(|n| to_opt(n).map(|n| n + 1).is_some())
.map(|a| to_opt(a).unwrap());
}

fn to_opt<T>(_: T) -> Option<T> {
unimplemented!()
}

fn to_res<T>(_: T) -> Result<T, ()> {
unimplemented!()
}
49 changes: 49 additions & 0 deletions tests/ui/manual_filter_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// run-rustfix
#![allow(dead_code)]
#![warn(clippy::manual_filter_map)]
#![allow(clippy::redundant_closure)] // FIXME suggestion may have redundant closure

use std::iter;

fn main() {
// is_some(), unwrap()
let _ = iter::once(1)
.filter(|n| to_opt(*n).is_some())
.map(|a| to_opt(a).unwrap());

// ref pattern, expect()
let _ = iter::once(1)
.filter(|&n| to_opt(n).is_some())
.map(|a| to_opt(a).expect("hi"));

// is_ok(), unwrap_or()
let _ = iter::once(1)
.filter(|&n| to_res(n).is_ok())
.map(|a| to_res(a).unwrap_or(1));
}

fn no_lint() {
// no shared code
let _ = iter::once(1).filter(|n| *n > 1).map(|n| n + 1);

// very close but different since filter() provides a reference
let _ = iter::once(1)
.filter(|n| to_opt(n).is_some())
.map(|a| to_opt(a).unwrap());

// similar but different
let _ = iter::once(1)
.filter(|n| to_opt(n).is_some())
.map(|n| to_res(n).unwrap());
let _ = iter::once(1)
.filter(|n| to_opt(n).map(|n| n + 1).is_some())
.map(|a| to_opt(a).unwrap());
}

fn to_opt<T>(_: T) -> Option<T> {
unimplemented!()
}

fn to_res<T>(_: T) -> Result<T, ()> {
unimplemented!()
}
31 changes: 31 additions & 0 deletions tests/ui/manual_filter_map.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
error: `filter(..).map(..)` can be simplified as `filter_map(..)`
--> $DIR/manual_filter_map.rs:10:26
|
LL | let _ = iter::once(1)
| __________________________^
LL | | .filter(|n| to_opt(*n).is_some())
LL | | .map(|a| to_opt(a).unwrap());
| |____________________________________^ help: try: `.filter_map(|a| to_opt(a))`
|
= note: `-D clippy::manual-filter-map` implied by `-D warnings`

error: `filter(..).map(..)` can be simplified as `filter_map(..)`
--> $DIR/manual_filter_map.rs:15:26
|
LL | let _ = iter::once(1)
| __________________________^
LL | | .filter(|&n| to_opt(n).is_some())
LL | | .map(|a| to_opt(a).expect("hi"));
| |________________________________________^ help: try: `.filter_map(|a| to_opt(a))`

error: `filter(..).map(..)` can be simplified as `filter_map(..)`
--> $DIR/manual_filter_map.rs:20:26
|
LL | let _ = iter::once(1)
| __________________________^
LL | | .filter(|&n| to_res(n).is_ok())
LL | | .map(|a| to_res(a).unwrap_or(1));
| |________________________________________^ help: try: `.filter_map(|a| to_res(a).ok())`

error: aborting due to 3 previous errors

0 comments on commit aaf6bcf

Please sign in to comment.