Skip to content

Commit fd30241

Browse files
committed
Auto merge of #7573 - Jarcho:option_if_let_else, r=giraffate
Fix `option_if_let_else` fixes: #5822 fixes: #6737 fixes: #7567 The inference from #6137 still exists so I'm not sure if this should be moved from the nursery. Before doing that though I'd almost want to see this split into two lints. One suggesting `map_or` and the other suggesting `map_or_else`. `map_or_else` tends to have longer expressions for both branches so it doesn't end up much shorter than a match expression in practice. It also seems most people find it harder to read. `map_or` at least has the terseness benefit of being on one line most of the time, especially when the `None` branch is just a literal or path expression. changelog: `break` and `continue` statments local to the would-be closure are allowed in `option_if_let_else` changelog: don't lint in const contexts in `option_if_let_else` changelog: don't lint when yield expressions are used in `option_if_let_else` changelog: don't lint when the captures made by the would-be closure conflict with the other branch in `option_if_let_else` changelog: don't lint when a field of a local is used when the type could be pontentially moved from in `option_if_let_else` changelog: in some cases, don't lint when scrutinee expression conflicts with the captures of the would-be closure in `option_if_let_else`
2 parents 5e1e9b0 + 3e5dcba commit fd30241

File tree

5 files changed

+237
-23
lines changed

5 files changed

+237
-23
lines changed

clippy_lints/src/option_if_let_else.rs

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::sugg::Sugg;
33
use clippy_utils::ty::is_type_diagnostic_item;
4-
use clippy_utils::usage::contains_return_break_continue_macro;
5-
use clippy_utils::{eager_or_lazy, in_macro, is_else_clause, is_lang_ctor};
4+
use clippy_utils::{
5+
can_move_expr_to_closure, eager_or_lazy, in_constant, in_macro, is_else_clause, is_lang_ctor, peel_hir_expr_while,
6+
CaptureKind,
7+
};
68
use if_chain::if_chain;
79
use rustc_errors::Applicability;
810
use rustc_hir::LangItem::OptionSome;
9-
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, MatchSource, Mutability, PatKind, UnOp};
11+
use rustc_hir::{
12+
def::Res, Arm, BindingAnnotation, Block, Expr, ExprKind, MatchSource, Mutability, PatKind, Path, QPath, UnOp,
13+
};
1014
use rustc_lint::{LateContext, LateLintPass};
1115
use rustc_session::{declare_lint_pass, declare_tool_lint};
1216
use rustc_span::sym;
@@ -127,21 +131,30 @@ fn detect_option_if_let_else<'tcx>(
127131
) -> Option<OptionIfLetElseOccurence> {
128132
if_chain! {
129133
if !in_macro(expr.span); // Don't lint macros, because it behaves weirdly
130-
if let ExprKind::Match(cond_expr, arms, MatchSource::IfLetDesugar{contains_else_clause: true}) = &expr.kind;
134+
if !in_constant(cx, expr.hir_id);
135+
if let ExprKind::Match(cond_expr, [some_arm, none_arm], MatchSource::IfLetDesugar{contains_else_clause: true})
136+
= &expr.kind;
131137
if !is_else_clause(cx.tcx, expr);
132-
if arms.len() == 2;
133138
if !is_result_ok(cx, cond_expr); // Don't lint on Result::ok because a different lint does it already
134-
if let PatKind::TupleStruct(struct_qpath, [inner_pat], _) = &arms[0].pat.kind;
139+
if let PatKind::TupleStruct(struct_qpath, [inner_pat], _) = &some_arm.pat.kind;
135140
if is_lang_ctor(cx, struct_qpath, OptionSome);
136141
if let PatKind::Binding(bind_annotation, _, id, _) = &inner_pat.kind;
137-
if !contains_return_break_continue_macro(arms[0].body);
138-
if !contains_return_break_continue_macro(arms[1].body);
142+
if let Some(some_captures) = can_move_expr_to_closure(cx, some_arm.body);
143+
if let Some(none_captures) = can_move_expr_to_closure(cx, none_arm.body);
144+
if some_captures
145+
.iter()
146+
.filter_map(|(id, &c)| none_captures.get(id).map(|&c2| (c, c2)))
147+
.all(|(x, y)| x.is_imm_ref() && y.is_imm_ref());
139148

140149
then {
141150
let capture_mut = if bind_annotation == &BindingAnnotation::Mutable { "mut " } else { "" };
142-
let some_body = extract_body_from_arm(&arms[0])?;
143-
let none_body = extract_body_from_arm(&arms[1])?;
144-
let method_sugg = if eager_or_lazy::is_eagerness_candidate(cx, none_body) { "map_or" } else { "map_or_else" };
151+
let some_body = extract_body_from_arm(some_arm)?;
152+
let none_body = extract_body_from_arm(none_arm)?;
153+
let method_sugg = if eager_or_lazy::is_eagerness_candidate(cx, none_body) {
154+
"map_or"
155+
} else {
156+
"map_or_else"
157+
};
145158
let capture_name = id.name.to_ident_string();
146159
let (as_ref, as_mut) = match &cond_expr.kind {
147160
ExprKind::AddrOf(_, Mutability::Not, _) => (true, false),
@@ -153,6 +166,24 @@ fn detect_option_if_let_else<'tcx>(
153166
ExprKind::Unary(UnOp::Deref, expr) | ExprKind::AddrOf(_, _, expr) => expr,
154167
_ => cond_expr,
155168
};
169+
// Check if captures the closure will need conflict with borrows made in the scrutinee.
170+
// TODO: check all the references made in the scrutinee expression. This will require interacting
171+
// with the borrow checker. Currently only `<local>[.<field>]*` is checked for.
172+
if as_ref || as_mut {
173+
let e = peel_hir_expr_while(cond_expr, |e| match e.kind {
174+
ExprKind::Field(e, _) | ExprKind::AddrOf(_, _, e) => Some(e),
175+
_ => None,
176+
});
177+
if let ExprKind::Path(QPath::Resolved(None, Path { res: Res::Local(local_id), .. })) = e.kind {
178+
match some_captures.get(local_id)
179+
.or_else(|| (method_sugg == "map_or_else").then(|| ()).and_then(|_| none_captures.get(local_id)))
180+
{
181+
Some(CaptureKind::Value | CaptureKind::Ref(Mutability::Mut)) => return None,
182+
Some(CaptureKind::Ref(Mutability::Not)) if as_mut => return None,
183+
Some(CaptureKind::Ref(Mutability::Not)) | None => (),
184+
}
185+
}
186+
}
156187
Some(OptionIfLetElseOccurence {
157188
option: format_option_in_sugg(cx, cond_expr, as_ref, as_mut),
158189
method_sugg: method_sugg.to_string(),

clippy_utils/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,11 @@ pub enum CaptureKind {
710710
Value,
711711
Ref(Mutability),
712712
}
713+
impl CaptureKind {
714+
pub fn is_imm_ref(self) -> bool {
715+
self == Self::Ref(Mutability::Not)
716+
}
717+
}
713718
impl std::ops::BitOr for CaptureKind {
714719
type Output = Self;
715720
fn bitor(self, rhs: Self) -> Self::Output {

tests/ui/option_if_let_else.fixed

+62
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// edition:2018
12
// run-rustfix
23
#![warn(clippy::option_if_let_else)]
34
#![allow(clippy::redundant_closure)]
@@ -86,4 +87,65 @@ fn main() {
8687
test_map_or_else(None);
8788
let _ = negative_tests(None);
8889
let _ = impure_else(None);
90+
91+
let _ = Some(0).map_or(0, |x| loop {
92+
if x == 0 {
93+
break x;
94+
}
95+
});
96+
97+
// #7576
98+
const fn _f(x: Option<u32>) -> u32 {
99+
// Don't lint, `map_or` isn't const
100+
if let Some(x) = x { x } else { 10 }
101+
}
102+
103+
// #5822
104+
let s = String::new();
105+
// Don't lint, `Some` branch consumes `s`, but else branch uses `s`
106+
let _ = if let Some(x) = Some(0) {
107+
let s = s;
108+
s.len() + x
109+
} else {
110+
s.len()
111+
};
112+
113+
let s = String::new();
114+
// Lint, both branches immutably borrow `s`.
115+
let _ = Some(0).map_or_else(|| s.len(), |x| s.len() + x);
116+
117+
let s = String::new();
118+
// Lint, `Some` branch consumes `s`, but else branch doesn't use `s`.
119+
let _ = Some(0).map_or(1, |x| {
120+
let s = s;
121+
s.len() + x
122+
});
123+
124+
let s = Some(String::new());
125+
// Don't lint, `Some` branch borrows `s`, but else branch consumes `s`
126+
let _ = if let Some(x) = &s {
127+
x.len()
128+
} else {
129+
let _s = s;
130+
10
131+
};
132+
133+
let mut s = Some(String::new());
134+
// Don't lint, `Some` branch mutably borrows `s`, but else branch also borrows `s`
135+
let _ = if let Some(x) = &mut s {
136+
x.push_str("test");
137+
x.len()
138+
} else {
139+
let _s = &s;
140+
10
141+
};
142+
143+
async fn _f1(x: u32) -> u32 {
144+
x
145+
}
146+
147+
async fn _f2() {
148+
// Don't lint. `await` can't be moved into a closure.
149+
let _ = if let Some(x) = Some(0) { _f1(x).await } else { 0 };
150+
}
89151
}

tests/ui/option_if_let_else.rs

+68
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// edition:2018
12
// run-rustfix
23
#![warn(clippy::option_if_let_else)]
34
#![allow(clippy::redundant_closure)]
@@ -105,4 +106,71 @@ fn main() {
105106
test_map_or_else(None);
106107
let _ = negative_tests(None);
107108
let _ = impure_else(None);
109+
110+
let _ = if let Some(x) = Some(0) {
111+
loop {
112+
if x == 0 {
113+
break x;
114+
}
115+
}
116+
} else {
117+
0
118+
};
119+
120+
// #7576
121+
const fn _f(x: Option<u32>) -> u32 {
122+
// Don't lint, `map_or` isn't const
123+
if let Some(x) = x { x } else { 10 }
124+
}
125+
126+
// #5822
127+
let s = String::new();
128+
// Don't lint, `Some` branch consumes `s`, but else branch uses `s`
129+
let _ = if let Some(x) = Some(0) {
130+
let s = s;
131+
s.len() + x
132+
} else {
133+
s.len()
134+
};
135+
136+
let s = String::new();
137+
// Lint, both branches immutably borrow `s`.
138+
let _ = if let Some(x) = Some(0) { s.len() + x } else { s.len() };
139+
140+
let s = String::new();
141+
// Lint, `Some` branch consumes `s`, but else branch doesn't use `s`.
142+
let _ = if let Some(x) = Some(0) {
143+
let s = s;
144+
s.len() + x
145+
} else {
146+
1
147+
};
148+
149+
let s = Some(String::new());
150+
// Don't lint, `Some` branch borrows `s`, but else branch consumes `s`
151+
let _ = if let Some(x) = &s {
152+
x.len()
153+
} else {
154+
let _s = s;
155+
10
156+
};
157+
158+
let mut s = Some(String::new());
159+
// Don't lint, `Some` branch mutably borrows `s`, but else branch also borrows `s`
160+
let _ = if let Some(x) = &mut s {
161+
x.push_str("test");
162+
x.len()
163+
} else {
164+
let _s = &s;
165+
10
166+
};
167+
168+
async fn _f1(x: u32) -> u32 {
169+
x
170+
}
171+
172+
async fn _f2() {
173+
// Don't lint. `await` can't be moved into a closure.
174+
let _ = if let Some(x) = Some(0) { _f1(x).await } else { 0 };
175+
}
108176
}

tests/ui/option_if_let_else.stderr

+60-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: use Option::map_or instead of an if let/else
2-
--> $DIR/option_if_let_else.rs:7:5
2+
--> $DIR/option_if_let_else.rs:8:5
33
|
44
LL | / if let Some(x) = string {
55
LL | | (true, x)
@@ -11,19 +11,19 @@ LL | | }
1111
= note: `-D clippy::option-if-let-else` implied by `-D warnings`
1212

1313
error: use Option::map_or instead of an if let/else
14-
--> $DIR/option_if_let_else.rs:25:13
14+
--> $DIR/option_if_let_else.rs:26:13
1515
|
1616
LL | let _ = if let Some(s) = *string { s.len() } else { 0 };
1717
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `string.map_or(0, |s| s.len())`
1818

1919
error: use Option::map_or instead of an if let/else
20-
--> $DIR/option_if_let_else.rs:26:13
20+
--> $DIR/option_if_let_else.rs:27:13
2121
|
2222
LL | let _ = if let Some(s) = &num { s } else { &0 };
2323
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `num.as_ref().map_or(&0, |s| s)`
2424

2525
error: use Option::map_or instead of an if let/else
26-
--> $DIR/option_if_let_else.rs:27:13
26+
--> $DIR/option_if_let_else.rs:28:13
2727
|
2828
LL | let _ = if let Some(s) = &mut num {
2929
| _____________^
@@ -43,13 +43,13 @@ LL ~ });
4343
|
4444

4545
error: use Option::map_or instead of an if let/else
46-
--> $DIR/option_if_let_else.rs:33:13
46+
--> $DIR/option_if_let_else.rs:34:13
4747
|
4848
LL | let _ = if let Some(ref s) = num { s } else { &0 };
4949
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `num.as_ref().map_or(&0, |s| s)`
5050

5151
error: use Option::map_or instead of an if let/else
52-
--> $DIR/option_if_let_else.rs:34:13
52+
--> $DIR/option_if_let_else.rs:35:13
5353
|
5454
LL | let _ = if let Some(mut s) = num {
5555
| _____________^
@@ -69,7 +69,7 @@ LL ~ });
6969
|
7070

7171
error: use Option::map_or instead of an if let/else
72-
--> $DIR/option_if_let_else.rs:40:13
72+
--> $DIR/option_if_let_else.rs:41:13
7373
|
7474
LL | let _ = if let Some(ref mut s) = num {
7575
| _____________^
@@ -89,7 +89,7 @@ LL ~ });
8989
|
9090

9191
error: use Option::map_or instead of an if let/else
92-
--> $DIR/option_if_let_else.rs:49:5
92+
--> $DIR/option_if_let_else.rs:50:5
9393
|
9494
LL | / if let Some(x) = arg {
9595
LL | | let y = x * x;
@@ -108,7 +108,7 @@ LL + })
108108
|
109109

110110
error: use Option::map_or_else instead of an if let/else
111-
--> $DIR/option_if_let_else.rs:62:13
111+
--> $DIR/option_if_let_else.rs:63:13
112112
|
113113
LL | let _ = if let Some(x) = arg {
114114
| _____________^
@@ -120,7 +120,7 @@ LL | | };
120120
| |_____^ help: try: `arg.map_or_else(|| side_effect(), |x| x)`
121121

122122
error: use Option::map_or_else instead of an if let/else
123-
--> $DIR/option_if_let_else.rs:71:13
123+
--> $DIR/option_if_let_else.rs:72:13
124124
|
125125
LL | let _ = if let Some(x) = arg {
126126
| _____________^
@@ -143,10 +143,58 @@ LL ~ }, |x| x * x * x * x);
143143
|
144144

145145
error: use Option::map_or instead of an if let/else
146-
--> $DIR/option_if_let_else.rs:100:13
146+
--> $DIR/option_if_let_else.rs:101:13
147147
|
148148
LL | let _ = if let Some(x) = optional { x + 2 } else { 5 };
149149
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `optional.map_or(5, |x| x + 2)`
150150

151-
error: aborting due to 11 previous errors
151+
error: use Option::map_or instead of an if let/else
152+
--> $DIR/option_if_let_else.rs:110:13
153+
|
154+
LL | let _ = if let Some(x) = Some(0) {
155+
| _____________^
156+
LL | | loop {
157+
LL | | if x == 0 {
158+
LL | | break x;
159+
... |
160+
LL | | 0
161+
LL | | };
162+
| |_____^
163+
|
164+
help: try
165+
|
166+
LL ~ let _ = Some(0).map_or(0, |x| loop {
167+
LL + if x == 0 {
168+
LL + break x;
169+
LL + }
170+
LL ~ });
171+
|
172+
173+
error: use Option::map_or_else instead of an if let/else
174+
--> $DIR/option_if_let_else.rs:138:13
175+
|
176+
LL | let _ = if let Some(x) = Some(0) { s.len() + x } else { s.len() };
177+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Some(0).map_or_else(|| s.len(), |x| s.len() + x)`
178+
179+
error: use Option::map_or instead of an if let/else
180+
--> $DIR/option_if_let_else.rs:142:13
181+
|
182+
LL | let _ = if let Some(x) = Some(0) {
183+
| _____________^
184+
LL | | let s = s;
185+
LL | | s.len() + x
186+
LL | | } else {
187+
LL | | 1
188+
LL | | };
189+
| |_____^
190+
|
191+
help: try
192+
|
193+
LL ~ let _ = Some(0).map_or(1, |x| {
194+
LL + let s = s;
195+
LL + s.len() + x
196+
LL ~ });
197+
|
198+
199+
error: aborting due to 14 previous errors
152200

0 commit comments

Comments
 (0)