Skip to content

Commit d54ed67

Browse files
committed
Properly check that an expression might be the one returned
The `TyCtxt::hir_get_fn_id_for_return_block()` function was too broad, as it will return positively even when given part of an expression that can be used as a return value. A new `potential_return_of_enclosing_body()` utility function has been made to represent the fact that an expression might be directly returned from its enclosing body.
1 parent 76118ec commit d54ed67

File tree

4 files changed

+135
-2
lines changed

4 files changed

+135
-2
lines changed

clippy_lints/src/methods/return_and_then.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use clippy_utils::diagnostics::span_lint_and_sugg;
99
use clippy_utils::source::{indent_of, reindent_multiline, snippet_with_applicability};
1010
use clippy_utils::ty::get_type_diagnostic_name;
1111
use clippy_utils::visitors::for_each_unconsumed_temporary;
12-
use clippy_utils::{get_parent_expr, peel_blocks};
12+
use clippy_utils::{get_parent_expr, peel_blocks, potential_return_of_enclosing_body};
1313

1414
use super::RETURN_AND_THEN;
1515

@@ -21,7 +21,7 @@ pub(super) fn check<'tcx>(
2121
recv: &'tcx hir::Expr<'tcx>,
2222
arg: &'tcx hir::Expr<'_>,
2323
) {
24-
if cx.tcx.hir_get_fn_id_for_return_block(expr.hir_id).is_none() {
24+
if !potential_return_of_enclosing_body(cx, expr) {
2525
return;
2626
}
2727

clippy_utils/src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3497,3 +3497,46 @@ pub fn is_expr_default<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) ->
34973497
false
34983498
}
34993499
}
3500+
3501+
/// Checks if `expr` may be directly used as the return value of its enclosing body.
3502+
/// The following cases are covered:
3503+
/// - `expr` as the last expression of the body, or of a block that can be used as the return value
3504+
/// - `return expr`
3505+
/// - then or else part of a `if` in return position
3506+
/// - arm body of a `match` in a return position
3507+
///
3508+
/// Contrary to [`TyCtxt::hir_get_fn_id_for_return_block()`], if `expr` is part of a
3509+
/// larger expression, for example a field expression of a `struct`, it will not be
3510+
/// considered as matching the condition and will return `false`.
3511+
///
3512+
/// Also, even if `expr` is assigned to a variable which is later returned, this function
3513+
/// will still return `false` because `expr` is not used *directly* as the return value
3514+
/// as it goes through the intermediate variable.
3515+
pub fn potential_return_of_enclosing_body(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
3516+
let enclosing_body_owner = cx
3517+
.tcx
3518+
.local_def_id_to_hir_id(cx.tcx.hir_enclosing_body_owner(expr.hir_id));
3519+
let mut prev_id = expr.hir_id;
3520+
for (hir_id, node) in cx.tcx.hir_parent_iter(expr.hir_id) {
3521+
if hir_id == enclosing_body_owner {
3522+
return true;
3523+
}
3524+
match node {
3525+
Node::Block(Block { expr, .. }) if expr.is_some_and(|expr| expr.hir_id == prev_id) => {},
3526+
Node::Expr(expr) => match expr.kind {
3527+
ExprKind::Ret(_) => return true,
3528+
ExprKind::If(_, then, opt_else)
3529+
if then.hir_id == prev_id || opt_else.is_some_and(|els| els.hir_id == prev_id) => {},
3530+
ExprKind::Match(_, arms, _) if arms.iter().any(|arm| arm.body.hir_id == prev_id) => {},
3531+
ExprKind::Block(block, _) if block.hir_id == prev_id => {},
3532+
_ => break,
3533+
},
3534+
_ => break,
3535+
}
3536+
prev_id = hir_id;
3537+
}
3538+
3539+
// `expr` is used as part of "something" and is not returned directly from its
3540+
// enclosing body.
3541+
false
3542+
}

tests/ui/return_and_then.fixed

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,48 @@ mod issue14781 {
124124
Ok(())
125125
}
126126
}
127+
128+
mod issue15111 {
129+
#[derive(Debug)]
130+
struct EvenOdd {
131+
even: Option<u32>,
132+
odd: Option<u32>,
133+
}
134+
135+
impl EvenOdd {
136+
fn new(i: Option<u32>) -> Self {
137+
Self {
138+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
139+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
140+
}
141+
}
142+
}
143+
144+
fn with_if_let(i: Option<u32>) -> u32 {
145+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
146+
x
147+
} else {
148+
std::hint::black_box(0)
149+
}
150+
}
151+
152+
fn main() {
153+
let _ = EvenOdd::new(Some(2));
154+
}
155+
}
156+
157+
mod issue14927 {
158+
use std::path::Path;
159+
struct A {
160+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
161+
}
162+
const MY_A: A = A {
163+
func: |check, a, b| {
164+
if check {
165+
let _ = ();
166+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
167+
let _ = ();
168+
}
169+
},
170+
};
171+
}

tests/ui/return_and_then.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,48 @@ mod issue14781 {
115115
Ok(())
116116
}
117117
}
118+
119+
mod issue15111 {
120+
#[derive(Debug)]
121+
struct EvenOdd {
122+
even: Option<u32>,
123+
odd: Option<u32>,
124+
}
125+
126+
impl EvenOdd {
127+
fn new(i: Option<u32>) -> Self {
128+
Self {
129+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
130+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
131+
}
132+
}
133+
}
134+
135+
fn with_if_let(i: Option<u32>) -> u32 {
136+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
137+
x
138+
} else {
139+
std::hint::black_box(0)
140+
}
141+
}
142+
143+
fn main() {
144+
let _ = EvenOdd::new(Some(2));
145+
}
146+
}
147+
148+
mod issue14927 {
149+
use std::path::Path;
150+
struct A {
151+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
152+
}
153+
const MY_A: A = A {
154+
func: |check, a, b| {
155+
if check {
156+
let _ = ();
157+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
158+
let _ = ();
159+
}
160+
},
161+
};
162+
}

0 commit comments

Comments
 (0)