Skip to content

Commit 3f17c5c

Browse files
committed
Auto merge of rust-lang#10924 - est31:manual_let_else_question_mark, r=Centri3,flip1995,Manishearth
Don't lint manual_let_else in cases where ? would work Don't lint `manual_let_else` where the question mark operator `?` would be sufficient, that is, mostly in cases like: ```Rust let v = if let Some(v) = ex { v } else { return None }; ``` Also, this PR emits the `question_mark` lint for `let...else` patterns that could be written with `?` (also, only `return None` like cases). ``` changelog: [`manual_let_else`]: don't lint in cases where question_mark already lints changelog: [`question_mark`]: lint for `let Some(...) = ex else { return None };` ``` Fixes rust-lang#8755
2 parents a959061 + d80581c commit 3f17c5c

8 files changed

+299
-39
lines changed

clippy_lints/src/lib.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
663663
});
664664
store.register_late_pass(move |_| Box::new(matches::Matches::new(msrv())));
665665
let matches_for_let_else = conf.matches_for_let_else;
666-
store.register_late_pass(move |_| Box::new(manual_let_else::ManualLetElse::new(msrv(), matches_for_let_else)));
667666
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(msrv())));
668667
store.register_late_pass(move |_| Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(msrv())));
669668
store.register_late_pass(move |_| Box::new(manual_strip::ManualStrip::new(msrv())));
@@ -772,7 +771,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
772771
store.register_late_pass(|_| Box::<useless_conversion::UselessConversion>::default());
773772
store.register_late_pass(|_| Box::new(implicit_hasher::ImplicitHasher));
774773
store.register_late_pass(|_| Box::new(fallible_impl_from::FallibleImplFrom));
775-
store.register_late_pass(|_| Box::<question_mark::QuestionMark>::default());
774+
store.register_late_pass(move |_| Box::new(question_mark::QuestionMark::new(msrv(), matches_for_let_else)));
776775
store.register_late_pass(|_| Box::new(question_mark_used::QuestionMarkUsed));
777776
store.register_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings));
778777
store.register_late_pass(|_| Box::new(suspicious_trait_impl::SuspiciousImpl));

clippy_lints/src/manual_let_else.rs

+14-31
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1+
use crate::question_mark::{QuestionMark, QUESTION_MARK};
12
use clippy_utils::diagnostics::span_lint_and_then;
23
use clippy_utils::higher::IfLetOrMatch;
3-
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};
8-
use if_chain::if_chain;
7+
use clippy_utils::{is_lint_allowed, msrvs, pat_and_expr_can_be_question_mark, peel_blocks};
98
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
109
use rustc_errors::Applicability;
1110
use rustc_hir::intravisit::{walk_expr, Visitor};
1211
use rustc_hir::{Expr, ExprKind, HirId, ItemId, Local, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind, Ty};
13-
use rustc_lint::{LateContext, LateLintPass, LintContext};
12+
use rustc_lint::{LateContext, LintContext};
1413
use rustc_middle::lint::in_external_macro;
15-
use rustc_session::{declare_tool_lint, impl_lint_pass};
14+
use rustc_session::declare_tool_lint;
1615
use rustc_span::symbol::{sym, Symbol};
1716
use rustc_span::Span;
1817
use serde::Deserialize;
@@ -50,25 +49,8 @@ declare_clippy_lint! {
5049
"manual implementation of a let...else statement"
5150
}
5251

53-
pub struct ManualLetElse {
54-
msrv: Msrv,
55-
matches_behaviour: MatchLintBehaviour,
56-
}
57-
58-
impl ManualLetElse {
59-
#[must_use]
60-
pub fn new(msrv: Msrv, matches_behaviour: MatchLintBehaviour) -> Self {
61-
Self {
62-
msrv,
63-
matches_behaviour,
64-
}
65-
}
66-
}
67-
68-
impl_lint_pass!(ManualLetElse => [MANUAL_LET_ELSE]);
69-
70-
impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
71-
fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
52+
impl<'tcx> QuestionMark {
53+
pub(crate) fn check_manual_let_else(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
7254
if !self.msrv.meets(msrvs::LET_ELSE) || in_external_macro(cx.sess(), stmt.span) {
7355
return;
7456
}
@@ -81,11 +63,14 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
8163
let Some(if_let_or_match) = IfLetOrMatch::parse(cx, init)
8264
{
8365
match if_let_or_match {
84-
IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => if_chain! {
85-
if let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then);
86-
if let Some(if_else) = if_else;
87-
if expr_diverges(cx, if_else);
88-
then {
66+
IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => {
67+
if
68+
let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then) &&
69+
let Some(if_else) = if_else &&
70+
expr_diverges(cx, if_else) &&
71+
let qm_allowed = is_lint_allowed(cx, QUESTION_MARK, stmt.hir_id) &&
72+
(qm_allowed || pat_and_expr_can_be_question_mark(cx, let_pat, if_else).is_none())
73+
{
8974
emit_manual_let_else(cx, stmt.span, if_let_expr, &ident_map, let_pat, if_else);
9075
}
9176
},
@@ -128,8 +113,6 @@ impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
128113
}
129114
};
130115
}
131-
132-
extract_msrv_attr!(LateContext);
133116
}
134117

135118
fn emit_manual_let_else(

clippy_lints/src/manual_rem_euclid.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ fn check_for_either_unsigned_int_constant<'a>(
119119
}
120120

121121
fn check_for_unsigned_int_constant<'a>(cx: &'a LateContext<'_>, expr: &'a Expr<'_>) -> Option<u128> {
122-
let Some(int_const) = constant_full_int(cx, cx.typeck_results(), expr) else { return None };
122+
let int_const = constant_full_int(cx, cx.typeck_results(), expr)?;
123123
match int_const {
124124
FullInt::S(s) => s.try_into().ok(),
125125
FullInt::U(u) => Some(u),

clippy_lints/src/question_mark.rs

+52-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
use crate::manual_let_else::{MatchLintBehaviour, MANUAL_LET_ELSE};
12
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::msrvs::Msrv;
24
use clippy_utils::source::snippet_with_applicability;
35
use clippy_utils::ty::is_type_diagnostic_item;
46
use clippy_utils::{
5-
eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, path_to_local, path_to_local_id,
6-
peel_blocks, peel_blocks_with_stmt,
7+
eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, pat_and_expr_can_be_question_mark,
8+
path_to_local, path_to_local_id, peel_blocks, peel_blocks_with_stmt,
79
};
810
use clippy_utils::{higher, is_path_lang_item};
911
use if_chain::if_chain;
1012
use rustc_errors::Applicability;
1113
use rustc_hir::def::Res;
1214
use rustc_hir::LangItem::{self, OptionNone, OptionSome, ResultErr, ResultOk};
13-
use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, Node, PatKind, PathSegment, QPath};
15+
use rustc_hir::{
16+
BindingAnnotation, Block, ByRef, Expr, ExprKind, Local, Node, PatKind, PathSegment, QPath, Stmt, StmtKind,
17+
};
1418
use rustc_lint::{LateContext, LateLintPass};
1519
use rustc_middle::ty::Ty;
1620
use rustc_session::declare_tool_lint;
@@ -42,16 +46,29 @@ declare_clippy_lint! {
4246
"checks for expressions that could be replaced by the question mark operator"
4347
}
4448

45-
#[derive(Default)]
4649
pub struct QuestionMark {
50+
pub(crate) msrv: Msrv,
51+
pub(crate) matches_behaviour: MatchLintBehaviour,
4752
/// Keeps track of how many try blocks we are in at any point during linting.
4853
/// This allows us to answer the question "are we inside of a try block"
4954
/// very quickly, without having to walk up the parent chain, by simply checking
5055
/// if it is greater than zero.
5156
/// As for why we need this in the first place: <https://github.com/rust-lang/rust-clippy/issues/8628>
5257
try_block_depth_stack: Vec<u32>,
5358
}
54-
impl_lint_pass!(QuestionMark => [QUESTION_MARK]);
59+
60+
impl_lint_pass!(QuestionMark => [QUESTION_MARK, MANUAL_LET_ELSE]);
61+
62+
impl QuestionMark {
63+
#[must_use]
64+
pub fn new(msrv: Msrv, matches_behaviour: MatchLintBehaviour) -> Self {
65+
Self {
66+
msrv,
67+
matches_behaviour,
68+
try_block_depth_stack: Vec::new(),
69+
}
70+
}
71+
}
5572

5673
enum IfBlockType<'hir> {
5774
/// An `if x.is_xxx() { a } else { b } ` expression.
@@ -78,6 +95,29 @@ enum IfBlockType<'hir> {
7895
),
7996
}
8097

98+
fn check_let_some_else_return_none(cx: &LateContext<'_>, stmt: &Stmt<'_>) {
99+
if let StmtKind::Local(Local { pat, init: Some(init_expr), els: Some(els), .. }) = stmt.kind &&
100+
let Block { stmts: &[], expr: Some(els), .. } = els &&
101+
let Some(inner_pat) = pat_and_expr_can_be_question_mark(cx, pat, els)
102+
{
103+
let mut applicability = Applicability::MaybeIncorrect;
104+
let init_expr_str = snippet_with_applicability(cx, init_expr.span, "..", &mut applicability);
105+
let receiver_str = snippet_with_applicability(cx, inner_pat.span, "..", &mut applicability);
106+
let sugg = format!(
107+
"let {receiver_str} = {init_expr_str}?;",
108+
);
109+
span_lint_and_sugg(
110+
cx,
111+
QUESTION_MARK,
112+
stmt.span,
113+
"this `let...else` may be rewritten with the `?` operator",
114+
"replace it with",
115+
sugg,
116+
applicability,
117+
);
118+
}
119+
}
120+
81121
fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool {
82122
match *if_block {
83123
IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => {
@@ -259,6 +299,12 @@ fn is_try_block(cx: &LateContext<'_>, bl: &rustc_hir::Block<'_>) -> bool {
259299
}
260300

261301
impl<'tcx> LateLintPass<'tcx> for QuestionMark {
302+
fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) {
303+
if !in_constant(cx, stmt.hir_id) {
304+
check_let_some_else_return_none(cx, stmt);
305+
}
306+
self.check_manual_let_else(cx, stmt);
307+
}
262308
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
263309
if !in_constant(cx, expr.hir_id) {
264310
self.check_is_none_or_err_and_early_return(cx, expr);
@@ -291,4 +337,5 @@ impl<'tcx> LateLintPass<'tcx> for QuestionMark {
291337
.expect("blocks are always part of bodies and must have a depth") -= 1;
292338
}
293339
}
340+
extract_msrv_attr!(LateContext);
294341
}

clippy_utils/src/lib.rs

+45
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ use rustc_hir::def::{DefKind, Res};
8787
use rustc_hir::def_id::{CrateNum, DefId, LocalDefId, LOCAL_CRATE};
8888
use rustc_hir::hir_id::{HirIdMap, HirIdSet};
8989
use rustc_hir::intravisit::{walk_expr, FnKind, Visitor};
90+
use rustc_hir::LangItem::OptionSome;
9091
use rustc_hir::LangItem::{OptionNone, ResultErr, ResultOk};
9192
use rustc_hir::{
9293
self as hir, def, Arm, ArrayLen, BindingAnnotation, Block, BlockCheckMode, Body, Closure, Destination, Expr,
@@ -2543,6 +2544,50 @@ pub fn span_find_starting_semi(sm: &SourceMap, span: Span) -> Span {
25432544
sm.span_take_while(span, |&ch| ch == ' ' || ch == ';')
25442545
}
25452546

2547+
/// Returns whether the given let pattern and else body can be turned into a question mark
2548+
///
2549+
/// For this example:
2550+
/// ```ignore
2551+
/// let FooBar { a, b } = if let Some(a) = ex { a } else { return None };
2552+
/// ```
2553+
/// We get as parameters:
2554+
/// ```ignore
2555+
/// pat: Some(a)
2556+
/// else_body: return None
2557+
/// ```
2558+
2559+
/// And for this example:
2560+
/// ```ignore
2561+
/// let Some(FooBar { a, b }) = ex else { return None };
2562+
/// ```
2563+
/// We get as parameters:
2564+
/// ```ignore
2565+
/// pat: Some(FooBar { a, b })
2566+
/// else_body: return None
2567+
/// ```
2568+
2569+
/// We output `Some(a)` in the first instance, and `Some(FooBar { a, b })` in the second, because
2570+
/// the question mark operator is applicable here. Callers have to check whether we are in a
2571+
/// constant or not.
2572+
pub fn pat_and_expr_can_be_question_mark<'a, 'hir>(
2573+
cx: &LateContext<'_>,
2574+
pat: &'a Pat<'hir>,
2575+
else_body: &Expr<'_>,
2576+
) -> Option<&'a Pat<'hir>> {
2577+
if let PatKind::TupleStruct(pat_path, [inner_pat], _) = pat.kind &&
2578+
is_res_lang_ctor(cx, cx.qpath_res(&pat_path, pat.hir_id), OptionSome) &&
2579+
!is_refutable(cx, inner_pat) &&
2580+
let else_body = peel_blocks(else_body) &&
2581+
let ExprKind::Ret(Some(ret_val)) = else_body.kind &&
2582+
let ExprKind::Path(ret_path) = ret_val.kind &&
2583+
is_res_lang_ctor(cx, cx.qpath_res(&ret_path, ret_val.hir_id), OptionNone)
2584+
{
2585+
Some(inner_pat)
2586+
} else {
2587+
None
2588+
}
2589+
}
2590+
25462591
macro_rules! op_utils {
25472592
($($name:ident $assign:ident)*) => {
25482593
/// Binary operation traits like `LangItem::Add`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
// This is a copy of the case above where we'd fire the question_mark lint, but here we have allowed
56+
// it. Make sure that manual_let_else is fired as the fallback.
57+
#[allow(clippy::question_mark)]
58+
{
59+
let Some(v) = g() else { return None };
60+
}
61+
62+
Some(())
63+
}

0 commit comments

Comments
 (0)