Skip to content

Commit 87b3afc

Browse files
committed
Auto merge of #8696 - J-ZhengLi:issue8492, r=flip1995
check for if-some-or-ok-else-none-or-err fixes: #8492 --- changelog: make [`option_if_let_else`] to check for match expression with both Option and Result; **TODO: Change lint name? Add new lint with similar functionality?**
2 parents 41309df + 1f75845 commit 87b3afc

File tree

4 files changed

+198
-47
lines changed

4 files changed

+198
-47
lines changed

clippy_lints/src/option_if_let_else.rs

Lines changed: 121 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::sugg::Sugg;
3-
use clippy_utils::ty::is_type_diagnostic_item;
43
use clippy_utils::{
54
can_move_expr_to_closure, eager_or_lazy, higher, in_constant, is_else_clause, is_lang_ctor, peel_blocks,
65
peel_hir_expr_while, CaptureKind,
76
};
87
use if_chain::if_chain;
98
use rustc_errors::Applicability;
10-
use rustc_hir::LangItem::OptionSome;
11-
use rustc_hir::{def::Res, BindingAnnotation, Expr, ExprKind, Mutability, PatKind, Path, QPath, UnOp};
9+
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
10+
use rustc_hir::{
11+
def::Res, Arm, BindingAnnotation, Expr, ExprKind, MatchSource, Mutability, Pat, PatKind, Path, QPath, UnOp,
12+
};
1213
use rustc_lint::{LateContext, LateLintPass};
1314
use rustc_session::{declare_lint_pass, declare_tool_lint};
14-
use rustc_span::sym;
1515

1616
declare_clippy_lint! {
1717
/// ### What it does
18-
/// Lints usage of `if let Some(v) = ... { y } else { x }` which is more
18+
/// Lints usage of `if let Some(v) = ... { y } else { x }` and
19+
/// `match .. { Some(v) => y, None/_ => x }` which are more
1920
/// idiomatically done with `Option::map_or` (if the else bit is a pure
2021
/// expression) or `Option::map_or_else` (if the else bit is an impure
2122
/// expression).
@@ -39,6 +40,10 @@ declare_clippy_lint! {
3940
/// } else {
4041
/// 5
4142
/// };
43+
/// let _ = match optional {
44+
/// Some(val) => val + 1,
45+
/// None => 5
46+
/// };
4247
/// let _ = if let Some(foo) = optional {
4348
/// foo
4449
/// } else {
@@ -53,11 +58,14 @@ declare_clippy_lint! {
5358
/// # let optional: Option<u32> = Some(0);
5459
/// # fn do_complicated_function() -> u32 { 5 };
5560
/// let _ = optional.map_or(5, |foo| foo);
61+
/// let _ = optional.map_or(5, |val| val + 1);
5662
/// let _ = optional.map_or_else(||{
5763
/// let y = do_complicated_function();
5864
/// y*y
5965
/// }, |foo| foo);
6066
/// ```
67+
// FIXME: Before moving this lint out of nursery, the lint name needs to be updated. It now also
68+
// covers matches and `Result`.
6169
#[clippy::version = "1.47.0"]
6270
pub OPTION_IF_LET_ELSE,
6371
nursery,
@@ -66,19 +74,21 @@ declare_clippy_lint! {
6674

6775
declare_lint_pass!(OptionIfLetElse => [OPTION_IF_LET_ELSE]);
6876

69-
/// Returns true iff the given expression is the result of calling `Result::ok`
70-
fn is_result_ok(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
71-
if let ExprKind::MethodCall(path, &[ref receiver], _) = &expr.kind {
72-
path.ident.name.as_str() == "ok"
73-
&& is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(receiver), sym::Result)
74-
} else {
75-
false
76-
}
77-
}
78-
79-
/// A struct containing information about occurrences of the
80-
/// `if let Some(..) = .. else` construct that this lint detects.
81-
struct OptionIfLetElseOccurrence {
77+
/// A struct containing information about occurrences of construct that this lint detects
78+
///
79+
/// Such as:
80+
///
81+
/// ```ignore
82+
/// if let Some(..) = {..} else {..}
83+
/// ```
84+
/// or
85+
/// ```ignore
86+
/// match x {
87+
/// Some(..) => {..},
88+
/// None/_ => {..}
89+
/// }
90+
/// ```
91+
struct OptionOccurence {
8292
option: String,
8393
method_sugg: String,
8494
some_expr: String,
@@ -99,43 +109,38 @@ fn format_option_in_sugg(cx: &LateContext<'_>, cond_expr: &Expr<'_>, as_ref: boo
99109
)
100110
}
101111

102-
/// If this expression is the option if let/else construct we're detecting, then
103-
/// this function returns an `OptionIfLetElseOccurrence` struct with details if
104-
/// this construct is found, or None if this construct is not found.
105-
fn detect_option_if_let_else<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<OptionIfLetElseOccurrence> {
112+
fn try_get_option_occurence<'tcx>(
113+
cx: &LateContext<'tcx>,
114+
pat: &Pat<'tcx>,
115+
expr: &Expr<'_>,
116+
if_then: &'tcx Expr<'_>,
117+
if_else: &'tcx Expr<'_>,
118+
) -> Option<OptionOccurence> {
119+
let cond_expr = match expr.kind {
120+
ExprKind::Unary(UnOp::Deref, inner_expr) | ExprKind::AddrOf(_, _, inner_expr) => inner_expr,
121+
_ => expr,
122+
};
123+
let inner_pat = try_get_inner_pat(cx, pat)?;
106124
if_chain! {
107-
if !expr.span.from_expansion(); // Don't lint macros, because it behaves weirdly
108-
if !in_constant(cx, expr.hir_id);
109-
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else: Some(if_else) })
110-
= higher::IfLet::hir(cx, expr);
111-
if !is_else_clause(cx.tcx, expr);
112-
if !is_result_ok(cx, let_expr); // Don't lint on Result::ok because a different lint does it already
113-
if let PatKind::TupleStruct(struct_qpath, [inner_pat], _) = &let_pat.kind;
114-
if is_lang_ctor(cx, struct_qpath, OptionSome);
115-
if let PatKind::Binding(bind_annotation, _, id, None) = &inner_pat.kind;
125+
if let PatKind::Binding(bind_annotation, _, id, None) = inner_pat.kind;
116126
if let Some(some_captures) = can_move_expr_to_closure(cx, if_then);
117127
if let Some(none_captures) = can_move_expr_to_closure(cx, if_else);
118128
if some_captures
119129
.iter()
120130
.filter_map(|(id, &c)| none_captures.get(id).map(|&c2| (c, c2)))
121131
.all(|(x, y)| x.is_imm_ref() && y.is_imm_ref());
122-
123132
then {
124-
let capture_mut = if bind_annotation == &BindingAnnotation::Mutable { "mut " } else { "" };
133+
let capture_mut = if bind_annotation == BindingAnnotation::Mutable { "mut " } else { "" };
125134
let some_body = peel_blocks(if_then);
126135
let none_body = peel_blocks(if_else);
127136
let method_sugg = if eager_or_lazy::switch_to_eager_eval(cx, none_body) { "map_or" } else { "map_or_else" };
128137
let capture_name = id.name.to_ident_string();
129-
let (as_ref, as_mut) = match &let_expr.kind {
138+
let (as_ref, as_mut) = match &expr.kind {
130139
ExprKind::AddrOf(_, Mutability::Not, _) => (true, false),
131140
ExprKind::AddrOf(_, Mutability::Mut, _) => (false, true),
132-
_ => (bind_annotation == &BindingAnnotation::Ref, bind_annotation == &BindingAnnotation::RefMut),
133-
};
134-
let cond_expr = match let_expr.kind {
135-
// Pointer dereferencing happens automatically, so we can omit it in the suggestion
136-
ExprKind::Unary(UnOp::Deref, expr) | ExprKind::AddrOf(_, _, expr) => expr,
137-
_ => let_expr,
141+
_ => (bind_annotation == BindingAnnotation::Ref, bind_annotation == BindingAnnotation::RefMut),
138142
};
143+
139144
// Check if captures the closure will need conflict with borrows made in the scrutinee.
140145
// TODO: check all the references made in the scrutinee expression. This will require interacting
141146
// with the borrow checker. Currently only `<local>[.<field>]*` is checked for.
@@ -154,30 +159,100 @@ fn detect_option_if_let_else<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) ->
154159
}
155160
}
156161
}
157-
Some(OptionIfLetElseOccurrence {
162+
163+
return Some(OptionOccurence {
158164
option: format_option_in_sugg(cx, cond_expr, as_ref, as_mut),
159165
method_sugg: method_sugg.to_string(),
160166
some_expr: format!("|{}{}| {}", capture_mut, capture_name, Sugg::hir_with_macro_callsite(cx, some_body, "..")),
161167
none_expr: format!("{}{}", if method_sugg == "map_or" { "" } else { "|| " }, Sugg::hir_with_macro_callsite(cx, none_body, "..")),
162-
})
168+
});
169+
}
170+
}
171+
172+
None
173+
}
174+
175+
fn try_get_inner_pat<'tcx>(cx: &LateContext<'tcx>, pat: &Pat<'tcx>) -> Option<&'tcx Pat<'tcx>> {
176+
if let PatKind::TupleStruct(ref qpath, [inner_pat], ..) = pat.kind {
177+
if is_lang_ctor(cx, qpath, OptionSome) || is_lang_ctor(cx, qpath, ResultOk) {
178+
return Some(inner_pat);
179+
}
180+
}
181+
None
182+
}
183+
184+
/// If this expression is the option if let/else construct we're detecting, then
185+
/// this function returns an `OptionOccurence` struct with details if
186+
/// this construct is found, or None if this construct is not found.
187+
fn detect_option_if_let_else<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<OptionOccurence> {
188+
if let Some(higher::IfLet {
189+
let_pat,
190+
let_expr,
191+
if_then,
192+
if_else: Some(if_else),
193+
}) = higher::IfLet::hir(cx, expr)
194+
{
195+
if !is_else_clause(cx.tcx, expr) {
196+
return try_get_option_occurence(cx, let_pat, let_expr, if_then, if_else);
197+
}
198+
}
199+
None
200+
}
201+
202+
fn detect_option_match<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<OptionOccurence> {
203+
if let ExprKind::Match(ex, arms, MatchSource::Normal) = expr.kind {
204+
if let Some((let_pat, if_then, if_else)) = try_convert_match(cx, arms) {
205+
return try_get_option_occurence(cx, let_pat, ex, if_then, if_else);
206+
}
207+
}
208+
None
209+
}
210+
211+
fn try_convert_match<'tcx>(
212+
cx: &LateContext<'tcx>,
213+
arms: &[Arm<'tcx>],
214+
) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> {
215+
if arms.len() == 2 {
216+
return if is_none_or_err_arm(cx, &arms[1]) {
217+
Some((arms[0].pat, arms[0].body, arms[1].body))
218+
} else if is_none_or_err_arm(cx, &arms[0]) {
219+
Some((arms[1].pat, arms[1].body, arms[0].body))
163220
} else {
164221
None
165-
}
222+
};
223+
}
224+
None
225+
}
226+
227+
fn is_none_or_err_arm(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
228+
match arm.pat.kind {
229+
PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
230+
PatKind::TupleStruct(ref qpath, [first_pat], _) => {
231+
is_lang_ctor(cx, qpath, ResultErr) && matches!(first_pat.kind, PatKind::Wild)
232+
},
233+
PatKind::Wild => true,
234+
_ => false,
166235
}
167236
}
168237

169238
impl<'tcx> LateLintPass<'tcx> for OptionIfLetElse {
170239
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
171-
if let Some(detection) = detect_option_if_let_else(cx, expr) {
240+
// Don't lint macros and constants
241+
if expr.span.from_expansion() || in_constant(cx, expr.hir_id) {
242+
return;
243+
}
244+
245+
let detection = detect_option_if_let_else(cx, expr).or_else(|| detect_option_match(cx, expr));
246+
if let Some(det) = detection {
172247
span_lint_and_sugg(
173248
cx,
174249
OPTION_IF_LET_ELSE,
175250
expr.span,
176-
format!("use Option::{} instead of an if let/else", detection.method_sugg).as_str(),
251+
format!("use Option::{} instead of an if let/else", det.method_sugg).as_str(),
177252
"try",
178253
format!(
179254
"{}.{}({}, {})",
180-
detection.option, detection.method_sugg, detection.none_expr, detection.some_expr,
255+
det.option, det.method_sugg, det.none_expr, det.some_expr
181256
),
182257
Applicability::MaybeIncorrect,
183258
);

tests/ui/option_if_let_else.fixed

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,13 @@ fn main() {
179179

180180
let _ = pattern_to_vec("hello world");
181181
let _ = complex_subpat();
182+
183+
// issue #8492
184+
let _ = s.map_or(1, |string| string.len());
185+
let _ = Some(10).map_or(5, |a| a + 1);
186+
187+
let res: Result<i32, i32> = Ok(5);
188+
let _ = res.map_or(1, |a| a + 1);
189+
let _ = res.map_or(1, |a| a + 1);
190+
let _ = res.map_or(5, |a| a + 1);
182191
}

tests/ui/option_if_let_else.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,25 @@ fn main() {
208208

209209
let _ = pattern_to_vec("hello world");
210210
let _ = complex_subpat();
211+
212+
// issue #8492
213+
let _ = match s {
214+
Some(string) => string.len(),
215+
None => 1,
216+
};
217+
let _ = match Some(10) {
218+
Some(a) => a + 1,
219+
None => 5,
220+
};
221+
222+
let res: Result<i32, i32> = Ok(5);
223+
let _ = match res {
224+
Ok(a) => a + 1,
225+
_ => 1,
226+
};
227+
let _ = match res {
228+
Err(_) => 1,
229+
Ok(a) => a + 1,
230+
};
231+
let _ = if let Ok(a) = res { a + 1 } else { 5 };
211232
}

tests/ui/option_if_let_else.stderr

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,51 @@ LL + s.len() + x
206206
LL ~ });
207207
|
208208

209-
error: aborting due to 15 previous errors
209+
error: use Option::map_or instead of an if let/else
210+
--> $DIR/option_if_let_else.rs:213:13
211+
|
212+
LL | let _ = match s {
213+
| _____________^
214+
LL | | Some(string) => string.len(),
215+
LL | | None => 1,
216+
LL | | };
217+
| |_____^ help: try: `s.map_or(1, |string| string.len())`
218+
219+
error: use Option::map_or instead of an if let/else
220+
--> $DIR/option_if_let_else.rs:217:13
221+
|
222+
LL | let _ = match Some(10) {
223+
| _____________^
224+
LL | | Some(a) => a + 1,
225+
LL | | None => 5,
226+
LL | | };
227+
| |_____^ help: try: `Some(10).map_or(5, |a| a + 1)`
228+
229+
error: use Option::map_or instead of an if let/else
230+
--> $DIR/option_if_let_else.rs:223:13
231+
|
232+
LL | let _ = match res {
233+
| _____________^
234+
LL | | Ok(a) => a + 1,
235+
LL | | _ => 1,
236+
LL | | };
237+
| |_____^ help: try: `res.map_or(1, |a| a + 1)`
238+
239+
error: use Option::map_or instead of an if let/else
240+
--> $DIR/option_if_let_else.rs:227:13
241+
|
242+
LL | let _ = match res {
243+
| _____________^
244+
LL | | Err(_) => 1,
245+
LL | | Ok(a) => a + 1,
246+
LL | | };
247+
| |_____^ help: try: `res.map_or(1, |a| a + 1)`
248+
249+
error: use Option::map_or instead of an if let/else
250+
--> $DIR/option_if_let_else.rs:231:13
251+
|
252+
LL | let _ = if let Ok(a) = res { a + 1 } else { 5 };
253+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `res.map_or(5, |a| a + 1)`
254+
255+
error: aborting due to 20 previous errors
210256

0 commit comments

Comments
 (0)