Skip to content

Commit 86391ab

Browse files
committed
Don't lint manual_let_else in cases where the question mark operator would work
Also, lint question_mark for `let...else` clauses that can be simplified to use `?`. This lint isn't perfect as it doesn't support the unstable try blocks.
1 parent 73f1417 commit 86391ab

File tree

5 files changed

+245
-2
lines changed

5 files changed

+245
-2
lines changed

clippy_lints/src/manual_let_else.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::higher::IfLetOrMatch;
33
use clippy_utils::msrvs::{self, Msrv};
4-
use clippy_utils::peel_blocks;
54
use clippy_utils::source::snippet_with_context;
65
use clippy_utils::ty::is_type_diagnostic_item;
76
use clippy_utils::visitors::{Descend, Visitable};
7+
use clippy_utils::{is_refutable, is_res_lang_ctor, peel_blocks};
88
use if_chain::if_chain;
99
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
1010
use rustc_errors::Applicability;
1111
use rustc_hir::intravisit::{walk_expr, Visitor};
12+
use rustc_hir::LangItem::{OptionNone, OptionSome};
1213
use rustc_hir::{Expr, ExprKind, HirId, ItemId, Local, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind, Ty};
1314
use rustc_lint::{LateContext, LateLintPass, LintContext};
1415
use rustc_middle::lint::in_external_macro;
@@ -85,6 +86,7 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
8586
if let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then);
8687
if let Some(if_else) = if_else;
8788
if expr_diverges(cx, if_else);
89+
if pat_and_expr_can_be_question_mark(cx, let_pat, if_else).is_none();
8890
then {
8991
emit_manual_let_else(cx, stmt.span, if_let_expr, &ident_map, let_pat, if_else);
9092
}
@@ -167,6 +169,50 @@ fn emit_manual_let_else(
167169
);
168170
}
169171

172+
/// Returns whether the given let pattern and else body can be turned into a question mark
173+
///
174+
/// For this example:
175+
/// ```ignore
176+
/// let FooBar { a, b } = if let Some(a) = ex { a } else { return None };
177+
/// ```
178+
/// We get as parameters:
179+
/// ```ignore
180+
/// pat: Some(a)
181+
/// else_body: return None
182+
/// ```
183+
184+
/// And for this example:
185+
/// ```ignore
186+
/// let Some(FooBar { a, b }) = ex else { return None };
187+
/// ```
188+
/// We get as parameters:
189+
/// ```ignore
190+
/// pat: Some(FooBar { a, b })
191+
/// else_body: return None
192+
/// ```
193+
194+
/// We output `Some(a)` in the first instance, and `Some(FooBar { a, b })` in the second, because
195+
/// the question mark operator is applicable here. Callers have to check whether we are in a
196+
/// constant or not.
197+
pub fn pat_and_expr_can_be_question_mark<'a, 'hir>(
198+
cx: &LateContext<'_>,
199+
pat: &'a Pat<'hir>,
200+
else_body: &Expr<'_>,
201+
) -> Option<&'a Pat<'hir>> {
202+
if let PatKind::TupleStruct(pat_path, [inner_pat], _) = pat.kind &&
203+
is_res_lang_ctor(cx, cx.qpath_res(&pat_path, pat.hir_id), OptionSome) &&
204+
!is_refutable(cx, inner_pat) &&
205+
let else_body = peel_blocks(else_body) &&
206+
let ExprKind::Ret(Some(ret_val)) = else_body.kind &&
207+
let ExprKind::Path(ret_path) = ret_val.kind &&
208+
is_res_lang_ctor(cx, cx.qpath_res(&ret_path, ret_val.hir_id), OptionNone)
209+
{
210+
Some(inner_pat)
211+
} else {
212+
None
213+
}
214+
}
215+
170216
/// Replaces the locals in the pattern
171217
///
172218
/// For this example:

clippy_lints/src/question_mark.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::manual_let_else::pat_and_expr_can_be_question_mark;
12
use clippy_utils::diagnostics::span_lint_and_sugg;
23
use clippy_utils::source::snippet_with_applicability;
34
use clippy_utils::ty::is_type_diagnostic_item;
@@ -10,7 +11,9 @@ use if_chain::if_chain;
1011
use rustc_errors::Applicability;
1112
use rustc_hir::def::Res;
1213
use rustc_hir::LangItem::{self, OptionNone, OptionSome, ResultErr, ResultOk};
13-
use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, Node, PatKind, PathSegment, QPath};
14+
use rustc_hir::{
15+
BindingAnnotation, Block, ByRef, Expr, ExprKind, Local, Node, PatKind, PathSegment, QPath, Stmt, StmtKind,
16+
};
1417
use rustc_lint::{LateContext, LateLintPass};
1518
use rustc_middle::ty::Ty;
1619
use rustc_session::declare_tool_lint;
@@ -78,6 +81,29 @@ enum IfBlockType<'hir> {
7881
),
7982
}
8083

84+
fn check_let_some_else_return_none(cx: &LateContext<'_>, stmt: &Stmt<'_>) {
85+
if let StmtKind::Local(Local { pat, init: Some(init_expr), els: Some(els), .. }) = stmt.kind &&
86+
let Block { stmts: &[], expr: Some(els), .. } = els &&
87+
let Some(inner_pat) = pat_and_expr_can_be_question_mark(cx, pat, els)
88+
{
89+
let mut applicability = Applicability::MaybeIncorrect;
90+
let init_expr_str = snippet_with_applicability(cx, init_expr.span, "..", &mut applicability);
91+
let receiver_str = snippet_with_applicability(cx, inner_pat.span, "..", &mut applicability);
92+
let sugg = format!(
93+
"let {receiver_str} = {init_expr_str}?;",
94+
);
95+
span_lint_and_sugg(
96+
cx,
97+
QUESTION_MARK,
98+
stmt.span,
99+
"this `let...else` may be rewritten with the `?` operator",
100+
"replace it with",
101+
sugg,
102+
applicability,
103+
);
104+
}
105+
}
106+
81107
fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool {
82108
match *if_block {
83109
IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => {
@@ -259,6 +285,11 @@ fn is_try_block(cx: &LateContext<'_>, bl: &rustc_hir::Block<'_>) -> bool {
259285
}
260286

261287
impl<'tcx> LateLintPass<'tcx> for QuestionMark {
288+
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) {
289+
if !in_constant(cx, stmt.hir_id) {
290+
check_let_some_else_return_none(cx, stmt);
291+
}
292+
}
262293
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
263294
if !in_constant(cx, expr.hir_id) {
264295
self.check_is_none_or_err_and_early_return(cx, expr);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//@run-rustfix
2+
#![allow(unused_braces, unused_variables, dead_code)]
3+
#![allow(
4+
clippy::collapsible_else_if,
5+
clippy::unused_unit,
6+
clippy::let_unit_value,
7+
clippy::match_single_binding,
8+
clippy::never_loop
9+
)]
10+
#![warn(clippy::manual_let_else, clippy::question_mark)]
11+
12+
enum Variant {
13+
A(usize, usize),
14+
B(usize),
15+
C,
16+
}
17+
18+
fn g() -> Option<(u8, u8)> {
19+
None
20+
}
21+
22+
fn e() -> Variant {
23+
Variant::A(0, 0)
24+
}
25+
26+
fn main() {}
27+
28+
fn foo() -> Option<()> {
29+
// Fire here, normal case
30+
let v = g()?;
31+
32+
// Don't fire here, the pattern is refutable
33+
let Variant::A(v, w) = e() else { return None };
34+
35+
// Fire here, the pattern is irrefutable
36+
let (v, w) = g()?;
37+
38+
// Don't fire manual_let_else in this instance: question mark can be used instead.
39+
let v = g()?;
40+
41+
// Do fire manual_let_else in this instance: question mark cannot be used here due to the return
42+
// body.
43+
let Some(v) = g() else {
44+
return Some(());
45+
};
46+
47+
// Here we could also fire the question_mark lint, but we don't (as it's a match and not an if let).
48+
// So we still emit manual_let_else here. For the *resulting* code, we *do* emit the question_mark
49+
// lint, so for rustfix reasons, we allow the question_mark lint here.
50+
#[allow(clippy::question_mark)]
51+
{
52+
let Some(v) = g() else { return None };
53+
}
54+
55+
Some(())
56+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//@run-rustfix
2+
#![allow(unused_braces, unused_variables, dead_code)]
3+
#![allow(
4+
clippy::collapsible_else_if,
5+
clippy::unused_unit,
6+
clippy::let_unit_value,
7+
clippy::match_single_binding,
8+
clippy::never_loop
9+
)]
10+
#![warn(clippy::manual_let_else, clippy::question_mark)]
11+
12+
enum Variant {
13+
A(usize, usize),
14+
B(usize),
15+
C,
16+
}
17+
18+
fn g() -> Option<(u8, u8)> {
19+
None
20+
}
21+
22+
fn e() -> Variant {
23+
Variant::A(0, 0)
24+
}
25+
26+
fn main() {}
27+
28+
fn foo() -> Option<()> {
29+
// Fire here, normal case
30+
let Some(v) = g() else { return None };
31+
32+
// Don't fire here, the pattern is refutable
33+
let Variant::A(v, w) = e() else { return None };
34+
35+
// Fire here, the pattern is irrefutable
36+
let Some((v, w)) = g() else { return None };
37+
38+
// Don't fire manual_let_else in this instance: question mark can be used instead.
39+
let v = if let Some(v_some) = g() { v_some } else { return None };
40+
41+
// Do fire manual_let_else in this instance: question mark cannot be used here due to the return
42+
// body.
43+
let v = if let Some(v_some) = g() {
44+
v_some
45+
} else {
46+
return Some(());
47+
};
48+
49+
// Here we could also fire the question_mark lint, but we don't (as it's a match and not an if let).
50+
// So we still emit manual_let_else here. For the *resulting* code, we *do* emit the question_mark
51+
// lint, so for rustfix reasons, we allow the question_mark lint here.
52+
#[allow(clippy::question_mark)]
53+
{
54+
let v = match g() {
55+
Some(v_some) => v_some,
56+
_ => return None,
57+
};
58+
}
59+
60+
Some(())
61+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
error: this `let...else` may be rewritten with the `?` operator
2+
--> $DIR/manual_let_else_question_mark.rs:30:5
3+
|
4+
LL | let Some(v) = g() else { return None };
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `let v = g()?;`
6+
|
7+
= note: `-D clippy::question-mark` implied by `-D warnings`
8+
9+
error: this `let...else` may be rewritten with the `?` operator
10+
--> $DIR/manual_let_else_question_mark.rs:36:5
11+
|
12+
LL | let Some((v, w)) = g() else { return None };
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `let (v, w) = g()?;`
14+
15+
error: this block may be rewritten with the `?` operator
16+
--> $DIR/manual_let_else_question_mark.rs:39:13
17+
|
18+
LL | let v = if let Some(v_some) = g() { v_some } else { return None };
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `g()?`
20+
21+
error: this could be rewritten as `let...else`
22+
--> $DIR/manual_let_else_question_mark.rs:43:5
23+
|
24+
LL | / let v = if let Some(v_some) = g() {
25+
LL | | v_some
26+
LL | | } else {
27+
LL | | return Some(());
28+
LL | | };
29+
| |______^
30+
|
31+
= note: `-D clippy::manual-let-else` implied by `-D warnings`
32+
help: consider writing
33+
|
34+
LL ~ let Some(v) = g() else {
35+
LL + return Some(());
36+
LL + };
37+
|
38+
39+
error: this could be rewritten as `let...else`
40+
--> $DIR/manual_let_else_question_mark.rs:54:9
41+
|
42+
LL | / let v = match g() {
43+
LL | | Some(v_some) => v_some,
44+
LL | | _ => return None,
45+
LL | | };
46+
| |__________^ help: consider writing: `let Some(v) = g() else { return None };`
47+
48+
error: aborting due to 5 previous errors
49+

0 commit comments

Comments
 (0)