Auto merge of #12341 - y21:issue12337, r=dswij

Check for try blocks in `question_mark` more consistently

Fixes #12337

I split this PR up into two commits since this moves a method out of an `impl`, which makes for a pretty bad diff (the `&self` parameter is now unused, and there isn't a reason for that function to be part of the `impl` now).

The first commit is the actual relevant change and the 2nd commit just moves stuff (github's "hide whitespace" makes the diff easier to look at)

------------
Now for the actual issue:

`?` within `try {}` blocks desugars to a `break` to the block, rather than a `return`, so that changes behavior in those cases.

The lint has multiple patterns to look for and in *some* of them it already does correctly check whether we're in a try block, but this isn't done for all of its patterns.

We could add another `self.inside_try_block()` check to the function that looks for `let-else-return`, but I chose to actually just move those checks out and instead have them in `LintPass::check_{stmt,expr}`. This has the advantage that we can't (easily) accidentally forget to add that check in new patterns that might be added in the future.

(There's also a bit of a subtle interaction between two lints, where `question_mark`'s LintPass calls into `manual_let_else`, so I added a check to make sure we don't avoid linting for something that doesn't have anything to do with `?`)

changelog: [`question_mark`]: avoid linting on try blocks in more cases
This commit is contained in:
bors 2024-03-04 14:34:56 +00:00
commit e970fa52e7
5 changed files with 171 additions and 134 deletions

View File

@ -203,109 +203,107 @@ fn expr_return_none_or_err(
}
}
/// Checks if the given expression on the given context matches the following structure:
///
/// ```ignore
/// if option.is_none() {
/// return None;
/// }
/// ```
///
/// ```ignore
/// if result.is_err() {
/// return result;
/// }
/// ```
///
/// If it matches, it will suggest to use the question mark operator instead
fn check_is_none_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
if let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr)
&& !is_else_clause(cx.tcx, expr)
&& let ExprKind::MethodCall(segment, caller, ..) = &cond.kind
&& let caller_ty = cx.typeck_results().expr_ty(caller)
&& let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then)
&& (is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block))
{
let mut applicability = Applicability::MachineApplicable;
let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability);
let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env)
&& !matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..));
let sugg = if let Some(else_inner) = r#else {
if eq_expr_value(cx, caller, peel_blocks(else_inner)) {
format!("Some({receiver_str}?)")
} else {
return;
}
} else {
format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" })
};
span_lint_and_sugg(
cx,
QUESTION_MARK,
expr.span,
"this block may be rewritten with the `?` operator",
"replace it with",
sugg,
applicability,
);
}
}
fn check_if_let_some_or_err_and_early_return<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
if let Some(higher::IfLet {
let_pat,
let_expr,
if_then,
if_else,
..
}) = higher::IfLet::hir(cx, expr)
&& !is_else_clause(cx.tcx, expr)
&& let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind
&& ddpos.as_opt_usize().is_none()
&& let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind
&& let caller_ty = cx.typeck_results().expr_ty(let_expr)
&& let if_block = IfBlockType::IfLet(
cx.qpath_res(path1, let_pat.hir_id),
caller_ty,
ident.name,
let_expr,
if_then,
if_else,
)
&& ((is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id))
|| is_early_return(sym::Result, cx, &if_block))
&& if_else
.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e)))
.filter(|e| *e)
.is_none()
{
let mut applicability = Applicability::MachineApplicable;
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
let requires_semi = matches!(cx.tcx.parent_hir_node(expr.hir_id), Node::Stmt(_));
let sugg = format!(
"{receiver_str}{}?{}",
if by_ref == ByRef::Yes { ".as_ref()" } else { "" },
if requires_semi { ";" } else { "" }
);
span_lint_and_sugg(
cx,
QUESTION_MARK,
expr.span,
"this block may be rewritten with the `?` operator",
"replace it with",
sugg,
applicability,
);
}
}
impl QuestionMark {
fn inside_try_block(&self) -> bool {
self.try_block_depth_stack.last() > Some(&0)
}
/// Checks if the given expression on the given context matches the following structure:
///
/// ```ignore
/// if option.is_none() {
/// return None;
/// }
/// ```
///
/// ```ignore
/// if result.is_err() {
/// return result;
/// }
/// ```
///
/// If it matches, it will suggest to use the question mark operator instead
fn check_is_none_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
if !self.inside_try_block()
&& let Some(higher::If { cond, then, r#else }) = higher::If::hir(expr)
&& !is_else_clause(cx.tcx, expr)
&& let ExprKind::MethodCall(segment, caller, ..) = &cond.kind
&& let caller_ty = cx.typeck_results().expr_ty(caller)
&& let if_block = IfBlockType::IfIs(caller, caller_ty, segment.ident.name, then)
&& (is_early_return(sym::Option, cx, &if_block) || is_early_return(sym::Result, cx, &if_block))
{
let mut applicability = Applicability::MachineApplicable;
let receiver_str = snippet_with_applicability(cx, caller.span, "..", &mut applicability);
let by_ref = !caller_ty.is_copy_modulo_regions(cx.tcx, cx.param_env)
&& !matches!(caller.kind, ExprKind::Call(..) | ExprKind::MethodCall(..));
let sugg = if let Some(else_inner) = r#else {
if eq_expr_value(cx, caller, peel_blocks(else_inner)) {
format!("Some({receiver_str}?)")
} else {
return;
}
} else {
format!("{receiver_str}{}?;", if by_ref { ".as_ref()" } else { "" })
};
span_lint_and_sugg(
cx,
QUESTION_MARK,
expr.span,
"this block may be rewritten with the `?` operator",
"replace it with",
sugg,
applicability,
);
}
}
fn check_if_let_some_or_err_and_early_return<'tcx>(&self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
if !self.inside_try_block()
&& let Some(higher::IfLet {
let_pat,
let_expr,
if_then,
if_else,
..
}) = higher::IfLet::hir(cx, expr)
&& !is_else_clause(cx.tcx, expr)
&& let PatKind::TupleStruct(ref path1, [field], ddpos) = let_pat.kind
&& ddpos.as_opt_usize().is_none()
&& let PatKind::Binding(BindingAnnotation(by_ref, _), bind_id, ident, None) = field.kind
&& let caller_ty = cx.typeck_results().expr_ty(let_expr)
&& let if_block = IfBlockType::IfLet(
cx.qpath_res(path1, let_pat.hir_id),
caller_ty,
ident.name,
let_expr,
if_then,
if_else,
)
&& ((is_early_return(sym::Option, cx, &if_block) && path_to_local_id(peel_blocks(if_then), bind_id))
|| is_early_return(sym::Result, cx, &if_block))
&& if_else
.map(|e| eq_expr_value(cx, let_expr, peel_blocks(e)))
.filter(|e| *e)
.is_none()
{
let mut applicability = Applicability::MachineApplicable;
let receiver_str = snippet_with_applicability(cx, let_expr.span, "..", &mut applicability);
let requires_semi = matches!(cx.tcx.parent_hir_node(expr.hir_id), Node::Stmt(_));
let sugg = format!(
"{receiver_str}{}?{}",
if by_ref == ByRef::Yes { ".as_ref()" } else { "" },
if requires_semi { ";" } else { "" }
);
span_lint_and_sugg(
cx,
QUESTION_MARK,
expr.span,
"this block may be rewritten with the `?` operator",
"replace it with",
sugg,
applicability,
);
}
}
}
fn is_try_block(cx: &LateContext<'_>, bl: &rustc_hir::Block<'_>) -> bool {
@ -324,15 +322,18 @@ fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'_>) {
return;
}
if !in_constant(cx, stmt.hir_id) {
if !self.inside_try_block() && !in_constant(cx, stmt.hir_id) {
check_let_some_else_return_none(cx, stmt);
}
self.check_manual_let_else(cx, stmt);
}
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if !in_constant(cx, expr.hir_id) && is_lint_allowed(cx, QUESTION_MARK_USED, expr.hir_id) {
self.check_is_none_or_err_and_early_return(cx, expr);
self.check_if_let_some_or_err_and_early_return(cx, expr);
if !self.inside_try_block()
&& !in_constant(cx, expr.hir_id)
&& is_lint_allowed(cx, QUESTION_MARK_USED, expr.hir_id)
{
check_is_none_or_err_and_early_return(cx, expr);
check_if_let_some_or_err_and_early_return(cx, expr);
}
}

View File

@ -1,3 +1,4 @@
#![feature(try_blocks)]
#![allow(unused_braces, unused_variables, dead_code)]
#![allow(
clippy::collapsible_else_if,
@ -446,3 +447,12 @@ struct U<T> {
w: T,
x: T,
}
fn issue12337() {
// We want to generally silence question_mark lints within try blocks, since `?` has different
// behavior to `return`, and question_mark calls into manual_let_else logic, so make sure that
// we still emit a lint for manual_let_else
let _: Option<()> = try {
let v = if let Some(v_some) = g() { v_some } else { return };
};
}

View File

@ -1,5 +1,5 @@
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:27:5
--> tests/ui/manual_let_else.rs:28:5
|
LL | let v = if let Some(v_some) = g() { v_some } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some(v) = g() else { return };`
@ -8,7 +8,7 @@ LL | let v = if let Some(v_some) = g() { v_some } else { return };
= help: to override `-D warnings` add `#[allow(clippy::manual_let_else)]`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:30:5
--> tests/ui/manual_let_else.rs:31:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -26,7 +26,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:37:5
--> tests/ui/manual_let_else.rs:38:5
|
LL | / let v = if let Some(v) = g() {
LL | |
@ -47,25 +47,25 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:49:9
--> tests/ui/manual_let_else.rs:50:9
|
LL | let v = if let Some(v_some) = g() { v_some } else { continue };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some(v) = g() else { continue };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:51:9
--> tests/ui/manual_let_else.rs:52:9
|
LL | let v = if let Some(v_some) = g() { v_some } else { break };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some(v) = g() else { break };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:56:5
--> tests/ui/manual_let_else.rs:57:5
|
LL | let v = if let Some(v_some) = g() { v_some } else { panic!() };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some(v) = g() else { panic!() };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:60:5
--> tests/ui/manual_let_else.rs:61:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -83,7 +83,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:68:5
--> tests/ui/manual_let_else.rs:69:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -101,7 +101,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:76:5
--> tests/ui/manual_let_else.rs:77:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -121,7 +121,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:85:5
--> tests/ui/manual_let_else.rs:86:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -141,7 +141,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:94:5
--> tests/ui/manual_let_else.rs:95:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -168,7 +168,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:110:5
--> tests/ui/manual_let_else.rs:111:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -190,7 +190,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:121:5
--> tests/ui/manual_let_else.rs:122:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -217,7 +217,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:137:5
--> tests/ui/manual_let_else.rs:138:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -239,7 +239,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:148:5
--> tests/ui/manual_let_else.rs:149:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -257,7 +257,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:156:5
--> tests/ui/manual_let_else.rs:157:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -278,7 +278,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:166:5
--> tests/ui/manual_let_else.rs:167:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -299,7 +299,7 @@ LL + } };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:176:5
--> tests/ui/manual_let_else.rs:177:5
|
LL | / let v = if let Some(v_some) = g() {
LL | |
@ -328,7 +328,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:194:5
--> tests/ui/manual_let_else.rs:195:5
|
LL | / let (v, w) = if let Some(v_some) = g().map(|v| (v, 42)) {
LL | |
@ -346,7 +346,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:202:5
--> tests/ui/manual_let_else.rs:203:5
|
LL | / let (w, S { v }) = if let (Some(v_some), w_some) = (g().map(|_| S { v: 0 }), 0) {
LL | |
@ -364,7 +364,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:212:13
--> tests/ui/manual_let_else.rs:213:13
|
LL | let $n = if let Some(v) = $e { v } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some($n) = g() else { return };`
@ -375,19 +375,19 @@ LL | create_binding_if_some!(w, g());
= note: this error originates in the macro `create_binding_if_some` (in Nightly builds, run with -Z macro-backtrace for more info)
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:221:5
--> tests/ui/manual_let_else.rs:222:5
|
LL | let v = if let Variant::A(a, 0) = e() { a } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Variant::A(v, 0) = e() else { return };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:225:5
--> tests/ui/manual_let_else.rs:226:5
|
LL | let mut v = if let Variant::B(b) = e() { b } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Variant::B(mut v) = e() else { return };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:230:5
--> tests/ui/manual_let_else.rs:231:5
|
LL | / let v = if let Ok(Some(Variant::B(b))) | Err(Some(Variant::A(b, _))) = nested {
LL | |
@ -405,19 +405,19 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:237:5
--> tests/ui/manual_let_else.rs:238:5
|
LL | let v = if let Variant::A(.., a) = e() { a } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Variant::A(.., v) = e() else { return };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:241:5
--> tests/ui/manual_let_else.rs:242:5
|
LL | let w = if let (Some(v), ()) = (g(), ()) { v } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let (Some(w), ()) = (g(), ()) else { return };`
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:245:5
--> tests/ui/manual_let_else.rs:246:5
|
LL | / let w = if let Some(S { v: x }) = Some(S { v: 0 }) {
LL | |
@ -435,7 +435,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:253:5
--> tests/ui/manual_let_else.rs:254:5
|
LL | / let v = if let Some(S { v: x }) = Some(S { v: 0 }) {
LL | |
@ -453,7 +453,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:261:5
--> tests/ui/manual_let_else.rs:262:5
|
LL | / let (x, S { v }, w) = if let Some(U { v, w, x }) = None::<U<S<()>>> {
LL | |
@ -471,7 +471,7 @@ LL + };
|
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:378:5
--> tests/ui/manual_let_else.rs:379:5
|
LL | / let _ = match ff {
LL | |
@ -480,5 +480,11 @@ LL | | _ => macro_call!(),
LL | | };
| |______^ help: consider writing: `let Some(_) = ff else { macro_call!() };`
error: aborting due to 30 previous errors
error: this could be rewritten as `let...else`
--> tests/ui/manual_let_else.rs:456:9
|
LL | let v = if let Some(v_some) = g() { v_some } else { return };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider writing: `let Some(v) = g() else { return };`
error: aborting due to 31 previous errors

View File

@ -273,3 +273,13 @@ const fn issue9175(option: Option<()>) -> Option<()> {
//stuff
Some(())
}
fn issue12337() -> Option<i32> {
let _: Option<i32> = try {
let Some(_) = Some(42) else {
return None;
};
123
};
Some(42)
}

View File

@ -313,3 +313,13 @@ const fn issue9175(option: Option<()>) -> Option<()> {
//stuff
Some(())
}
fn issue12337() -> Option<i32> {
let _: Option<i32> = try {
let Some(_) = Some(42) else {
return None;
};
123
};
Some(42)
}