Skip to content

Commit

Permalink
Auto merge of rust-lang#8958 - Alexendoo:simple_filter_map, r=giraffate
Browse files Browse the repository at this point in the history
Lint simple expressions in `manual_filter_map`, `manual_find_map`

changelog: Lint simple expressions in [`manual_filter_map`], [`manual_find_map`]

The current comparison rules out `.find(|a| a.is_some()).map(|b| b.unwrap())` because `a` being a reference can effect more complicated expressions, this adds a simple check for that case and adds the necessary derefs

There's some overlap with `option_filter_map` so `lint_filter_some_map_unwrap` now returns a `bool` to indicate it linted
  • Loading branch information
bors committed Jul 7, 2022
2 parents 2058b92 + 307b8cd commit 54feac1
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 73 deletions.
107 changes: 54 additions & 53 deletions clippy_lints/src/methods/filter_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use rustc_hir as hir;
use rustc_hir::def::Res;
use rustc_hir::{Expr, ExprKind, PatKind, PathSegment, QPath, UnOp};
use rustc_lint::LateContext;
use rustc_middle::ty::adjustment::Adjust;
use rustc_span::source_map::Span;
use rustc_span::symbol::{sym, Symbol};
use std::borrow::Cow;
Expand Down Expand Up @@ -49,35 +50,18 @@ fn is_option_filter_map<'tcx>(cx: &LateContext<'tcx>, filter_arg: &hir::Expr<'_>
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some))
}

/// lint use of `filter().map()` for `Iterators`
fn lint_filter_some_map_unwrap(
/// is `filter(|x| x.is_some()).map(|x| x.unwrap())`
fn is_filter_some_map_unwrap(
cx: &LateContext<'_>,
expr: &hir::Expr<'_>,
filter_recv: &hir::Expr<'_>,
filter_arg: &hir::Expr<'_>,
map_arg: &hir::Expr<'_>,
target_span: Span,
methods_span: Span,
) {
) -> bool {
let iterator = is_trait_method(cx, expr, sym::Iterator);
let option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(filter_recv), sym::Option);
if (iterator || option) && is_option_filter_map(cx, filter_arg, map_arg) {
let msg = "`filter` for `Some` followed by `unwrap`";
let help = "consider using `flatten` instead";
let sugg = format!(
"{}",
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, target_span),)
);
span_lint_and_sugg(
cx,
OPTION_FILTER_MAP,
methods_span,
msg,
help,
sugg,
Applicability::MachineApplicable,
);
}

(iterator || option) && is_option_filter_map(cx, filter_arg, map_arg)
}

/// lint use of `filter().map()` or `find().map()` for `Iterators`
Expand All @@ -93,15 +77,20 @@ pub(super) fn check<'tcx>(
map_span: Span,
is_find: bool,
) {
lint_filter_some_map_unwrap(
cx,
expr,
filter_recv,
filter_arg,
map_arg,
map_span,
filter_span.with_hi(expr.span.hi()),
);
if is_filter_some_map_unwrap(cx, expr, filter_recv, filter_arg, map_arg) {
span_lint_and_sugg(
cx,
OPTION_FILTER_MAP,
filter_span.with_hi(expr.span.hi()),
"`filter` for `Some` followed by `unwrap`",
"consider using `flatten` instead",
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, map_span)).into_owned(),
Applicability::MachineApplicable,
);

return;
}

if_chain! {
if is_trait_method(cx, map_recv, sym::Iterator);

Expand All @@ -118,7 +107,7 @@ pub(super) fn check<'tcx>(
// closure ends with is_some() or is_ok()
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
if let ExprKind::MethodCall(path, [filter_arg], _) = filter_body.value.kind;
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).ty_adt_def();
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).peel_refs().ty_adt_def();
if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::Option, opt_ty.did()) {
Some(false)
} else if cx.tcx.is_diagnostic_item(sym::Result, opt_ty.did()) {
Expand All @@ -137,6 +126,19 @@ pub(super) fn check<'tcx>(
if let ExprKind::MethodCall(seg, [map_arg, ..], _) = map_body.value.kind;
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);

// .filter(..).map(|y| f(y).copied().unwrap())
// ~~~~
let map_arg_peeled = match map_arg.kind {
ExprKind::MethodCall(method, [original_arg], _) if acceptable_methods(method) => {
original_arg
},
_ => map_arg,
};

// .filter(|x| x.is_some()).map(|y| y[.acceptable_method()].unwrap())
let simple_equal = path_to_local_id(filter_arg, filter_param_id)
&& path_to_local_id(map_arg_peeled, map_param_id);

let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
// in `filter(|x| ..)`, replace `*x` with `x`
let a_path = if_chain! {
Expand All @@ -145,36 +147,35 @@ pub(super) fn check<'tcx>(
then { expr_path } else { a }
};
// let the filter closure arg and the map closure arg be equal
if_chain! {
if path_to_local_id(a_path, filter_param_id);
if path_to_local_id(b, map_param_id);
if cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b);
then {
return true;
}
}
false
};

if match map_arg.kind {
ExprKind::MethodCall(method, [original_arg], _) => {
acceptable_methods(method)
&& SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, original_arg)
},
_ => SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg)
path_to_local_id(a_path, filter_param_id)
&& path_to_local_id(b, map_param_id)
&& cx.typeck_results().expr_ty_adjusted(a) == cx.typeck_results().expr_ty_adjusted(b)
};

if simple_equal || SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg_peeled);
then {
let span = filter_span.with_hi(expr.span.hi());
let (filter_name, lint) = if is_find {
("find", MANUAL_FIND_MAP)
} else {
("filter", MANUAL_FILTER_MAP)
};
let msg = format!("`{}(..).map(..)` can be simplified as `{0}_map(..)`", filter_name);
let to_opt = if is_result { ".ok()" } else { "" };
let sugg = format!("{}_map(|{}| {}{})", filter_name, map_param_ident,
snippet(cx, map_arg.span, ".."), to_opt);
let msg = format!("`{filter_name}(..).map(..)` can be simplified as `{filter_name}_map(..)`");
let (to_opt, deref) = if is_result {
(".ok()", String::new())
} else {
let derefs = cx.typeck_results()
.expr_adjustments(map_arg)
.iter()
.filter(|adj| matches!(adj.kind, Adjust::Deref(_)))
.count();

("", "*".repeat(derefs))
};
let sugg = format!(
"{filter_name}_map(|{map_param_ident}| {deref}{}{to_opt})",
snippet(cx, map_arg.span, ".."),
);
span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable);
}
}
Expand Down
34 changes: 34 additions & 0 deletions tests/ui/manual_filter_map.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ fn main() {

// is_ok(), unwrap_or()
let _ = (0..).filter_map(|a| to_res(a).ok());

let _ = (1..5)
.filter_map(|y| *to_ref(to_opt(y)));
let _ = (1..5)
.filter_map(|y| *to_ref(to_opt(y)));

let _ = (1..5)
.filter_map(|y| to_ref(to_res(y)).ok());
let _ = (1..5)
.filter_map(|y| to_ref(to_res(y)).ok());
}

#[rustfmt::skip]
fn simple_equal() {
iter::<Option<&u8>>().find_map(|x| x.cloned());
iter::<&Option<&u8>>().find_map(|x| x.cloned());
iter::<&Option<String>>().find_map(|x| x.as_deref());
iter::<Option<&String>>().find_map(|y| to_ref(y).cloned());

iter::<Result<u8, ()>>().find_map(|x| x.ok());
iter::<&Result<u8, ()>>().find_map(|x| x.ok());
iter::<&&Result<u8, ()>>().find_map(|x| x.ok());
iter::<Result<&u8, ()>>().find_map(|x| x.cloned().ok());
iter::<&Result<&u8, ()>>().find_map(|x| x.cloned().ok());
iter::<&Result<String, ()>>().find_map(|x| x.as_deref().ok());
iter::<Result<&String, ()>>().find_map(|y| to_ref(y).cloned().ok());
}

fn no_lint() {
Expand All @@ -28,6 +54,10 @@ fn no_lint() {
.map(|a| to_opt(a).unwrap());
}

fn iter<T>() -> impl Iterator<Item = T> {
std::iter::empty()
}

fn to_opt<T>(_: T) -> Option<T> {
unimplemented!()
}
Expand All @@ -36,6 +66,10 @@ fn to_res<T>(_: T) -> Result<T, ()> {
unimplemented!()
}

fn to_ref<'a, T>(_: T) -> &'a T {
unimplemented!()
}

struct Issue8920<'a> {
option_field: Option<String>,
result_field: Result<String, ()>,
Expand Down
38 changes: 38 additions & 0 deletions tests/ui/manual_filter_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,36 @@ fn main() {

// is_ok(), unwrap_or()
let _ = (0..).filter(|&n| to_res(n).is_ok()).map(|a| to_res(a).unwrap_or(1));

let _ = (1..5)
.filter(|&x| to_ref(to_opt(x)).is_some())
.map(|y| to_ref(to_opt(y)).unwrap());
let _ = (1..5)
.filter(|x| to_ref(to_opt(*x)).is_some())
.map(|y| to_ref(to_opt(y)).unwrap());

let _ = (1..5)
.filter(|&x| to_ref(to_res(x)).is_ok())
.map(|y| to_ref(to_res(y)).unwrap());
let _ = (1..5)
.filter(|x| to_ref(to_res(*x)).is_ok())
.map(|y| to_ref(to_res(y)).unwrap());
}

#[rustfmt::skip]
fn simple_equal() {
iter::<Option<&u8>>().find(|x| x.is_some()).map(|x| x.cloned().unwrap());
iter::<&Option<&u8>>().find(|x| x.is_some()).map(|x| x.cloned().unwrap());
iter::<&Option<String>>().find(|x| x.is_some()).map(|x| x.as_deref().unwrap());
iter::<Option<&String>>().find(|&x| to_ref(x).is_some()).map(|y| to_ref(y).cloned().unwrap());

iter::<Result<u8, ()>>().find(|x| x.is_ok()).map(|x| x.unwrap());
iter::<&Result<u8, ()>>().find(|x| x.is_ok()).map(|x| x.unwrap());
iter::<&&Result<u8, ()>>().find(|x| x.is_ok()).map(|x| x.unwrap());
iter::<Result<&u8, ()>>().find(|x| x.is_ok()).map(|x| x.cloned().unwrap());
iter::<&Result<&u8, ()>>().find(|x| x.is_ok()).map(|x| x.cloned().unwrap());
iter::<&Result<String, ()>>().find(|x| x.is_ok()).map(|x| x.as_deref().unwrap());
iter::<Result<&String, ()>>().find(|&x| to_ref(x).is_ok()).map(|y| to_ref(y).cloned().unwrap());
}

fn no_lint() {
Expand All @@ -28,6 +58,10 @@ fn no_lint() {
.map(|a| to_opt(a).unwrap());
}

fn iter<T>() -> impl Iterator<Item = T> {
std::iter::empty()
}

fn to_opt<T>(_: T) -> Option<T> {
unimplemented!()
}
Expand All @@ -36,6 +70,10 @@ fn to_res<T>(_: T) -> Result<T, ()> {
unimplemented!()
}

fn to_ref<'a, T>(_: T) -> &'a T {
unimplemented!()
}

struct Issue8920<'a> {
option_field: Option<String>,
result_field: Result<String, ()>,
Expand Down
Loading

0 comments on commit 54feac1

Please sign in to comment.