Skip to content

Commit f727193

Browse files
committed
Add: option_manual_map lint
1 parent 2950c8e commit f727193

File tree

7 files changed

+293
-3
lines changed

7 files changed

+293
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,7 @@ Released 2018-09-13
21222122
[`option_if_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
21232123
[`option_map_or_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_or_none
21242124
[`option_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unit_fn
2125+
[`option_match_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_match_map
21252126
[`option_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_option
21262127
[`or_fun_call`]: https://rust-lang.github.io/rust-clippy/master/index.html#or_fun_call
21272128
[`out_of_bounds_indexing`]: https://rust-lang.github.io/rust-clippy/master/index.html#out_of_bounds_indexing

clippy_lints/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ mod non_expressive_names;
282282
mod open_options;
283283
mod option_env_unwrap;
284284
mod option_if_let_else;
285+
mod option_manual_map;
285286
mod overflow_check_conditional;
286287
mod panic_in_result_fn;
287288
mod panic_unimplemented;
@@ -817,6 +818,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
817818
&open_options::NONSENSICAL_OPEN_OPTIONS,
818819
&option_env_unwrap::OPTION_ENV_UNWRAP,
819820
&option_if_let_else::OPTION_IF_LET_ELSE,
821+
&option_manual_map::OPTION_MANUAL_MAP,
820822
&overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL,
821823
&panic_in_result_fn::PANIC_IN_RESULT_FN,
822824
&panic_unimplemented::PANIC,
@@ -1224,6 +1226,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12241226
store.register_late_pass(|| box zero_sized_map_values::ZeroSizedMapValues);
12251227
store.register_late_pass(|| box vec_init_then_push::VecInitThenPush::default());
12261228
store.register_late_pass(move || box types::PtrAsPtr::new(msrv));
1229+
store.register_late_pass(|| box option_manual_map::OptionManualMap);
12271230

12281231
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
12291232
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1569,6 +1572,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
15691572
LintId::of(&non_expressive_names::MANY_SINGLE_CHAR_NAMES),
15701573
LintId::of(&open_options::NONSENSICAL_OPEN_OPTIONS),
15711574
LintId::of(&option_env_unwrap::OPTION_ENV_UNWRAP),
1575+
LintId::of(&option_manual_map::OPTION_MANUAL_MAP),
15721576
LintId::of(&overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL),
15731577
LintId::of(&partialeq_ne_impl::PARTIALEQ_NE_IMPL),
15741578
LintId::of(&precedence::PRECEDENCE),
@@ -1744,6 +1748,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
17441748
LintId::of(&non_copy_const::DECLARE_INTERIOR_MUTABLE_CONST),
17451749
LintId::of(&non_expressive_names::JUST_UNDERSCORES_AND_DIGITS),
17461750
LintId::of(&non_expressive_names::MANY_SINGLE_CHAR_NAMES),
1751+
LintId::of(&option_manual_map::OPTION_MANUAL_MAP),
17471752
LintId::of(&ptr::CMP_NULL),
17481753
LintId::of(&ptr::PTR_ARG),
17491754
LintId::of(&ptr_eq::PTR_EQ),

clippy_lints/src/methods/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3143,10 +3143,9 @@ fn lint_search_is_some<'tcx>(
31433143
then {
31443144
if let hir::PatKind::Ref(..) = closure_arg.pat.kind {
31453145
Some(search_snippet.replacen('&', "", 1))
3146-
} else if let Some(name) = get_arg_name(&closure_arg.pat) {
3147-
Some(search_snippet.replace(&format!("*{}", name), &name.as_str()))
31483146
} else {
3149-
None
3147+
get_arg_name(&closure_arg.pat).map(|name|
3148+
search_snippet.replace(&format!("*{}", name), &name.as_str()))
31503149
}
31513150
} else {
31523151
None

clippy_lints/src/option_manual_map.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use crate::utils::{is_type_diagnostic_item, match_def_path, paths, snippet_with_applicability, span_lint_and_sugg};
2+
use core::fmt;
3+
use if_chain::if_chain;
4+
use rustc_errors::Applicability;
5+
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, PatKind, QPath};
6+
use rustc_lint::{LateContext, LateLintPass, LintContext};
7+
use rustc_middle::lint::in_external_macro;
8+
use rustc_session::{declare_lint_pass, declare_tool_lint};
9+
use rustc_span::symbol::{sym, Ident};
10+
11+
declare_clippy_lint! {
12+
/// **What it does:** Checks for usages of `match` which could be implemented using `Option::map`
13+
///
14+
/// **Why is this bad?** Using the `map` method is clearer and more concise.
15+
///
16+
/// **Known problems:** None.
17+
///
18+
/// **Example:**
19+
///
20+
/// ```rust
21+
/// match Some(0) {
22+
/// Some(x) => Some(x + 1),
23+
/// None => None,
24+
/// };
25+
/// ```
26+
/// Use instead:
27+
/// ```rust
28+
/// Some(0).map(|x| x + 1);
29+
/// ```
30+
pub OPTION_MANUAL_MAP,
31+
style,
32+
"reimplementation of `Option::map`"
33+
}
34+
35+
declare_lint_pass!(OptionManualMap => [OPTION_MANUAL_MAP]);
36+
37+
impl LateLintPass<'_> for OptionManualMap {
38+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
39+
if_chain! {
40+
if !in_external_macro(cx.sess(), expr.span);
41+
if let ExprKind::Match(scrutinee, [arm1, arm2], _) = expr.kind;
42+
if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr), sym::option_type);
43+
if let Some((some_binding, some_expr, none_expr)) = if is_none_arm(cx, arm1) {
44+
get_some_binding(cx, arm2).map(|binding| (binding, arm2.body, arm1.body))
45+
} else if is_none_arm(cx, arm2) {
46+
get_some_binding(cx, arm1).map(|binding| (binding, arm1.body, arm2.body))
47+
} else {
48+
None
49+
};
50+
51+
if is_none_expr(cx, none_expr);
52+
if let Some(some_expr) = get_some_expr(cx, some_expr);
53+
54+
then {
55+
let mut app = Applicability::MachineApplicable;
56+
57+
let body = if_chain! {
58+
if let SomeBinding::Name(some_name) = some_binding;
59+
if let ExprKind::Call(func, [arg]) = some_expr.kind;
60+
if let ExprKind::Path(QPath::Resolved(_, arg_path)) = arg.kind;
61+
if let [arg_name] = arg_path.segments;
62+
if arg_name.ident.name == some_name.name;
63+
then {
64+
snippet_with_applicability(cx, func.span, "_", &mut app).into_owned()
65+
} else {
66+
format!("|{}| {}", some_binding, snippet_with_applicability(cx, some_expr.span, "..", &mut app))
67+
}
68+
};
69+
70+
let scrutinee = snippet_with_applicability(cx, scrutinee.span, "_", &mut app);
71+
span_lint_and_sugg(
72+
cx,
73+
OPTION_MANUAL_MAP,
74+
expr.span,
75+
"manual implementation of `Option::map`",
76+
"use map instead",
77+
format!("{}.map({})", scrutinee, body),
78+
app
79+
);
80+
}
81+
}
82+
}
83+
}
84+
85+
enum SomeBinding {
86+
Wild,
87+
Name(Ident),
88+
}
89+
impl fmt::Display for SomeBinding {
90+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91+
match self {
92+
Self::Wild => f.write_str("_"),
93+
Self::Name(name) => name.fmt(f),
94+
}
95+
}
96+
}
97+
98+
fn get_some_expr(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
99+
match expr.kind {
100+
ExprKind::Call(
101+
Expr {
102+
kind: ExprKind::Path(QPath::Resolved(None, path)),
103+
..
104+
},
105+
[arg],
106+
) => {
107+
if match_def_path(cx, path.res.opt_def_id()?, &paths::OPTION_SOME) {
108+
Some(arg)
109+
} else {
110+
None
111+
}
112+
},
113+
ExprKind::Block(
114+
Block {
115+
stmts: [],
116+
expr: Some(expr),
117+
..
118+
},
119+
_,
120+
) => get_some_expr(cx, expr),
121+
_ => None,
122+
}
123+
}
124+
125+
fn is_none_expr(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> bool {
126+
match expr.kind {
127+
ExprKind::Path(QPath::Resolved(None, path)) => path
128+
.res
129+
.opt_def_id()
130+
.map_or(false, |id| match_def_path(cx, id, &paths::OPTION_NONE)),
131+
ExprKind::Block(
132+
Block {
133+
stmts: [],
134+
expr: Some(expr),
135+
..
136+
},
137+
_,
138+
) => is_none_expr(cx, expr),
139+
_ => false,
140+
}
141+
}
142+
143+
fn is_none_arm(cx: &LateContext<'tcx>, arm: &'tcx Arm<'_>) -> bool {
144+
if arm.guard.is_some() {
145+
return false;
146+
}
147+
match &arm.pat.kind {
148+
PatKind::Wild => true,
149+
PatKind::Path(QPath::Resolved(None, path)) => path
150+
.res
151+
.opt_def_id()
152+
.map_or(false, |id| match_def_path(cx, id, &paths::OPTION_NONE)),
153+
_ => false,
154+
}
155+
}
156+
157+
fn get_some_binding(cx: &LateContext<'tcx>, arm: &'tcx Arm<'_>) -> Option<SomeBinding> {
158+
if_chain! {
159+
if arm.guard.is_none();
160+
if let PatKind::TupleStruct(QPath::Resolved(None, path), [some_pat], _) = arm.pat.kind;
161+
if let Some(id) = path.res.opt_def_id();
162+
if match_def_path(cx, id, &paths::OPTION_SOME);
163+
then {
164+
match some_pat.kind {
165+
PatKind::Binding(BindingAnnotation::Unannotated, _, bind_name, None) => {
166+
Some(SomeBinding::Name(bind_name))
167+
}
168+
PatKind::Wild => Some(SomeBinding::Wild),
169+
_ => None,
170+
}
171+
} else {
172+
None
173+
}
174+
}
175+
}

tests/ui/option_manual_map.fixed

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::option_manual_map)]
4+
#![allow(clippy::map_identity)]
5+
6+
fn main() {
7+
Some(0).map(|_| 2);
8+
9+
Some(0).map(|x| x + 1);
10+
11+
Some("").map(|x| x.is_empty());
12+
13+
Some(0).map(|x| !x);
14+
15+
#[rustfmt::skip]
16+
Some(0).map(std::convert::identity);
17+
18+
match Some(0) {
19+
Some(x) if false => Some(x + 1),
20+
_ => None,
21+
};
22+
}

tests/ui/option_manual_map.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::option_manual_map)]
4+
#![allow(clippy::map_identity)]
5+
6+
fn main() {
7+
match Some(0) {
8+
Some(_) => Some(2),
9+
None::<u32> => None,
10+
};
11+
12+
match Some(0) {
13+
Some(x) => Some(x + 1),
14+
_ => None,
15+
};
16+
17+
match Some("") {
18+
Some(x) => Some(x.is_empty()),
19+
None => None,
20+
};
21+
22+
if let Some(x) = Some(0) {
23+
Some(!x)
24+
} else {
25+
None
26+
};
27+
28+
#[rustfmt::skip]
29+
match Some(0) {
30+
Some(x) => { Some(std::convert::identity(x)) }
31+
None => { None }
32+
};
33+
34+
match Some(0) {
35+
Some(x) if false => Some(x + 1),
36+
_ => None,
37+
};
38+
}

tests/ui/option_manual_map.stderr

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
error: manual implementation of `Option::map`
2+
--> $DIR/option_manual_map.rs:7:5
3+
|
4+
LL | / match Some(0) {
5+
LL | | Some(_) => Some(2),
6+
LL | | None::<u32> => None,
7+
LL | | };
8+
| |_____^ help: use map instead: `Some(0).map(|_| 2)`
9+
|
10+
= note: `-D clippy::option-manual-map` implied by `-D warnings`
11+
12+
error: manual implementation of `Option::map`
13+
--> $DIR/option_manual_map.rs:12:5
14+
|
15+
LL | / match Some(0) {
16+
LL | | Some(x) => Some(x + 1),
17+
LL | | _ => None,
18+
LL | | };
19+
| |_____^ help: use map instead: `Some(0).map(|x| x + 1)`
20+
21+
error: manual implementation of `Option::map`
22+
--> $DIR/option_manual_map.rs:17:5
23+
|
24+
LL | / match Some("") {
25+
LL | | Some(x) => Some(x.is_empty()),
26+
LL | | None => None,
27+
LL | | };
28+
| |_____^ help: use map instead: `Some("").map(|x| x.is_empty())`
29+
30+
error: manual implementation of `Option::map`
31+
--> $DIR/option_manual_map.rs:22:5
32+
|
33+
LL | / if let Some(x) = Some(0) {
34+
LL | | Some(!x)
35+
LL | | } else {
36+
LL | | None
37+
LL | | };
38+
| |_____^ help: use map instead: `Some(0).map(|x| !x)`
39+
40+
error: manual implementation of `Option::map`
41+
--> $DIR/option_manual_map.rs:29:5
42+
|
43+
LL | / match Some(0) {
44+
LL | | Some(x) => { Some(std::convert::identity(x)) }
45+
LL | | None => { None }
46+
LL | | };
47+
| |_____^ help: use map instead: `Some(0).map(std::convert::identity)`
48+
49+
error: aborting due to 5 previous errors
50+

0 commit comments

Comments
 (0)