Skip to content

Commit ed748d3

Browse files
committed
Auto merge of #5301 - JarredAllen:option_if_let_else, r=flip1995
Suggest `Option::map_or`(_else) for `if let Some { y } else { x }` Fixes #5203 There are two issues with this code that I have noticed: - Use of `Option::map_or` causes it to always evaluate the code in the else block. If that block is computationally expensive or if it updates some state (such as getting the next value from an iterator), then this change would cause the code to behave differently. In either of those circumstances, it should suggest Option::map_or_else, which takes both cases as a closure and runs one. However, I don't know how to check if the expression would change some state, so I left the lint's applicability as MaybeIncorrect. - There are lints which can trigger on specific sub-cases of this lint (`if_let_some_result`, `question_mark`, and `while_let_loop`) and suggest different changes (usually better ones because they're more specific). Is this acceptable for clippy to give multiple suggestions, or should I have the code check if those other lints trigger and then not trigger this lint if they do?
2 parents f1fb815 + ff4da34 commit ed748d3

File tree

7 files changed

+381
-0
lines changed

7 files changed

+381
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@ Released 2018-09-13
14081408
[`option_as_ref_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_as_ref_deref
14091409
[`option_env_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_env_unwrap
14101410
[`option_expect_used`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_expect_used
1411+
[`option_if_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
14111412
[`option_map_or_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_or_none
14121413
[`option_map_unit_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unit_fn
14131414
[`option_map_unwrap_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unwrap_or

clippy_lints/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ mod non_copy_const;
278278
mod non_expressive_names;
279279
mod open_options;
280280
mod option_env_unwrap;
281+
mod option_if_let_else;
281282
mod overflow_check_conditional;
282283
mod panic_unimplemented;
283284
mod partialeq_ne_impl;
@@ -739,6 +740,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
739740
&non_expressive_names::SIMILAR_NAMES,
740741
&open_options::NONSENSICAL_OPEN_OPTIONS,
741742
&option_env_unwrap::OPTION_ENV_UNWRAP,
743+
&option_if_let_else::OPTION_IF_LET_ELSE,
742744
&overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL,
743745
&panic_unimplemented::PANIC,
744746
&panic_unimplemented::PANIC_PARAMS,
@@ -1047,6 +1049,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10471049
store.register_late_pass(|| box redundant_pub_crate::RedundantPubCrate::default());
10481050
store.register_late_pass(|| box unnamed_address::UnnamedAddress);
10491051
store.register_late_pass(|| box dereference::Dereferencing);
1052+
store.register_late_pass(|| box option_if_let_else::OptionIfLetElse);
10501053
store.register_late_pass(|| box future_not_send::FutureNotSend);
10511054

10521055
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
@@ -1341,6 +1344,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13411344
LintId::of(&non_expressive_names::MANY_SINGLE_CHAR_NAMES),
13421345
LintId::of(&open_options::NONSENSICAL_OPEN_OPTIONS),
13431346
LintId::of(&option_env_unwrap::OPTION_ENV_UNWRAP),
1347+
LintId::of(&option_if_let_else::OPTION_IF_LET_ELSE),
13441348
LintId::of(&overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL),
13451349
LintId::of(&panic_unimplemented::PANIC_PARAMS),
13461350
LintId::of(&partialeq_ne_impl::PARTIALEQ_NE_IMPL),
@@ -1487,6 +1491,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14871491
LintId::of(&new_without_default::NEW_WITHOUT_DEFAULT),
14881492
LintId::of(&non_expressive_names::JUST_UNDERSCORES_AND_DIGITS),
14891493
LintId::of(&non_expressive_names::MANY_SINGLE_CHAR_NAMES),
1494+
LintId::of(&option_if_let_else::OPTION_IF_LET_ELSE),
14901495
LintId::of(&panic_unimplemented::PANIC_PARAMS),
14911496
LintId::of(&ptr::CMP_NULL),
14921497
LintId::of(&ptr::PTR_ARG),
+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use crate::utils::sugg::Sugg;
2+
use crate::utils::{match_type, paths, span_lint_and_sugg};
3+
use if_chain::if_chain;
4+
5+
use rustc_errors::Applicability;
6+
use rustc_hir::intravisit::{NestedVisitorMap, Visitor};
7+
use rustc_hir::*;
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::hir::map::Map;
10+
use rustc_session::{declare_lint_pass, declare_tool_lint};
11+
12+
use std::marker::PhantomData;
13+
14+
declare_clippy_lint! {
15+
/// **What it does:**
16+
/// Lints usage of `if let Some(v) = ... { y } else { x }` which is more
17+
/// idiomatically done with `Option::map_or` (if the else bit is a simple
18+
/// expression) or `Option::map_or_else` (if the else bit is a longer
19+
/// block).
20+
///
21+
/// **Why is this bad?**
22+
/// Using the dedicated functions of the Option type is clearer and
23+
/// more concise than an if let expression.
24+
///
25+
/// **Known problems:**
26+
/// This lint uses whether the block is just an expression or if it has
27+
/// more statements to decide whether to use `Option::map_or` or
28+
/// `Option::map_or_else`. If you have a single expression which calls
29+
/// an expensive function, then it would be more efficient to use
30+
/// `Option::map_or_else`, but this lint would suggest `Option::map_or`.
31+
///
32+
/// Also, this lint uses a deliberately conservative metric for checking
33+
/// if the inside of either body contains breaks or continues which will
34+
/// cause it to not suggest a fix if either block contains a loop with
35+
/// continues or breaks contained within the loop.
36+
///
37+
/// **Example:**
38+
///
39+
/// ```rust
40+
/// # let optional: Option<u32> = Some(0);
41+
/// let _ = if let Some(foo) = optional {
42+
/// foo
43+
/// } else {
44+
/// 5
45+
/// };
46+
/// let _ = if let Some(foo) = optional {
47+
/// foo
48+
/// } else {
49+
/// let y = do_complicated_function();
50+
/// y*y
51+
/// };
52+
/// ```
53+
///
54+
/// should be
55+
///
56+
/// ```rust
57+
/// # let optional: Option<u32> = Some(0);
58+
/// let _ = optional.map_or(5, |foo| foo);
59+
/// let _ = optional.map_or_else(||{
60+
/// let y = do_complicated_function;
61+
/// y*y
62+
/// }, |foo| foo);
63+
/// ```
64+
pub OPTION_IF_LET_ELSE,
65+
style,
66+
"reimplementation of Option::map_or"
67+
}
68+
69+
declare_lint_pass!(OptionIfLetElse => [OPTION_IF_LET_ELSE]);
70+
71+
/// Returns true iff the given expression is the result of calling Result::ok
72+
fn is_result_ok(cx: &LateContext<'_, '_>, expr: &'_ Expr<'_>) -> bool {
73+
if_chain! {
74+
if let ExprKind::MethodCall(ref path, _, &[ref receiver]) = &expr.kind;
75+
if path.ident.name.to_ident_string() == "ok";
76+
if match_type(cx, &cx.tables.expr_ty(&receiver), &paths::RESULT);
77+
then {
78+
true
79+
} else {
80+
false
81+
}
82+
}
83+
}
84+
85+
/// A struct containing information about occurences of the
86+
/// `if let Some(..) = .. else` construct that this lint detects.
87+
struct OptionIfLetElseOccurence {
88+
option: String,
89+
method_sugg: String,
90+
some_expr: String,
91+
none_expr: String,
92+
}
93+
94+
struct ReturnBreakContinueVisitor<'tcx> {
95+
seen_return_break_continue: bool,
96+
phantom_data: PhantomData<&'tcx bool>,
97+
}
98+
impl<'tcx> ReturnBreakContinueVisitor<'tcx> {
99+
fn new() -> ReturnBreakContinueVisitor<'tcx> {
100+
ReturnBreakContinueVisitor {
101+
seen_return_break_continue: false,
102+
phantom_data: PhantomData,
103+
}
104+
}
105+
}
106+
impl<'tcx> Visitor<'tcx> for ReturnBreakContinueVisitor<'tcx> {
107+
type Map = Map<'tcx>;
108+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
109+
NestedVisitorMap::None
110+
}
111+
112+
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) {
113+
if self.seen_return_break_continue {
114+
// No need to look farther if we've already seen one of them
115+
return;
116+
}
117+
match &ex.kind {
118+
ExprKind::Ret(..) | ExprKind::Break(..) | ExprKind::Continue(..) => {
119+
self.seen_return_break_continue = true;
120+
},
121+
// Something special could be done here to handle while or for loop
122+
// desugaring, as this will detect a break if there's a while loop
123+
// or a for loop inside the expression.
124+
_ => {
125+
rustc_hir::intravisit::walk_expr(self, ex);
126+
},
127+
}
128+
}
129+
}
130+
131+
fn contains_return_break_continue<'tcx>(expression: &'tcx Expr<'tcx>) -> bool {
132+
let mut recursive_visitor: ReturnBreakContinueVisitor<'tcx> = ReturnBreakContinueVisitor::new();
133+
recursive_visitor.visit_expr(expression);
134+
recursive_visitor.seen_return_break_continue
135+
}
136+
137+
/// If this expression is the option if let/else construct we're detecting, then
138+
/// this function returns an OptionIfLetElseOccurence struct with details if
139+
/// this construct is found, or None if this construct is not found.
140+
fn detect_option_if_let_else<'a>(cx: &LateContext<'_, 'a>, expr: &'a Expr<'a>) -> Option<OptionIfLetElseOccurence> {
141+
//(String, String, String, String)> {
142+
if_chain! {
143+
if let ExprKind::Match(let_body, arms, MatchSource::IfLetDesugar{contains_else_clause: true}) = &expr.kind;
144+
if arms.len() == 2;
145+
if match_type(cx, &cx.tables.expr_ty(let_body), &paths::OPTION);
146+
if !is_result_ok(cx, let_body); // Don't lint on Result::ok because a different lint does it already
147+
if let PatKind::TupleStruct(_, &[inner_pat], _) = &arms[0].pat.kind;
148+
if let PatKind::Binding(_, _, id, _) = &inner_pat.kind;
149+
if !contains_return_break_continue(arms[0].body);
150+
if !contains_return_break_continue(arms[1].body);
151+
then {
152+
let some_body = if let ExprKind::Block(Block { stmts: statements, expr: Some(expr), .. }, _)
153+
= &arms[0].body.kind {
154+
if let &[] = &statements {
155+
expr
156+
} else {
157+
&arms[0].body
158+
}
159+
} else {
160+
return None;
161+
};
162+
let (none_body, method_sugg) = if let ExprKind::Block(Block { stmts: statements, expr: Some(expr), .. }, _)
163+
= &arms[1].body.kind {
164+
if let &[] = &statements {
165+
(expr, "map_or")
166+
} else {
167+
(&arms[1].body, "map_or_else")
168+
}
169+
} else {
170+
return None;
171+
};
172+
let capture_name = id.name.to_ident_string();
173+
Some(OptionIfLetElseOccurence {
174+
option: format!("{}", Sugg::hir(cx, let_body, "..")),
175+
method_sugg: format!("{}", method_sugg),
176+
some_expr: format!("|{}| {}", capture_name, Sugg::hir(cx, some_body, "..")),
177+
none_expr: format!("{}{}", if method_sugg == "map_or" { "" } else { "|| " }, Sugg::hir(cx, none_body, ".."))
178+
})
179+
} else {
180+
None
181+
}
182+
}
183+
}
184+
185+
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for OptionIfLetElse {
186+
fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) {
187+
if let Some(detection) = detect_option_if_let_else(cx, expr) {
188+
span_lint_and_sugg(
189+
cx,
190+
OPTION_IF_LET_ELSE,
191+
expr.span,
192+
format!("use Option::{} instead of an if let/else", detection.method_sugg).as_str(),
193+
"try",
194+
format!(
195+
"{}.{}({}, {})",
196+
detection.option, detection.method_sugg, detection.none_expr, detection.some_expr
197+
),
198+
Applicability::MachineApplicable,
199+
);
200+
}
201+
}
202+
}

src/lintlist/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -1557,6 +1557,13 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
15571557
deprecation: None,
15581558
module: "methods",
15591559
},
1560+
Lint {
1561+
name: "option_if_let_else",
1562+
group: "style",
1563+
desc: "reimplementation of Option::map_or",
1564+
deprecation: None,
1565+
module: "option_if_let_else",
1566+
},
15601567
Lint {
15611568
name: "option_map_or_none",
15621569
group: "style",

tests/ui/option_if_let_else.fixed

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// run-rustfix
2+
#![warn(clippy::option_if_let_else)]
3+
4+
fn bad1(string: Option<&str>) -> (bool, &str) {
5+
string.map_or((false, "hello"), |x| (true, x))
6+
}
7+
8+
fn longer_body(arg: Option<u32>) -> u32 {
9+
arg.map_or(13, |x| {
10+
let y = x * x;
11+
y * y
12+
})
13+
}
14+
15+
fn test_map_or_else(arg: Option<u32>) {
16+
let _ = arg.map_or_else(|| {
17+
let mut y = 1;
18+
y = (y + 2 / y) / 2;
19+
y = (y + 2 / y) / 2;
20+
y
21+
}, |x| x * x * x * x);
22+
}
23+
24+
fn negative_tests(arg: Option<u32>) -> u32 {
25+
let _ = if let Some(13) = arg { "unlucky" } else { "lucky" };
26+
for _ in 0..10 {
27+
let _ = if let Some(x) = arg {
28+
x
29+
} else {
30+
continue;
31+
};
32+
}
33+
let _ = if let Some(x) = arg {
34+
return x;
35+
} else {
36+
5
37+
};
38+
7
39+
}
40+
41+
fn main() {
42+
let optional = Some(5);
43+
let _ = optional.map_or(5, |x| x + 2);
44+
let _ = bad1(None);
45+
let _ = longer_body(None);
46+
test_map_or_else(None);
47+
let _ = negative_tests(None);
48+
}

tests/ui/option_if_let_else.rs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// run-rustfix
2+
#![warn(clippy::option_if_let_else)]
3+
4+
fn bad1(string: Option<&str>) -> (bool, &str) {
5+
if let Some(x) = string {
6+
(true, x)
7+
} else {
8+
(false, "hello")
9+
}
10+
}
11+
12+
fn longer_body(arg: Option<u32>) -> u32 {
13+
if let Some(x) = arg {
14+
let y = x * x;
15+
y * y
16+
} else {
17+
13
18+
}
19+
}
20+
21+
fn test_map_or_else(arg: Option<u32>) {
22+
let _ = if let Some(x) = arg {
23+
x * x * x * x
24+
} else {
25+
let mut y = 1;
26+
y = (y + 2 / y) / 2;
27+
y = (y + 2 / y) / 2;
28+
y
29+
};
30+
}
31+
32+
fn negative_tests(arg: Option<u32>) -> u32 {
33+
let _ = if let Some(13) = arg { "unlucky" } else { "lucky" };
34+
for _ in 0..10 {
35+
let _ = if let Some(x) = arg {
36+
x
37+
} else {
38+
continue;
39+
};
40+
}
41+
let _ = if let Some(x) = arg {
42+
return x;
43+
} else {
44+
5
45+
};
46+
7
47+
}
48+
49+
fn main() {
50+
let optional = Some(5);
51+
let _ = if let Some(x) = optional { x + 2 } else { 5 };
52+
let _ = bad1(None);
53+
let _ = longer_body(None);
54+
test_map_or_else(None);
55+
let _ = negative_tests(None);
56+
}

0 commit comments

Comments
 (0)