Auto merge of #11869 - PartiallyTyped:result-filter-map, r=Alexendoo

New Lint: `result_filter_map` / Mirror of `option_filter_map`

Added the `Result` mirror of `option_filter_map`.

changelog: New Lint: [`result_filter_map`]

I had to move around some code because the function def was too long 🙃.

I have also added some pattern checks on `option_filter_map`
This commit is contained in:
bors 2023-12-16 23:29:07 +00:00
commit f9b5def2ae
10 changed files with 217 additions and 31 deletions

View file

@ -419,6 +419,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::methods::READ_LINE_WITHOUT_TRIM_INFO,
crate::methods::REDUNDANT_AS_STR_INFO,
crate::methods::REPEAT_ONCE_INFO,
crate::methods::RESULT_FILTER_MAP_INFO,
crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO,
crate::methods::SEARCH_IS_SOME_INFO,
crate::methods::SEEK_FROM_CURRENT_INFO,

View file

@ -14,7 +14,7 @@ use rustc_span::symbol::{sym, Ident, Symbol};
use rustc_span::Span;
use std::borrow::Cow;
use super::{MANUAL_FILTER_MAP, MANUAL_FIND_MAP, OPTION_FILTER_MAP};
use super::{MANUAL_FILTER_MAP, MANUAL_FIND_MAP, OPTION_FILTER_MAP, RESULT_FILTER_MAP};
fn is_method(cx: &LateContext<'_>, expr: &hir::Expr<'_>, method_name: Symbol) -> bool {
match &expr.kind {
@ -46,6 +46,9 @@ fn is_method(cx: &LateContext<'_>, expr: &hir::Expr<'_>, method_name: Symbol) ->
fn is_option_filter_map(cx: &LateContext<'_>, filter_arg: &hir::Expr<'_>, map_arg: &hir::Expr<'_>) -> bool {
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some))
}
fn is_ok_filter_map(cx: &LateContext<'_>, filter_arg: &hir::Expr<'_>, map_arg: &hir::Expr<'_>) -> bool {
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_ok))
}
#[derive(Debug, Copy, Clone)]
enum OffendingFilterExpr<'tcx> {
@ -273,6 +276,18 @@ fn is_filter_some_map_unwrap(
(iterator || option) && is_option_filter_map(cx, filter_arg, map_arg)
}
/// is `filter(|x| x.is_ok()).map(|x| x.unwrap())`
fn is_filter_ok_map_unwrap(
cx: &LateContext<'_>,
expr: &hir::Expr<'_>,
filter_arg: &hir::Expr<'_>,
map_arg: &hir::Expr<'_>,
) -> bool {
// result has no filter, so we only check for iterators
let iterator = is_trait_method(cx, expr, sym::Iterator);
iterator && is_ok_filter_map(cx, filter_arg, map_arg)
}
/// lint use of `filter().map()` or `find().map()` for `Iterators`
#[allow(clippy::too_many_arguments)]
pub(super) fn check(
@ -300,30 +315,21 @@ pub(super) fn check(
return;
}
if is_trait_method(cx, map_recv, sym::Iterator)
if is_filter_ok_map_unwrap(cx, expr, filter_arg, map_arg) {
span_lint_and_sugg(
cx,
RESULT_FILTER_MAP,
filter_span.with_hi(expr.span.hi()),
"`filter` for `Ok` followed by `unwrap`",
"consider using `flatten` instead",
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, map_span)).into_owned(),
Applicability::MachineApplicable,
);
// filter(|x| ...is_some())...
&& let ExprKind::Closure(&Closure { body: filter_body_id, .. }) = filter_arg.kind
&& let filter_body = cx.tcx.hir().body(filter_body_id)
&& 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)
}
return;
}
&& let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind
&& let Some(mut offending_expr) = OffendingFilterExpr::hir(cx, filter_body.value, filter_param_id)
&& let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind
&& let map_body = cx.tcx.hir().body(map_body_id)
&& let [map_param] = map_body.params
&& let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind
&& let Some(check_result) =
offending_expr.check_map_call(cx, map_body, map_param_id, filter_param_id, is_filter_param_ref)
{
if let Some((map_param_ident, check_result)) = is_find_or_filter(cx, map_recv, filter_arg, map_arg) {
let span = filter_span.with_hi(expr.span.hi());
let (filter_name, lint) = if is_find {
("find", MANUAL_FIND_MAP)
@ -395,6 +401,40 @@ pub(super) fn check(
}
}
fn is_find_or_filter<'a>(
cx: &LateContext<'a>,
map_recv: &hir::Expr<'_>,
filter_arg: &hir::Expr<'_>,
map_arg: &hir::Expr<'_>,
) -> Option<(Ident, CheckResult<'a>)> {
if is_trait_method(cx, map_recv, sym::Iterator)
// filter(|x| ...is_some())...
&& let ExprKind::Closure(&Closure { body: filter_body_id, .. }) = filter_arg.kind
&& let filter_body = cx.tcx.hir().body(filter_body_id)
&& 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)
}
&& let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind
&& let Some(mut offending_expr) = OffendingFilterExpr::hir(cx, filter_body.value, filter_param_id)
&& let ExprKind::Closure(&Closure { body: map_body_id, .. }) = map_arg.kind
&& let map_body = cx.tcx.hir().body(map_body_id)
&& let [map_param] = map_body.params
&& let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind
&& let Some(check_result) =
offending_expr.check_map_call(cx, map_body, map_param_id, filter_param_id, is_filter_param_ref)
{
return Some((map_param_ident, check_result));
}
None
}
fn acceptable_methods(method: &PathSegment<'_>) -> bool {
let methods: [Symbol; 8] = [
sym::clone,

View file

@ -1175,7 +1175,8 @@ declare_clippy_lint! {
declare_clippy_lint! {
/// ### What it does
/// Checks for indirect collection of populated `Option`
/// Checks for iterators of `Option`s using ``.filter(Option::is_some).map(Option::unwrap)` that may
/// be replaced with a `.flatten()` call.
///
/// ### Why is this bad?
/// `Option` is like a collection of 0-1 things, so `flatten`
@ -3752,6 +3753,29 @@ declare_clippy_lint! {
"using `Option.map_or(Err(_), Ok)`, which is more succinctly expressed as `Option.ok_or(_)`"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for iterators of `Result`s using ``.filter(Result::is_ok).map(Result::unwrap)` that may
/// be replaced with a `.flatten()` call.
///
/// ### Why is this bad?
/// `Result` implements `IntoIterator<Item = T>`. This means that `Result` can be flattened
/// automatically without suspicious-looking `unwrap` calls.
///
/// ### Example
/// ```no_run
/// let _ = std::iter::empty::<Result<i32, ()>>().filter(Result::is_ok).map(Result::unwrap);
/// ```
/// Use instead:
/// ```no_run
/// let _ = std::iter::empty::<Result<i32, ()>>().flatten();
/// ```
#[clippy::version = "1.76.0"]
pub RESULT_FILTER_MAP,
complexity,
"filtering `Result` for `Ok` then force-unwrapping, which can be one type-safe operation"
}
pub struct Methods {
avoid_breaking_exported_api: bool,
msrv: Msrv,
@ -3903,6 +3927,7 @@ impl_lint_pass!(Methods => [
UNNECESSARY_FALLIBLE_CONVERSIONS,
JOIN_ABSOLUTE_PATHS,
OPTION_MAP_OR_ERR_OK,
RESULT_FILTER_MAP,
]);
/// Extracts a method call name, args, and `Span` of the method name.