Skip to content

Commit 56fbbf7

Browse files
author
Eric Loren
committed
Suggest flatten instead of is_some -> unwrap
1 parent 0e06b3c commit 56fbbf7

File tree

8 files changed

+280
-70
lines changed

8 files changed

+280
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,6 +2389,7 @@ Released 2018-09-13
23892389
[`op_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#op_ref
23902390
[`option_as_ref_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
23912391
[`option_env_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_env_unwrap
2392+
[`option_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_filter_map
23922393
[`option_if_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
23932394
[`option_map_or_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_or_none
23942395
[`option_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unit_fn

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 400 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 450 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
804804
&methods::NEW_RET_NO_SELF,
805805
&methods::OK_EXPECT,
806806
&methods::OPTION_AS_REF_DEREF,
807+
&methods::OPTION_FILTER_MAP,
807808
&methods::OPTION_MAP_OR_NONE,
808809
&methods::OR_FUN_CALL,
809810
&methods::RESULT_MAP_OR_INTO_OPTION,
@@ -1596,6 +1597,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
15961597
LintId::of(&methods::NEW_RET_NO_SELF),
15971598
LintId::of(&methods::OK_EXPECT),
15981599
LintId::of(&methods::OPTION_AS_REF_DEREF),
1600+
LintId::of(&methods::OPTION_FILTER_MAP),
15991601
LintId::of(&methods::OPTION_MAP_OR_NONE),
16001602
LintId::of(&methods::OR_FUN_CALL),
16011603
LintId::of(&methods::RESULT_MAP_OR_INTO_OPTION),
@@ -1891,6 +1893,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
18911893
LintId::of(&methods::MANUAL_FILTER_MAP),
18921894
LintId::of(&methods::MANUAL_FIND_MAP),
18931895
LintId::of(&methods::OPTION_AS_REF_DEREF),
1896+
LintId::of(&methods::OPTION_FILTER_MAP),
18941897
LintId::of(&methods::SEARCH_IS_SOME),
18951898
LintId::of(&methods::SKIP_WHILE_NEXT),
18961899
LintId::of(&methods::SUSPICIOUS_MAP),
Lines changed: 146 additions & 67 deletions
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

Lines changed: 25 additions & 2 deletions
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

Lines changed: 23 additions & 0 deletions
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

Lines changed: 25 additions & 0 deletions
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)