Skip to content

Commit e6febbd

Browse files
authored
Add lint unnecessary_option_map_or_else (rust-lang#14662)
changelog: [`unnecessary_option_map_or_else`]: Added lint unnecessary_option_map_or_else. As suggested in the issue description, the implementation takes as reference the issue rust-lang/rust-clippy#7328. The tests for lints `option_if_let_else` and `or_fun_call` needed to be adjusted to comply with new lint. fixes rust-lang/rust-clippy#14588
2 parents 99b8106 + e410d6f commit e6febbd

13 files changed

+428
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6897,6 +6897,7 @@ Released 2018-09-13
68976897
[`unnecessary_min_or_max`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_min_or_max
68986898
[`unnecessary_mut_passed`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_mut_passed
68996899
[`unnecessary_operation`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_operation
6900+
[`unnecessary_option_map_or_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_option_map_or_else
69006901
[`unnecessary_owned_empty_strings`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_owned_empty_strings
69016902
[`unnecessary_result_map_or_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_result_map_or_else
69026903
[`unnecessary_safety_comment`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_safety_comment

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
488488
crate::methods::UNNECESSARY_LITERAL_UNWRAP_INFO,
489489
crate::methods::UNNECESSARY_MAP_OR_INFO,
490490
crate::methods::UNNECESSARY_MIN_OR_MAX_INFO,
491+
crate::methods::UNNECESSARY_OPTION_MAP_OR_ELSE_INFO,
491492
crate::methods::UNNECESSARY_RESULT_MAP_OR_ELSE_INFO,
492493
crate::methods::UNNECESSARY_SORT_BY_INFO,
493494
crate::methods::UNNECESSARY_TO_OWNED_INFO,

clippy_lints/src/methods/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ mod unnecessary_lazy_eval;
133133
mod unnecessary_literal_unwrap;
134134
mod unnecessary_map_or;
135135
mod unnecessary_min_or_max;
136+
mod unnecessary_option_map_or_else;
136137
mod unnecessary_result_map_or_else;
137138
mod unnecessary_sort_by;
138139
mod unnecessary_to_owned;
@@ -4639,6 +4640,30 @@ declare_clippy_lint! {
46394640
"detects redundant calls to `Iterator::cloned`"
46404641
}
46414642

4643+
declare_clippy_lint! {
4644+
/// Checks for usage of `.map_or_else()` "map closure" for `Option` type.
4645+
///
4646+
/// ### Why is this bad?
4647+
/// This can be written more concisely by using `unwrap_or_else()`.
4648+
///
4649+
/// ### Example
4650+
/// ```no_run
4651+
/// let k = 10;
4652+
/// let x: Option<u32> = Some(4);
4653+
/// let y = x.map_or_else(|| 2 * k, |n| n);
4654+
/// ```
4655+
/// Use instead:
4656+
/// ```no_run
4657+
/// let k = 10;
4658+
/// let x: Option<u32> = Some(4);
4659+
/// let y = x.unwrap_or_else(|| 2 * k);
4660+
/// ```
4661+
#[clippy::version = "1.88.0"]
4662+
pub UNNECESSARY_OPTION_MAP_OR_ELSE,
4663+
suspicious,
4664+
"making no use of the \"map closure\" when calling `.map_or_else(|| 2 * k, |n| n)`"
4665+
}
4666+
46424667
#[expect(clippy::struct_excessive_bools)]
46434668
pub struct Methods {
46444669
avoid_breaking_exported_api: bool,
@@ -4820,6 +4845,7 @@ impl_lint_pass!(Methods => [
48204845
SWAP_WITH_TEMPORARY,
48214846
IP_CONSTANT,
48224847
REDUNDANT_ITER_CLONED,
4848+
UNNECESSARY_OPTION_MAP_OR_ELSE,
48234849
]);
48244850

48254851
/// Extracts a method call name, args, and `Span` of the method name.
@@ -5275,6 +5301,7 @@ impl Methods {
52755301
},
52765302
(sym::map_or_else, [def, map]) => {
52775303
result_map_or_else_none::check(cx, expr, recv, def, map);
5304+
unnecessary_option_map_or_else::check(cx, expr, recv, def, map);
52785305
unnecessary_result_map_or_else::check(cx, expr, recv, def, map);
52795306
},
52805307
(sym::next, []) => {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::source::snippet_with_applicability;
3+
use clippy_utils::ty::is_type_diagnostic_item;
4+
use clippy_utils::{expr_or_init, find_binding_init, peel_blocks};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::def::{DefKind, Res};
7+
use rustc_hir::{Body, BodyId, Closure, Expr, ExprKind, HirId, QPath};
8+
use rustc_lint::LateContext;
9+
use rustc_span::symbol::sym;
10+
11+
use super::UNNECESSARY_OPTION_MAP_OR_ELSE;
12+
use super::utils::get_last_chain_binding_hir_id;
13+
14+
fn emit_lint(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, def_arg: &Expr<'_>) {
15+
let msg = "unused \"map closure\" when calling `Option::map_or_else` value";
16+
let mut applicability = Applicability::MachineApplicable;
17+
let self_snippet = snippet_with_applicability(cx, recv.span, "_", &mut applicability);
18+
let err_snippet = snippet_with_applicability(cx, def_arg.span, "..", &mut applicability);
19+
span_lint_and_sugg(
20+
cx,
21+
UNNECESSARY_OPTION_MAP_OR_ELSE,
22+
expr.span,
23+
msg,
24+
"consider using `unwrap_or_else`",
25+
format!("{self_snippet}.unwrap_or_else({err_snippet})"),
26+
Applicability::MachineApplicable,
27+
);
28+
}
29+
30+
fn handle_qpath(
31+
cx: &LateContext<'_>,
32+
expr: &Expr<'_>,
33+
recv: &Expr<'_>,
34+
def_arg: &Expr<'_>,
35+
expected_hir_id: HirId,
36+
qpath: QPath<'_>,
37+
) {
38+
if let QPath::Resolved(_, path) = qpath
39+
&& let Res::Local(hir_id) = path.res
40+
&& expected_hir_id == hir_id
41+
{
42+
emit_lint(cx, expr, recv, def_arg);
43+
}
44+
}
45+
46+
fn handle_closure(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, def_arg: &Expr<'_>, body_id: BodyId) {
47+
let body = cx.tcx.hir_body(body_id);
48+
handle_fn_body(cx, expr, recv, def_arg, body);
49+
}
50+
51+
fn handle_fn_body(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, def_arg: &Expr<'_>, body: &Body<'_>) {
52+
if let Some(first_param) = body.params.first() {
53+
let body_expr = peel_blocks(body.value);
54+
match body_expr.kind {
55+
ExprKind::Path(qpath) => {
56+
handle_qpath(cx, expr, recv, def_arg, first_param.pat.hir_id, qpath);
57+
},
58+
// If this is a block (that wasn't peeled off), then it means there are statements.
59+
ExprKind::Block(block, _) => {
60+
if let Some(block_expr) = block.expr
61+
// First we ensure that this is a "binding chain" (each statement is a binding
62+
// of the previous one) and that it is a binding of the closure argument.
63+
&& let Some(last_chain_binding_id) =
64+
get_last_chain_binding_hir_id(first_param.pat.hir_id, block.stmts)
65+
&& let ExprKind::Path(qpath) = block_expr.kind
66+
{
67+
handle_qpath(cx, expr, recv, def_arg, last_chain_binding_id, qpath);
68+
}
69+
},
70+
_ => {},
71+
}
72+
}
73+
}
74+
75+
/// lint use of `_.map_or_else(|err| err, |n| n)` for `Option`s.
76+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, def_arg: &Expr<'_>, map_arg: &Expr<'_>) {
77+
// lint if the caller of `map_or_else()` is an `Option`
78+
if !is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option) {
79+
return;
80+
}
81+
match map_arg.kind {
82+
// If the second argument is a closure, we can check its body.
83+
ExprKind::Closure(&Closure { body, .. }) => {
84+
handle_closure(cx, expr, recv, def_arg, body);
85+
},
86+
ExprKind::Path(qpath) => {
87+
let res = cx.qpath_res(&qpath, map_arg.hir_id);
88+
match res {
89+
// Case 1: Local variable (could be a closure)
90+
Res::Local(hir_id) => {
91+
if let Some(init_expr) = find_binding_init(cx, hir_id) {
92+
let origin = expr_or_init(cx, init_expr);
93+
if let ExprKind::Closure(&Closure { body, .. }) = origin.kind {
94+
handle_closure(cx, expr, recv, def_arg, body);
95+
}
96+
}
97+
},
98+
// Case 2: Function definition
99+
Res::Def(DefKind::Fn, def_id) => {
100+
if let Some(local_def_id) = def_id.as_local()
101+
&& let Some(body) = cx.tcx.hir_maybe_body_owned_by(local_def_id)
102+
{
103+
handle_fn_body(cx, expr, recv, def_arg, body);
104+
}
105+
},
106+
_ => (),
107+
}
108+
},
109+
_ => (),
110+
}
111+
}

tests/ui/option_if_let_else.fixed

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
clippy::redundant_locals,
77
clippy::manual_midpoint,
88
clippy::manual_unwrap_or_default,
9-
clippy::manual_unwrap_or
9+
clippy::manual_unwrap_or,
10+
clippy::unnecessary_option_map_or_else
1011
)]
1112

1213
fn bad1(string: Option<&str>) -> (bool, &str) {

tests/ui/option_if_let_else.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
clippy::redundant_locals,
77
clippy::manual_midpoint,
88
clippy::manual_unwrap_or_default,
9-
clippy::manual_unwrap_or
9+
clippy::manual_unwrap_or,
10+
clippy::unnecessary_option_map_or_else
1011
)]
1112

1213
fn bad1(string: Option<&str>) -> (bool, &str) {

0 commit comments

Comments
 (0)