Skip to content

Commit 0958f94

Browse files
committed
Add manual_filter lint for Option
Share much of its implementation with `manual_map` and should greatly benefit from its previous feedback.
1 parent 18e10ca commit 0958f94

13 files changed

+951
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3986,6 +3986,7 @@ Released 2018-09-13
39863986
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
39873987
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
39883988
[`manual_clamp`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_clamp
3989+
[`manual_filter`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter
39893990
[`manual_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_filter_map
39903991
[`manual_find`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find
39913992
[`manual_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_find_map

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
135135
LintId::of(match_result_ok::MATCH_RESULT_OK),
136136
LintId::of(matches::COLLAPSIBLE_MATCH),
137137
LintId::of(matches::INFALLIBLE_DESTRUCTURING_MATCH),
138+
LintId::of(matches::MANUAL_FILTER),
138139
LintId::of(matches::MANUAL_MAP),
139140
LintId::of(matches::MANUAL_UNWRAP_OR),
140141
LintId::of(matches::MATCH_AS_REF),

clippy_lints/src/lib.register_complexity.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ store.register_group(true, "clippy::complexity", Some("clippy_complexity"), vec!
2727
LintId::of(manual_strip::MANUAL_STRIP),
2828
LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN),
2929
LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN),
30+
LintId::of(matches::MANUAL_FILTER),
3031
LintId::of(matches::MANUAL_UNWRAP_OR),
3132
LintId::of(matches::MATCH_AS_REF),
3233
LintId::of(matches::MATCH_SINGLE_BINDING),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ store.register_lints(&[
257257
match_result_ok::MATCH_RESULT_OK,
258258
matches::COLLAPSIBLE_MATCH,
259259
matches::INFALLIBLE_DESTRUCTURING_MATCH,
260+
matches::MANUAL_FILTER,
260261
matches::MANUAL_MAP,
261262
matches::MANUAL_UNWRAP_OR,
262263
matches::MATCH_AS_REF,
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::ty::is_type_diagnostic_item;
3+
use clippy_utils::{is_res_lang_ctor, path_res, path_to_local_id};
4+
use rustc_hir::intravisit::{walk_expr, Visitor};
5+
use rustc_hir::LangItem::OptionSome;
6+
use rustc_hir::{Arm, Block, BlockCheckMode, Expr, ExprKind, HirId, Pat, PatKind, UnsafeSource};
7+
use rustc_lint::LateContext;
8+
use rustc_span::{sym, SyntaxContext};
9+
10+
use super::manual_map::{check_with, SomeExpr};
11+
use super::MANUAL_FILTER;
12+
13+
#[derive(Default)]
14+
struct NeedsUnsafeBlock(pub bool);
15+
16+
impl<'tcx> Visitor<'tcx> for NeedsUnsafeBlock {
17+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
18+
match expr.kind {
19+
ExprKind::Block(
20+
Block {
21+
rules: BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided),
22+
..
23+
},
24+
_,
25+
) => {
26+
self.0 = true;
27+
},
28+
_ => walk_expr(self, expr),
29+
}
30+
}
31+
}
32+
33+
// Function called on the `expr` of `[&+]Some((ref | ref mut) x) => <expr>`
34+
// Need to check if it's of the `if <cond> {<then_expr>} else {<else_expr>}`
35+
// AND that only one `then/else_expr` resolves to `Some(x)` while the other resolves to `None`
36+
// return `cond` if
37+
fn get_cond_expr<'tcx>(
38+
cx: &LateContext<'tcx>,
39+
pat: &Pat<'_>,
40+
expr: &'tcx Expr<'_>,
41+
ctxt: SyntaxContext,
42+
) -> Option<SomeExpr<'tcx>> {
43+
if_chain! {
44+
if let Some(block_expr) = peels_blocks_incl_unsafe_opt(expr);
45+
if let ExprKind::If(cond, then_expr, Some(else_expr)) = block_expr.kind;
46+
if let PatKind::Binding(_,target, ..) = pat.kind;
47+
if let (then_visitor, else_visitor)
48+
= (handle_if_or_else_expr(cx, target, ctxt, then_expr),
49+
handle_if_or_else_expr(cx, target, ctxt, else_expr));
50+
if then_visitor != else_visitor; // check that one expr resolves to `Some(x)`, the other to `None`
51+
then {
52+
let mut needs_unsafe_block = NeedsUnsafeBlock::default();
53+
needs_unsafe_block.visit_expr(expr);
54+
return Some(SomeExpr {
55+
expr: peels_blocks_incl_unsafe(cond.peel_drop_temps()),
56+
needs_unsafe_block: needs_unsafe_block.0,
57+
needs_negated: !then_visitor // if the `then_expr` resolves to `None`, need to negate the cond
58+
})
59+
}
60+
};
61+
None
62+
}
63+
64+
fn peels_blocks_incl_unsafe_opt<'a>(expr: &'a Expr<'a>) -> Option<&'a Expr<'a>> {
65+
// we don't want to use `peel_blocks` here because we don't care if the block is unsafe, it's
66+
// checked by `NeedsUnsafeBlock`
67+
if let ExprKind::Block(block, None) = expr.kind {
68+
if block.stmts.is_empty() {
69+
return block.expr;
70+
}
71+
};
72+
None
73+
}
74+
75+
fn peels_blocks_incl_unsafe<'a>(expr: &'a Expr<'a>) -> &'a Expr<'a> {
76+
peels_blocks_incl_unsafe_opt(expr).unwrap_or(expr)
77+
}
78+
79+
// function called for each <ifelse> expression:
80+
// Some(x) => if <cond> {
81+
// <ifelse>
82+
// } else {
83+
// <ifelse>
84+
// }
85+
// Returns true if <ifelse> resolves to `Some(x)`, `false` otherwise
86+
fn handle_if_or_else_expr<'tcx>(
87+
cx: &LateContext<'_>,
88+
target: HirId,
89+
ctxt: SyntaxContext,
90+
if_or_else_expr: &'tcx Expr<'_>,
91+
) -> bool {
92+
if let Some(inner_expr) = peels_blocks_incl_unsafe_opt(if_or_else_expr) {
93+
// there can be not statements in the block as they would be removed when switching to `.filter`
94+
if let ExprKind::Call(callee, [arg]) = inner_expr.kind {
95+
return ctxt == if_or_else_expr.span.ctxt()
96+
&& is_res_lang_ctor(cx, path_res(cx, callee), OptionSome)
97+
&& path_to_local_id(arg, target);
98+
}
99+
};
100+
false
101+
}
102+
103+
// given the closure: `|<pattern>| <expr>`
104+
// returns `|&<pattern>| <expr>`
105+
fn add_ampersand_if_copy(body_str: String, has_copy_trait: bool) -> String {
106+
if has_copy_trait {
107+
let mut with_ampersand = body_str;
108+
with_ampersand.insert(1, '&');
109+
with_ampersand
110+
} else {
111+
body_str
112+
}
113+
}
114+
115+
pub(super) fn check_match<'tcx>(
116+
cx: &LateContext<'tcx>,
117+
scrutinee: &'tcx Expr<'_>,
118+
arms: &'tcx [Arm<'_>],
119+
expr: &'tcx Expr<'_>,
120+
) {
121+
let ty = cx.typeck_results().expr_ty(expr);
122+
if_chain! {
123+
if is_type_diagnostic_item(cx, ty, sym::Option);
124+
if arms.len() == 2;
125+
if arms[0].guard.is_none();
126+
if arms[1].guard.is_none();
127+
then {
128+
check(cx, expr, scrutinee, arms[0].pat, arms[0].body, Some(arms[1].pat), arms[1].body)
129+
}
130+
}
131+
}
132+
133+
pub(super) fn check_if_let<'tcx>(
134+
cx: &LateContext<'tcx>,
135+
expr: &'tcx Expr<'_>,
136+
let_pat: &'tcx Pat<'_>,
137+
let_expr: &'tcx Expr<'_>,
138+
then_expr: &'tcx Expr<'_>,
139+
else_expr: &'tcx Expr<'_>,
140+
) {
141+
check(cx, expr, let_expr, let_pat, then_expr, None, else_expr);
142+
}
143+
144+
fn check<'tcx>(
145+
cx: &LateContext<'tcx>,
146+
expr: &'tcx Expr<'_>,
147+
scrutinee: &'tcx Expr<'_>,
148+
then_pat: &'tcx Pat<'_>,
149+
then_body: &'tcx Expr<'_>,
150+
else_pat: Option<&'tcx Pat<'_>>,
151+
else_body: &'tcx Expr<'_>,
152+
) {
153+
if let Some(sugg_info) = check_with(
154+
cx,
155+
expr,
156+
scrutinee,
157+
then_pat,
158+
then_body,
159+
else_pat,
160+
else_body,
161+
get_cond_expr,
162+
) {
163+
let body_str = add_ampersand_if_copy(sugg_info.body_str, sugg_info.scrutinee_impl_copy);
164+
span_lint_and_sugg(
165+
cx,
166+
MANUAL_FILTER,
167+
expr.span,
168+
"manual implementation of `Option::filter`",
169+
"try this",
170+
if sugg_info.needs_brackets {
171+
format!(
172+
"{{ {}{}.filter({body_str}) }}",
173+
sugg_info.scrutinee_str, sugg_info.as_ref_str
174+
)
175+
} else {
176+
format!("{}{}.filter({body_str})", sugg_info.scrutinee_str, sugg_info.as_ref_str)
177+
},
178+
sugg_info.app,
179+
);
180+
}
181+
}

0 commit comments

Comments
 (0)