Skip to content

Commit 775ef47

Browse files
committed
Auto merge of #6342 - bbqbaron:issue-6061, r=flip1995
Lint: filter(Option::is_some).map(Option::unwrap) Fixes #6061 *Please write a short comment explaining your change (or "none" for internal only changes)* changelog: * add new lint for filter(Option::is_some).map(Option::unwrap) First Rust PR, so I'm sure I've violated some idioms. Happy to change anything. I'm getting one test failure locally -- a stderr diff for `compile_test`. I'm having a hard time seeing how I could be causing it, so I'm tentatively opening this in the hopes that it's an artifact of my local setup against `rustc`. Hoping it can at least still be reviewed in the meantime. I'm gathering that since this is a method lint, and `.filter(...).map(...)` is already checked, the means of implementation needs to be a little different, so I didn't exactly follow the setup boilerplate. My way of checking for method calls seems a little too direct (ie, "is the second element of the expression literally the path for `Option::is_some`?"), but it seems like that's how some other lints work, so I went with it. I'm assuming we're not concerned about, eg, closures that just end up equivalent to `Option::is_some` by eta reduction.
2 parents c1021b8 + 56fbbf7 commit 775ef47

File tree

7 files changed

+279
-69
lines changed

7 files changed

+279
-69
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,7 @@ Released 2018-09-13
23912391
[`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
23922392
[`option_as_ref_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
23932393
[`option_env_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_env_unwrap
2394+
[`option_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_filter_map
23942395
[`option_if_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
23952396
[`option_map_or_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_or_none
23962397
[`option_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unit_fn

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
812812
&methods::NEW_RET_NO_SELF,
813813
&methods::OK_EXPECT,
814814
&methods::OPTION_AS_REF_DEREF,
815+
&methods::OPTION_FILTER_MAP,
815816
&methods::OPTION_MAP_OR_NONE,
816817
&methods::OR_FUN_CALL,
817818
&methods::RESULT_MAP_OR_INTO_OPTION,
@@ -1606,6 +1607,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16061607
LintId::of(&methods::NEW_RET_NO_SELF),
16071608
LintId::of(&methods::OK_EXPECT),
16081609
LintId::of(&methods::OPTION_AS_REF_DEREF),
1610+
LintId::of(&methods::OPTION_FILTER_MAP),
16091611
LintId::of(&methods::OPTION_MAP_OR_NONE),
16101612
LintId::of(&methods::OR_FUN_CALL),
16111613
LintId::of(&methods::RESULT_MAP_OR_INTO_OPTION),
@@ -1901,6 +1903,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
19011903
LintId::of(&methods::MANUAL_FILTER_MAP),
19021904
LintId::of(&methods::MANUAL_FIND_MAP),
19031905
LintId::of(&methods::OPTION_AS_REF_DEREF),
1906+
LintId::of(&methods::OPTION_FILTER_MAP),
19041907
LintId::of(&methods::SEARCH_IS_SOME),
19051908
LintId::of(&methods::SKIP_WHILE_NEXT),
19061909
LintId::of(&methods::SUSPICIOUS_MAP),
+146-67
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,166 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::source::snippet;
3-
use clippy_utils::{is_trait_method, path_to_local_id, SpanlessEq};
2+
use clippy_utils::source::{indent_of, reindent_multiline, snippet};
3+
use clippy_utils::ty::is_type_diagnostic_item;
4+
use clippy_utils::{is_trait_method, path_to_local_id, remove_blocks, SpanlessEq};
45
use if_chain::if_chain;
56
use rustc_errors::Applicability;
67
use rustc_hir as hir;
7-
use rustc_hir::{Expr, ExprKind, PatKind, UnOp};
8+
use rustc_hir::def::Res;
9+
use rustc_hir::{Expr, ExprKind, PatKind, QPath, UnOp};
810
use rustc_lint::LateContext;
911
use rustc_middle::ty::TyS;
10-
use rustc_span::symbol::sym;
12+
use rustc_span::source_map::Span;
13+
use rustc_span::symbol::{sym, Symbol};
14+
use std::borrow::Cow;
1115

1216
use super::MANUAL_FILTER_MAP;
1317
use super::MANUAL_FIND_MAP;
18+
use super::OPTION_FILTER_MAP;
19+
20+
fn is_method<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, method_name: Symbol) -> bool {
21+
match &expr.kind {
22+
hir::ExprKind::Path(QPath::TypeRelative(_, ref mname)) => mname.ident.name == method_name,
23+
hir::ExprKind::Path(QPath::Resolved(_, segments)) => {
24+
segments.segments.last().unwrap().ident.name == method_name
25+
},
26+
hir::ExprKind::Closure(_, _, c, _, _) => {
27+
let body = cx.tcx.hir().body(*c);
28+
let closure_expr = remove_blocks(&body.value);
29+
let arg_id = body.params[0].pat.hir_id;
30+
match closure_expr.kind {
31+
hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, _, ref args, _) => {
32+
if_chain! {
33+
if ident.name == method_name;
34+
if let hir::ExprKind::Path(path) = &args[0].kind;
35+
if let Res::Local(ref local) = cx.qpath_res(path, args[0].hir_id);
36+
then {
37+
return arg_id == *local
38+
}
39+
}
40+
false
41+
},
42+
_ => false,
43+
}
44+
},
45+
_ => false,
46+
}
47+
}
48+
49+
fn is_option_filter_map<'tcx>(
50+
cx: &LateContext<'tcx>,
51+
filter_arg: &'tcx hir::Expr<'_>,
52+
map_arg: &'tcx hir::Expr<'_>,
53+
) -> bool {
54+
is_method(cx, map_arg, sym::unwrap) && is_method(cx, filter_arg, sym!(is_some))
55+
}
56+
57+
/// lint use of `filter().map()` for `Iterators`
58+
fn lint_filter_some_map_unwrap<'tcx>(
59+
cx: &LateContext<'tcx>,
60+
expr: &'tcx hir::Expr<'_>,
61+
filter_recv: &'tcx hir::Expr<'_>,
62+
filter_arg: &'tcx hir::Expr<'_>,
63+
map_arg: &'tcx hir::Expr<'_>,
64+
target_span: Span,
65+
methods_span: Span,
66+
) {
67+
let iterator = is_trait_method(cx, expr, sym::Iterator);
68+
let option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(&filter_recv), sym::option_type);
69+
if (iterator || option) && is_option_filter_map(cx, filter_arg, map_arg) {
70+
let msg = "`filter` for `Some` followed by `unwrap`";
71+
let help = "consider using `flatten` instead";
72+
let sugg = format!(
73+
"{}",
74+
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, target_span),)
75+
);
76+
span_lint_and_sugg(
77+
cx,
78+
OPTION_FILTER_MAP,
79+
methods_span,
80+
msg,
81+
help,
82+
sugg,
83+
Applicability::MachineApplicable,
84+
);
85+
}
86+
}
1487

1588
/// lint use of `filter().map()` or `find().map()` for `Iterators`
16-
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, is_find: bool) {
89+
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>, is_find: bool, target_span: Span) {
1790
if_chain! {
18-
if let ExprKind::MethodCall(_, _, [map_recv, map_arg], map_span) = expr.kind;
19-
if let ExprKind::MethodCall(_, _, [_, filter_arg], filter_span) = map_recv.kind;
20-
if is_trait_method(cx, map_recv, sym::Iterator);
91+
if let ExprKind::MethodCall(_, _, [map_recv, map_arg], map_span) = expr.kind;
92+
if let ExprKind::MethodCall(_, _, [filter_recv, filter_arg], filter_span) = map_recv.kind;
93+
then {
94+
lint_filter_some_map_unwrap(cx, expr, filter_recv, filter_arg,
95+
map_arg, target_span, filter_span.to(map_span));
96+
if_chain! {
97+
if is_trait_method(cx, map_recv, sym::Iterator);
2198

22-
// filter(|x| ...is_some())...
23-
if let ExprKind::Closure(_, _, filter_body_id, ..) = filter_arg.kind;
24-
let filter_body = cx.tcx.hir().body(filter_body_id);
25-
if let [filter_param] = filter_body.params;
26-
// optional ref pattern: `filter(|&x| ..)`
27-
let (filter_pat, is_filter_param_ref) = if let PatKind::Ref(ref_pat, _) = filter_param.pat.kind {
28-
(ref_pat, true)
29-
} else {
30-
(filter_param.pat, false)
31-
};
32-
// closure ends with is_some() or is_ok()
33-
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
34-
if let ExprKind::MethodCall(path, _, [filter_arg], _) = filter_body.value.kind;
35-
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).ty_adt_def();
36-
if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::option_type, opt_ty.did) {
37-
Some(false)
38-
} else if cx.tcx.is_diagnostic_item(sym::result_type, opt_ty.did) {
39-
Some(true)
40-
} else {
41-
None
42-
};
43-
if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" };
99+
// filter(|x| ...is_some())...
100+
if let ExprKind::Closure(_, _, filter_body_id, ..) = filter_arg.kind;
101+
let filter_body = cx.tcx.hir().body(filter_body_id);
102+
if let [filter_param] = filter_body.params;
103+
// optional ref pattern: `filter(|&x| ..)`
104+
let (filter_pat, is_filter_param_ref) = if let PatKind::Ref(ref_pat, _) = filter_param.pat.kind {
105+
(ref_pat, true)
106+
} else {
107+
(filter_param.pat, false)
108+
};
109+
// closure ends with is_some() or is_ok()
110+
if let PatKind::Binding(_, filter_param_id, _, None) = filter_pat.kind;
111+
if let ExprKind::MethodCall(path, _, [filter_arg], _) = filter_body.value.kind;
112+
if let Some(opt_ty) = cx.typeck_results().expr_ty(filter_arg).ty_adt_def();
113+
if let Some(is_result) = if cx.tcx.is_diagnostic_item(sym::option_type, opt_ty.did) {
114+
Some(false)
115+
} else if cx.tcx.is_diagnostic_item(sym::result_type, opt_ty.did) {
116+
Some(true)
117+
} else {
118+
None
119+
};
120+
if path.ident.name.as_str() == if is_result { "is_ok" } else { "is_some" };
44121

45-
// ...map(|x| ...unwrap())
46-
if let ExprKind::Closure(_, _, map_body_id, ..) = map_arg.kind;
47-
let map_body = cx.tcx.hir().body(map_body_id);
48-
if let [map_param] = map_body.params;
49-
if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind;
50-
// closure ends with expect() or unwrap()
51-
if let ExprKind::MethodCall(seg, _, [map_arg, ..], _) = map_body.value.kind;
52-
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);
122+
// ...map(|x| ...unwrap())
123+
if let ExprKind::Closure(_, _, map_body_id, ..) = map_arg.kind;
124+
let map_body = cx.tcx.hir().body(map_body_id);
125+
if let [map_param] = map_body.params;
126+
if let PatKind::Binding(_, map_param_id, map_param_ident, None) = map_param.pat.kind;
127+
// closure ends with expect() or unwrap()
128+
if let ExprKind::MethodCall(seg, _, [map_arg, ..], _) = map_body.value.kind;
129+
if matches!(seg.ident.name, sym::expect | sym::unwrap | sym::unwrap_or);
53130

54-
let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
55-
// in `filter(|x| ..)`, replace `*x` with `x`
56-
let a_path = if_chain! {
57-
if !is_filter_param_ref;
58-
if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind;
59-
then { expr_path } else { a }
60-
};
61-
// let the filter closure arg and the map closure arg be equal
62-
if_chain! {
63-
if path_to_local_id(a_path, filter_param_id);
64-
if path_to_local_id(b, map_param_id);
65-
if TyS::same_type(cx.typeck_results().expr_ty_adjusted(a), cx.typeck_results().expr_ty_adjusted(b));
66-
then {
67-
return true;
131+
let eq_fallback = |a: &Expr<'_>, b: &Expr<'_>| {
132+
// in `filter(|x| ..)`, replace `*x` with `x`
133+
let a_path = if_chain! {
134+
if !is_filter_param_ref;
135+
if let ExprKind::Unary(UnOp::Deref, expr_path) = a.kind;
136+
then { expr_path } else { a }
137+
};
138+
// let the filter closure arg and the map closure arg be equal
139+
if_chain! {
140+
if path_to_local_id(a_path, filter_param_id);
141+
if path_to_local_id(b, map_param_id);
142+
if TyS::same_type(cx.typeck_results().expr_ty_adjusted(a), cx.typeck_results().expr_ty_adjusted(b));
143+
then {
144+
return true;
145+
}
68146
}
69-
}
70-
false
71-
};
72-
if SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg);
73-
then {
74-
let span = filter_span.to(map_span);
75-
let (filter_name, lint) = if is_find {
76-
("find", MANUAL_FIND_MAP)
77-
} else {
78-
("filter", MANUAL_FILTER_MAP)
147+
false
79148
};
80-
let msg = format!("`{}(..).map(..)` can be simplified as `{0}_map(..)`", filter_name);
81-
let to_opt = if is_result { ".ok()" } else { "" };
82-
let sugg = format!("{}_map(|{}| {}{})", filter_name, map_param_ident,
83-
snippet(cx, map_arg.span, ".."), to_opt);
84-
span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable);
149+
if SpanlessEq::new(cx).expr_fallback(eq_fallback).eq_expr(filter_arg, map_arg);
150+
then {
151+
let span = filter_span.to(map_span);
152+
let (filter_name, lint) = if is_find {
153+
("find", MANUAL_FIND_MAP)
154+
} else {
155+
("filter", MANUAL_FILTER_MAP)
156+
};
157+
let msg = format!("`{}(..).map(..)` can be simplified as `{0}_map(..)`", filter_name);
158+
let to_opt = if is_result { ".ok()" } else { "" };
159+
let sugg = format!("{}_map(|{}| {}{})", filter_name, map_param_ident,
160+
snippet(cx, map_arg.span, ".."), to_opt);
161+
span_lint_and_sugg(cx, lint, span, &msg, "try", sugg, Applicability::MachineApplicable);
162+
}
163+
}
85164
}
86165
}
87166
}

clippy_lints/src/methods/mod.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,28 @@ declare_clippy_lint! {
896896
"using `Iterator::step_by(0)`, which will panic at runtime"
897897
}
898898

899+
declare_clippy_lint! {
900+
/// **What it does:** Checks for indirect collection of populated `Option`
901+
///
902+
/// **Why is this bad?** `Option` is like a collection of 0-1 things, so `flatten`
903+
/// automatically does this without suspicious-looking `unwrap` calls.
904+
///
905+
/// **Known problems:** None.
906+
///
907+
/// **Example:**
908+
///
909+
/// ```rust
910+
/// let _ = std::iter::empty::<Option<i32>>().filter(Option::is_some).map(Option::unwrap);
911+
/// ```
912+
/// Use instead:
913+
/// ```rust
914+
/// let _ = std::iter::empty::<Option<i32>>().flatten();
915+
/// ```
916+
pub OPTION_FILTER_MAP,
917+
complexity,
918+
"filtering `Option` for `Some` then force-unwrapping, which can be one type-safe operation"
919+
}
920+
899921
declare_clippy_lint! {
900922
/// **What it does:** Checks for the use of `iter.nth(0)`.
901923
///
@@ -1651,6 +1673,7 @@ impl_lint_pass!(Methods => [
16511673
FILTER_MAP_IDENTITY,
16521674
MANUAL_FILTER_MAP,
16531675
MANUAL_FIND_MAP,
1676+
OPTION_FILTER_MAP,
16541677
FILTER_MAP_NEXT,
16551678
FLAT_MAP_IDENTITY,
16561679
MAP_FLATTEN,
@@ -1720,10 +1743,10 @@ impl<'tcx> LateLintPass<'tcx> for Methods {
17201743
["next", "filter"] => filter_next::check(cx, expr, arg_lists[1]),
17211744
["next", "skip_while"] => skip_while_next::check(cx, expr, arg_lists[1]),
17221745
["next", "iter"] => iter_next_slice::check(cx, expr, arg_lists[1]),
1723-
["map", "filter"] => filter_map::check(cx, expr, false),
1746+
["map", "filter"] => filter_map::check(cx, expr, false, method_spans[0]),
17241747
["map", "filter_map"] => filter_map_map::check(cx, expr),
17251748
["next", "filter_map"] => filter_map_next::check(cx, expr, arg_lists[1], self.msrv.as_ref()),
1726-
["map", "find"] => filter_map::check(cx, expr, true),
1749+
["map", "find"] => filter_map::check(cx, expr, true, method_spans[0]),
17271750
["flat_map", "filter"] => filter_flat_map::check(cx, expr),
17281751
["flat_map", "filter_map"] => filter_map_flat_map::check(cx, expr),
17291752
["flat_map", ..] => flat_map_identity::check(cx, expr, arg_lists[0], method_spans[0]),

tests/ui/option_filter_map.fixed

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#![warn(clippy::option_filter_map)]
2+
// run-rustfix
3+
fn odds_out(x: i32) -> Option<i32> {
4+
if x % 2 == 0 { Some(x) } else { None }
5+
}
6+
7+
fn main() {
8+
let _ = Some(Some(1)).flatten();
9+
let _ = Some(Some(1)).flatten();
10+
let _ = Some(1).map(odds_out).flatten();
11+
let _ = Some(1).map(odds_out).flatten();
12+
13+
let _ = vec![Some(1)].into_iter().flatten();
14+
let _ = vec![Some(1)].into_iter().flatten();
15+
let _ = vec![1]
16+
.into_iter()
17+
.map(odds_out)
18+
.flatten();
19+
let _ = vec![1]
20+
.into_iter()
21+
.map(odds_out)
22+
.flatten();
23+
}

tests/ui/option_filter_map.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#![warn(clippy::option_filter_map)]
2+
// run-rustfix
3+
fn odds_out(x: i32) -> Option<i32> {
4+
if x % 2 == 0 { Some(x) } else { None }
5+
}
6+
7+
fn main() {
8+
let _ = Some(Some(1)).filter(Option::is_some).map(Option::unwrap);
9+
let _ = Some(Some(1)).filter(|o| o.is_some()).map(|o| o.unwrap());
10+
let _ = Some(1).map(odds_out).filter(Option::is_some).map(Option::unwrap);
11+
let _ = Some(1).map(odds_out).filter(|o| o.is_some()).map(|o| o.unwrap());
12+
13+
let _ = vec![Some(1)].into_iter().filter(Option::is_some).map(Option::unwrap);
14+
let _ = vec![Some(1)].into_iter().filter(|o| o.is_some()).map(|o| o.unwrap());
15+
let _ = vec![1]
16+
.into_iter()
17+
.map(odds_out)
18+
.filter(Option::is_some)
19+
.map(Option::unwrap);
20+
let _ = vec![1]
21+
.into_iter()
22+
.map(odds_out)
23+
.filter(|o| o.is_some())
24+
.map(|o| o.unwrap());
25+
}

0 commit comments

Comments
 (0)