Skip to content

Commit 5fd410e

Browse files
committed
option_if_let_else - distinguish pure from impure else expressions
1 parent 4104611 commit 5fd410e

File tree

4 files changed

+88
-28
lines changed

4 files changed

+88
-28
lines changed

clippy_lints/src/option_if_let_else.rs

+39-16
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,26 @@ use crate::utils::{match_type, paths, span_lint_and_sugg};
44
use if_chain::if_chain;
55

66
use rustc_errors::Applicability;
7+
use rustc_hir::def::{DefKind, Res};
78
use rustc_hir::intravisit::{NestedVisitorMap, Visitor};
8-
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, MatchSource, Mutability, PatKind, UnOp};
9+
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, MatchSource, Mutability, PatKind, Path, QPath, UnOp};
910
use rustc_lint::{LateContext, LateLintPass};
1011
use rustc_middle::hir::map::Map;
1112
use rustc_session::{declare_lint_pass, declare_tool_lint};
1213

1314
declare_clippy_lint! {
1415
/// **What it does:**
1516
/// Lints usage of `if let Some(v) = ... { y } else { x }` which is more
16-
/// idiomatically done with `Option::map_or` (if the else bit is a simple
17-
/// expression) or `Option::map_or_else` (if the else bit is a longer
18-
/// block).
17+
/// idiomatically done with `Option::map_or` (if the else bit is a pure
18+
/// expression) or `Option::map_or_else` (if the else bit is an impure
19+
/// expresion).
1920
///
2021
/// **Why is this bad?**
2122
/// Using the dedicated functions of the Option type is clearer and
2223
/// more concise than an if let expression.
2324
///
2425
/// **Known problems:**
25-
/// This lint uses whether the block is just an expression or if it has
26-
/// more statements to decide whether to use `Option::map_or` or
27-
/// `Option::map_or_else`. If you have a single expression which calls
28-
/// an expensive function, then it would be more efficient to use
29-
/// `Option::map_or_else`, but this lint would suggest `Option::map_or`.
30-
///
31-
/// Also, this lint uses a deliberately conservative metric for checking
26+
/// This lint uses a deliberately conservative metric for checking
3227
/// if the inside of either body contains breaks or continues which will
3328
/// cause it to not suggest a fix if either block contains a loop with
3429
/// continues or breaks contained within the loop.
@@ -92,13 +87,15 @@ struct OptionIfLetElseOccurence {
9287
struct ReturnBreakContinueMacroVisitor {
9388
seen_return_break_continue: bool,
9489
}
90+
9591
impl ReturnBreakContinueMacroVisitor {
9692
fn new() -> ReturnBreakContinueMacroVisitor {
9793
ReturnBreakContinueMacroVisitor {
9894
seen_return_break_continue: false,
9995
}
10096
}
10197
}
98+
10299
impl<'tcx> Visitor<'tcx> for ReturnBreakContinueMacroVisitor {
103100
type Map = Map<'tcx>;
104101
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
@@ -157,7 +154,7 @@ fn extract_body_from_arm<'a>(arm: &'a Arm<'a>) -> Option<&'a Expr<'a>> {
157154
}
158155

159156
/// If this is the else body of an if/else expression, then we need to wrap
160-
/// it in curcly braces. Otherwise, we don't.
157+
/// it in curly braces. Otherwise, we don't.
161158
fn should_wrap_in_braces(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
162159
utils::get_enclosing_block(cx, expr.hir_id).map_or(false, |parent| {
163160
if let Some(Expr {
@@ -196,6 +193,35 @@ fn format_option_in_sugg(cx: &LateContext<'_>, cond_expr: &Expr<'_>, as_ref: boo
196193
)
197194
}
198195

196+
/// Is the expr pure (is it free from side-effects)?
197+
/// If yes, we can use `map_or`, else we must use `map_or_else` to preserve semantic equivalence.
198+
/// This implementation can be improved and identify more pure patterns.
199+
fn is_pure(expr: &Expr<'_>) -> bool {
200+
match expr.kind {
201+
ExprKind::Lit(..) | ExprKind::Path(..) => true,
202+
ExprKind::AddrOf(_, _, addr_of_expr) => is_pure(addr_of_expr),
203+
ExprKind::Tup(tup_exprs) => tup_exprs.iter().all(|expr| is_pure(expr)),
204+
ExprKind::Struct(_, fields, expr) => {
205+
fields.iter().all(|f| is_pure(f.expr)) && expr.map_or(true, |e| is_pure(e))
206+
},
207+
ExprKind::Call(
208+
&Expr {
209+
kind:
210+
ExprKind::Path(QPath::Resolved(
211+
_,
212+
Path {
213+
res: Res::Def(DefKind::Ctor(..) | DefKind::Variant, ..),
214+
..
215+
},
216+
)),
217+
..
218+
},
219+
args,
220+
) => args.iter().all(|expr| is_pure(expr)),
221+
_ => false,
222+
}
223+
}
224+
199225
/// If this expression is the option if let/else construct we're detecting, then
200226
/// this function returns an `OptionIfLetElseOccurence` struct with details if
201227
/// this construct is found, or None if this construct is not found.
@@ -214,10 +240,7 @@ fn detect_option_if_let_else(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Op
214240
let capture_mut = if bind_annotation == &BindingAnnotation::Mutable { "mut " } else { "" };
215241
let some_body = extract_body_from_arm(&arms[0])?;
216242
let none_body = extract_body_from_arm(&arms[1])?;
217-
let method_sugg = match &none_body.kind {
218-
ExprKind::Block(..) => "map_or_else",
219-
_ => "map_or",
220-
};
243+
let method_sugg = if is_pure(none_body) { "map_or" } else { "map_or_else" };
221244
let capture_name = id.name.to_ident_string();
222245
let wrap_braces = should_wrap_in_braces(cx, expr);
223246
let (as_ref, as_mut) = match &cond_expr.kind {

tests/ui/option_if_let_else.fixed

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// run-rustfix
22
#![warn(clippy::option_if_let_else)]
3+
#![allow(clippy::redundant_closure)]
34

45
fn bad1(string: Option<&str>) -> (bool, &str) {
56
string.map_or((false, "hello"), |x| (true, x))
@@ -36,6 +37,14 @@ fn longer_body(arg: Option<u32>) -> u32 {
3637
})
3738
}
3839

40+
fn impure_else(arg: Option<i32>) {
41+
let side_effect = || {
42+
println!("return 1");
43+
1
44+
};
45+
let _ = arg.map_or_else(|| side_effect(), |x| x);
46+
}
47+
3948
fn test_map_or_else(arg: Option<u32>) {
4049
let _ = arg.map_or_else(|| {
4150
let mut y = 1;
@@ -71,4 +80,5 @@ fn main() {
7180
let _ = longer_body(None);
7281
test_map_or_else(None);
7382
let _ = negative_tests(None);
83+
let _ = impure_else(None);
7484
}

tests/ui/option_if_let_else.rs

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// run-rustfix
22
#![warn(clippy::option_if_let_else)]
3+
#![allow(clippy::redundant_closure)]
34

45
fn bad1(string: Option<&str>) -> (bool, &str) {
56
if let Some(x) = string {
@@ -52,6 +53,19 @@ fn longer_body(arg: Option<u32>) -> u32 {
5253
}
5354
}
5455

56+
fn impure_else(arg: Option<i32>) {
57+
let side_effect = || {
58+
println!("return 1");
59+
1
60+
};
61+
let _ = if let Some(x) = arg {
62+
x
63+
} else {
64+
// map_or_else must be suggested
65+
side_effect()
66+
};
67+
}
68+
5569
fn test_map_or_else(arg: Option<u32>) {
5670
let _ = if let Some(x) = arg {
5771
x * x * x * x
@@ -89,4 +103,5 @@ fn main() {
89103
let _ = longer_body(None);
90104
test_map_or_else(None);
91105
let _ = negative_tests(None);
106+
let _ = impure_else(None);
92107
}

tests/ui/option_if_let_else.stderr

+24-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:5:5
2+
--> $DIR/option_if_let_else.rs:6:5
33
|
44
LL | / if let Some(x) = string {
55
LL | | (true, x)
@@ -11,7 +11,7 @@ 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:15:12
14+
--> $DIR/option_if_let_else.rs:16:12
1515
|
1616
LL | } else if let Some(x) = string {
1717
| ____________^
@@ -22,19 +22,19 @@ LL | | }
2222
| |_____^ help: try: `{ string.map_or(Some((false, "")), |x| Some((true, x))) }`
2323

2424
error: use Option::map_or instead of an if let/else
25-
--> $DIR/option_if_let_else.rs:23:13
25+
--> $DIR/option_if_let_else.rs:24:13
2626
|
2727
LL | let _ = if let Some(s) = *string { s.len() } else { 0 };
2828
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `string.map_or(0, |s| s.len())`
2929

3030
error: use Option::map_or instead of an if let/else
31-
--> $DIR/option_if_let_else.rs:24:13
31+
--> $DIR/option_if_let_else.rs:25:13
3232
|
3333
LL | let _ = if let Some(s) = &num { s } else { &0 };
3434
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `num.as_ref().map_or(&0, |s| s)`
3535

3636
error: use Option::map_or instead of an if let/else
37-
--> $DIR/option_if_let_else.rs:25:13
37+
--> $DIR/option_if_let_else.rs:26:13
3838
|
3939
LL | let _ = if let Some(s) = &mut num {
4040
| _____________^
@@ -54,13 +54,13 @@ LL | });
5454
|
5555

5656
error: use Option::map_or instead of an if let/else
57-
--> $DIR/option_if_let_else.rs:31:13
57+
--> $DIR/option_if_let_else.rs:32:13
5858
|
5959
LL | let _ = if let Some(ref s) = num { s } else { &0 };
6060
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `num.as_ref().map_or(&0, |s| s)`
6161

6262
error: use Option::map_or instead of an if let/else
63-
--> $DIR/option_if_let_else.rs:32:13
63+
--> $DIR/option_if_let_else.rs:33:13
6464
|
6565
LL | let _ = if let Some(mut s) = num {
6666
| _____________^
@@ -80,7 +80,7 @@ LL | });
8080
|
8181

8282
error: use Option::map_or instead of an if let/else
83-
--> $DIR/option_if_let_else.rs:38:13
83+
--> $DIR/option_if_let_else.rs:39:13
8484
|
8585
LL | let _ = if let Some(ref mut s) = num {
8686
| _____________^
@@ -100,7 +100,7 @@ LL | });
100100
|
101101

102102
error: use Option::map_or instead of an if let/else
103-
--> $DIR/option_if_let_else.rs:47:5
103+
--> $DIR/option_if_let_else.rs:48:5
104104
|
105105
LL | / if let Some(x) = arg {
106106
LL | | let y = x * x;
@@ -119,7 +119,19 @@ LL | })
119119
|
120120

121121
error: use Option::map_or_else instead of an if let/else
122-
--> $DIR/option_if_let_else.rs:56:13
122+
--> $DIR/option_if_let_else.rs:61:13
123+
|
124+
LL | let _ = if let Some(x) = arg {
125+
| _____________^
126+
LL | | x
127+
LL | | } else {
128+
LL | | // map_or_else must be suggested
129+
LL | | side_effect()
130+
LL | | };
131+
| |_____^ help: try: `arg.map_or_else(|| side_effect(), |x| x)`
132+
133+
error: use Option::map_or_else instead of an if let/else
134+
--> $DIR/option_if_let_else.rs:70:13
123135
|
124136
LL | let _ = if let Some(x) = arg {
125137
| _____________^
@@ -142,10 +154,10 @@ LL | }, |x| x * x * x * x);
142154
|
143155

144156
error: use Option::map_or instead of an if let/else
145-
--> $DIR/option_if_let_else.rs:85:13
157+
--> $DIR/option_if_let_else.rs:99:13
146158
|
147159
LL | let _ = if let Some(x) = optional { x + 2 } else { 5 };
148160
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `optional.map_or(5, |x| x + 2)`
149161

150-
error: aborting due to 11 previous errors
162+
error: aborting due to 12 previous errors
151163

0 commit comments

Comments
 (0)