Skip to content

Commit 10ce1a6

Browse files
committed
Auto merge of rust-lang#11001 - y21:issue8628, r=Jarcho
[`question_mark`]: don't lint inside of `try` block Fixes rust-lang#8628. Diff looks a bit noisy because I had to move the two functions into an impl, because they now need to access the structs `try_block_depth` field to see if they're inside a try block. changelog: [`question_mark`]: don't lint inside of `try` block
2 parents ea4c5c5 + 70610c0 commit 10ce1a6

File tree

5 files changed

+227
-115
lines changed

5 files changed

+227
-115
lines changed

clippy_lints/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
771771
store.register_late_pass(|_| Box::<useless_conversion::UselessConversion>::default());
772772
store.register_late_pass(|_| Box::new(implicit_hasher::ImplicitHasher));
773773
store.register_late_pass(|_| Box::new(fallible_impl_from::FallibleImplFrom));
774-
store.register_late_pass(|_| Box::new(question_mark::QuestionMark));
774+
store.register_late_pass(|_| Box::<question_mark::QuestionMark>::default());
775775
store.register_late_pass(|_| Box::new(question_mark_used::QuestionMarkUsed));
776776
store.register_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings));
777777
store.register_late_pass(|_| Box::new(suspicious_trait_impl::SuspiciousImpl));

clippy_lints/src/question_mark.rs

+152-98
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::higher;
32
use clippy_utils::source::snippet_with_applicability;
43
use clippy_utils::ty::is_type_diagnostic_item;
54
use clippy_utils::{
65
eq_expr_value, get_parent_node, in_constant, is_else_clause, is_res_lang_ctor, path_to_local, path_to_local_id,
76
peel_blocks, peel_blocks_with_stmt,
87
};
8+
use clippy_utils::{higher, is_path_lang_item};
99
use if_chain::if_chain;
1010
use rustc_errors::Applicability;
1111
use rustc_hir::def::Res;
12-
use rustc_hir::LangItem::{OptionNone, OptionSome, ResultErr, ResultOk};
12+
use rustc_hir::LangItem::{self, OptionNone, OptionSome, ResultErr, ResultOk};
1313
use rustc_hir::{BindingAnnotation, ByRef, Expr, ExprKind, Node, PatKind, PathSegment, QPath};
1414
use rustc_lint::{LateContext, LateLintPass};
1515
use rustc_middle::ty::Ty;
16-
use rustc_session::{declare_lint_pass, declare_tool_lint};
16+
use rustc_session::declare_tool_lint;
17+
use rustc_session::impl_lint_pass;
1718
use rustc_span::{sym, symbol::Symbol};
1819

1920
declare_clippy_lint! {
@@ -41,7 +42,16 @@ declare_clippy_lint! {
4142
"checks for expressions that could be replaced by the question mark operator"
4243
}
4344

44-
declare_lint_pass!(QuestionMark => [QUESTION_MARK]);
45+
#[derive(Default)]
46+
pub struct QuestionMark {
47+
/// Keeps track of how many try blocks we are in at any point during linting.
48+
/// This allows us to answer the question "are we inside of a try block"
49+
/// very quickly, without having to walk up the parent chain, by simply checking
50+
/// if it is greater than zero.
51+
/// As for why we need this in the first place: <https://github.com/rust-lang/rust-clippy/issues/8628>
52+
try_block_depth_stack: Vec<u32>,
53+
}
54+
impl_lint_pass!(QuestionMark => [QUESTION_MARK]);
4555

4656
enum IfBlockType<'hir> {
4757
/// An `if x.is_xxx() { a } else { b } ` expression.
@@ -68,98 +78,6 @@ enum IfBlockType<'hir> {
6878
),
6979
}
7080

71-
/// Checks if the given expression on the given context matches the following structure:
72-
///
73-
/// ```ignore
74-
/// if option.is_none() {
75-
/// return None;
76-
/// }
77-
/// ```
78-
///
79-
/// ```ignore
80-
/// if result.is_err() {
81-
/// return result;
82-
/// }
83-
/// ```
84-
///
85-
/// If it matches, it will suggest to use the question mark operator instead
86-
fn check_is_none_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
87-
if_chain! {
88-
if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr);
89-
if !is_else_clause(cx.tcx, expr);
90-
if let ExprKind::MethodCall(segment, caller, ..) = &cond.kind;
91-
let caller_ty = cx.typeck_results().expr_ty(caller);
92-
let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then, r#else);
93-
if is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block);
94-
then {
95-
let mut applicability = Applicability::MachineApplicable;
96-
let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability);
97-
let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env) &&
98-
!matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..));
99-
let sugg = if let Some(else_inner) = r#else {
100-
if eq_expr_value(cx, caller, peel_blocks(else_inner)) {
101-
format!("Some({receiver_str}?)")
102-
} else {
103-
return;
104-
}
105-
} else {
106-
format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" })
107-
};
108-
109-
span_lint_and_sugg(
110-
cx,
111-
QUESTION_MARK,
112-
expr.span,
113-
"this block may be rewritten with the `?` operator",
114-
"replace it with",
115-
sugg,
116-
applicability,
117-
);
118-
}
119-
}
120-
}
121-
122-
fn check_if_let_some_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
123-
if_chain! {
124-
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else }) = higher::IfLet::hir(cx, expr);
125-
if !is_else_clause(cx.tcx, expr);
126-
if let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind;
127-
if ddpos.as_opt_usize().is_none();
128-
if let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind;
129-
let caller_ty = cx.typeck_results().expr_ty(let_expr);
130-
let if_block = IfBlockType::IfLet(
131-
cx.qpath_res(path1, let_pat.hir_id),
132-
caller_ty,
133-
ident.name,
134-
let_expr,
135-
if_then,
136-
if_else
137-
);
138-
if (is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id))
139-
|| is_early_return(sym::Result, cx, &if_block);
140-
if if_else.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e))).filter(|e| *e).is_none();
141-
then {
142-
let mut applicability = Applicability::MachineApplicable;
143-
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
144-
let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_)));
145-
let sugg = format!(
146-
"{receiver_str}{}?{}",
147-
if by_ref == ByRef::Yes { ".as_ref()" } else { "" },
148-
if requires_semi { ";" } else { "" }
149-
);
150-
span_lint_and_sugg(
151-
cx,
152-
QUESTION_MARK,
153-
expr.span,
154-
"this block may be rewritten with the `?` operator",
155-
"replace it with",
156-
sugg,
157-
applicability,
158-
);
159-
}
160-
}
161-
}
162-
16381
fn is_early_return(smbl: Symbol, cx: &LateContext<'_>, if_block: &IfBlockType<'_>) -> bool {
16482
match *if_block {
16583
IfBlockType::IfIs(caller, caller_ty, call_sym, if_then, _) => {
@@ -230,11 +148,147 @@ fn expr_return_none_or_err(
230148
}
231149
}
232150

151+
impl QuestionMark {
152+
fn inside_try_block(&self) -> bool {
153+
self.try_block_depth_stack.last() > Some(&0)
154+
}
155+
156+
/// Checks if the given expression on the given context matches the following structure:
157+
///
158+
/// ```ignore
159+
/// if option.is_none() {
160+
/// return None;
161+
/// }
162+
/// ```
163+
///
164+
/// ```ignore
165+
/// if result.is_err() {
166+
/// return result;
167+
/// }
168+
/// ```
169+
///
170+
/// If it matches, it will suggest to use the question mark operator instead
171+
fn check_is_none_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
172+
if_chain! {
173+
if !self.inside_try_block();
174+
if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr);
175+
if !is_else_clause(cx.tcx, expr);
176+
if let ExprKind::MethodCall(segment, caller, ..) = &cond.kind;
177+
let caller_ty = cx.typeck_results().expr_ty(caller);
178+
let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then, r#else);
179+
if is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block);
180+
then {
181+
let mut applicability = Applicability::MachineApplicable;
182+
let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability);
183+
let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env) &&
184+
!matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..));
185+
let sugg = if let Some(else_inner) = r#else {
186+
if eq_expr_value(cx, caller, peel_blocks(else_inner)) {
187+
format!("Some({receiver_str}?)")
188+
} else {
189+
return;
190+
}
191+
} else {
192+
format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" })
193+
};
194+
195+
span_lint_and_sugg(
196+
cx,
197+
QUESTION_MARK,
198+
expr.span,
199+
"this block may be rewritten with the `?` operator",
200+
"replace it with",
201+
sugg,
202+
applicability,
203+
);
204+
}
205+
}
206+
}
207+
208+
fn check_if_let_some_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
209+
if_chain! {
210+
if !self.inside_try_block();
211+
if let Some(higher::IfLet { let_pat, let_expr, if_then, if_else }) = higher::IfLet::hir(cx, expr);
212+
if !is_else_clause(cx.tcx, expr);
213+
if let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind;
214+
if ddpos.as_opt_usize().is_none();
215+
if let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind;
216+
let caller_ty = cx.typeck_results().expr_ty(let_expr);
217+
let if_block = IfBlockType::IfLet(
218+
cx.qpath_res(path1, let_pat.hir_id),
219+
caller_ty,
220+
ident.name,
221+
let_expr,
222+
if_then,
223+
if_else
224+
);
225+
if (is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id))
226+
|| is_early_return(sym::Result, cx, &if_block);
227+
if if_else.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e))).filter(|e| *e).is_none();
228+
then {
229+
let mut applicability = Applicability::MachineApplicable;
230+
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
231+
let requires_semi = matches!(get_parent_node(cx.tcx, expr.hir_id), Some(Node::Stmt(_)));
232+
let sugg = format!(
233+
"{receiver_str}{}?{}",
234+
if by_ref == ByRef::Yes { ".as_ref()" } else { "" },
235+
if requires_semi { ";" } else { "" }
236+
);
237+
span_lint_and_sugg(
238+
cx,
239+
QUESTION_MARK,
240+
expr.span,
241+
"this block may be rewritten with the `?` operator",
242+
"replace it with",
243+
sugg,
244+
applicability,
245+
);
246+
}
247+
}
248+
}
249+
}
250+
251+
fn is_try_block(cx: &LateContext<'_>, bl: &rustc_hir::Block<'_>) -> bool {
252+
if let Some(expr) = bl.expr
253+
&& let rustc_hir::ExprKind::Call(callee, _) = expr.kind
254+
{
255+
is_path_lang_item(cx, callee, LangItem::TryTraitFromOutput)
256+
} else {
257+
false
258+
}
259+
}
260+
233261
impl<'tcx> LateLintPass<'tcx> for QuestionMark {
234262
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
235263
if !in_constant(cx, expr.hir_id) {
236-
check_is_none_or_err_and_early_return(cx, expr);
237-
check_if_let_some_or_err_and_early_return(cx, expr);
264+
self.check_is_none_or_err_and_early_return(cx, expr);
265+
self.check_if_let_some_or_err_and_early_return(cx, expr);
266+
}
267+
}
268+
269+
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx rustc_hir::Block<'tcx>) {
270+
if is_try_block(cx, block) {
271+
*self
272+
.try_block_depth_stack
273+
.last_mut()
274+
.expect("blocks are always part of bodies and must have a depth") += 1;
275+
}
276+
}
277+
278+
fn check_body(&mut self, _: &LateContext<'tcx>, _: &'tcx rustc_hir::Body<'tcx>) {
279+
self.try_block_depth_stack.push(0);
280+
}
281+
282+
fn check_body_post(&mut self, _: &LateContext<'tcx>, _: &'tcx rustc_hir::Body<'tcx>) {
283+
self.try_block_depth_stack.pop();
284+
}
285+
286+
fn check_block_post(&mut self, cx: &LateContext<'tcx>, block: &'tcx rustc_hir::Block<'tcx>) {
287+
if is_try_block(cx, block) {
288+
*self
289+
.try_block_depth_stack
290+
.last_mut()
291+
.expect("blocks are always part of bodies and must have a depth") -= 1;
238292
}
239293
}
240294
}

tests/ui/question_mark.fixed

+22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//@run-rustfix
2+
#![feature(try_blocks)]
23
#![allow(unreachable_code)]
34
#![allow(dead_code)]
45
#![allow(clippy::unnecessary_wraps)]
@@ -227,6 +228,27 @@ fn pattern() -> Result<(), PatternedError> {
227228

228229
fn main() {}
229230

231+
// `?` is not the same as `return None;` if inside of a try block
232+
fn issue8628(a: Option<u32>) -> Option<u32> {
233+
let b: Option<u32> = try {
234+
if a.is_none() {
235+
return None;
236+
}
237+
32
238+
};
239+
b.or(Some(128))
240+
}
241+
242+
fn issue6828_nested_body() -> Option<u32> {
243+
try {
244+
fn f2(a: Option<i32>) -> Option<i32> {
245+
a?;
246+
Some(32)
247+
}
248+
123
249+
}
250+
}
251+
230252
// should not lint, `?` operator not available in const context
231253
const fn issue9175(option: Option<()>) -> Option<()> {
232254
if option.is_none() {

tests/ui/question_mark.rs

+26
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//@run-rustfix
2+
#![feature(try_blocks)]
23
#![allow(unreachable_code)]
34
#![allow(dead_code)]
45
#![allow(clippy::unnecessary_wraps)]
@@ -263,6 +264,31 @@ fn pattern() -> Result<(), PatternedError> {
263264

264265
fn main() {}
265266

267+
// `?` is not the same as `return None;` if inside of a try block
268+
fn issue8628(a: Option<u32>) -> Option<u32> {
269+
let b: Option<u32> = try {
270+
if a.is_none() {
271+
return None;
272+
}
273+
32
274+
};
275+
b.or(Some(128))
276+
}
277+
278+
fn issue6828_nested_body() -> Option<u32> {
279+
try {
280+
fn f2(a: Option<i32>) -> Option<i32> {
281+
if a.is_none() {
282+
return None;
283+
// do lint here, the outer `try` is not relevant here
284+
// https://github.com/rust-lang/rust-clippy/pull/11001#issuecomment-1610636867
285+
}
286+
Some(32)
287+
}
288+
123
289+
}
290+
}
291+
266292
// should not lint, `?` operator not available in const context
267293
const fn issue9175(option: Option<()>) -> Option<()> {
268294
if option.is_none() {

0 commit comments

Comments
 (0)