Skip to content

Commit 76ecbef

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 76ecbef

File tree

5 files changed

+293
-7
lines changed

5 files changed

+293
-7
lines changed

clippy_lints/src/methods/return_and_then.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use rustc_errors::Applicability;
2-
use rustc_hir as hir;
2+
use rustc_hir::{self as hir, Node};
33
use rustc_lint::LateContext;
44
use rustc_middle::ty::{self, GenericArg, Ty};
55
use rustc_span::sym;
@@ -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::{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

@@ -55,9 +55,15 @@ pub(super) fn check<'tcx>(
5555
None => &body_snip,
5656
};
5757

58-
// If suggestion is going to get inserted as part of a `return` expression, it must be blockified.
59-
let sugg = if let Some(parent_expr) = get_parent_expr(cx, expr) {
60-
let base_indent = indent_of(cx, parent_expr.span);
58+
// If suggestion is going to get inserted as part of a `return` expression or as a match expression
59+
// arm, it must be blockified.
60+
let parent_span_for_indent = match cx.tcx.parent_hir_node(expr.hir_id) {
61+
Node::Expr(parent_expr) => Some(parent_expr.span),
62+
Node::Arm(arm) => Some(arm.span),
63+
_ => None,
64+
};
65+
let sugg = if let Some(span) = parent_span_for_indent {
66+
let base_indent = indent_of(cx, span);
6167
let inner_indent = base_indent.map(|i| i + 4);
6268
format!(
6369
"{}\n{}\n{}",

clippy_utils/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3497,3 +3497,47 @@ 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::Arm(arm) if arm.body.hir_id == prev_id => {},
3527+
Node::Expr(expr) => match expr.kind {
3528+
ExprKind::Ret(_) => return true,
3529+
ExprKind::If(_, then, opt_else)
3530+
if then.hir_id == prev_id || opt_else.is_some_and(|els| els.hir_id == prev_id) => {},
3531+
ExprKind::Match(_, arms, _) if arms.iter().any(|arm| arm.hir_id == prev_id) => {},
3532+
ExprKind::Block(block, _) if block.hir_id == prev_id => {},
3533+
_ => break,
3534+
},
3535+
_ => break,
3536+
}
3537+
prev_id = hir_id;
3538+
}
3539+
3540+
// `expr` is used as part of "something" and is not returned directly from its
3541+
// enclosing body.
3542+
false
3543+
}

tests/ui/return_and_then.fixed

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,57 @@ fn main() {
9999
};
100100
None
101101
}
102+
103+
#[expect(clippy::diverging_sub_expression)]
104+
fn with_return_in_expression() -> Option<i32> {
105+
_ = (
106+
return {
107+
let x = Some("")?;
108+
if x.len() > 2 { Some(3) } else { None }
109+
},
110+
//~^ return_and_then
111+
10,
112+
);
113+
}
114+
115+
fn inside_if(a: bool, i: Option<u32>) -> Option<u32> {
116+
if a {
117+
let i = i?;
118+
if i > 3 { Some(i) } else { None }
119+
//~^ return_and_then
120+
} else {
121+
Some(42)
122+
}
123+
}
124+
125+
fn inside_match(a: u32, i: Option<u32>) -> Option<u32> {
126+
match a {
127+
1 | 2 => {
128+
let i = i?;
129+
if i > 3 { Some(i) } else { None }
130+
},
131+
//~^ return_and_then
132+
3 | 4 => Some(42),
133+
_ => None,
134+
}
135+
}
136+
137+
fn inside_match_and_block_and_if(a: u32, i: Option<u32>) -> Option<u32> {
138+
match a {
139+
1 | 2 => {
140+
let a = a * 3;
141+
if a.is_multiple_of(2) {
142+
let i = i?;
143+
if i > 3 { Some(i) } else { None }
144+
//~^ return_and_then
145+
} else {
146+
Some(10)
147+
}
148+
},
149+
3 | 4 => Some(42),
150+
_ => None,
151+
}
152+
}
102153
}
103154

104155
fn gen_option(n: i32) -> Option<i32> {
@@ -124,3 +175,48 @@ mod issue14781 {
124175
Ok(())
125176
}
126177
}
178+
179+
mod issue15111 {
180+
#[derive(Debug)]
181+
struct EvenOdd {
182+
even: Option<u32>,
183+
odd: Option<u32>,
184+
}
185+
186+
impl EvenOdd {
187+
fn new(i: Option<u32>) -> Self {
188+
Self {
189+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
190+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
191+
}
192+
}
193+
}
194+
195+
fn with_if_let(i: Option<u32>) -> u32 {
196+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
197+
x
198+
} else {
199+
std::hint::black_box(0)
200+
}
201+
}
202+
203+
fn main() {
204+
let _ = EvenOdd::new(Some(2));
205+
}
206+
}
207+
208+
mod issue14927 {
209+
use std::path::Path;
210+
struct A {
211+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
212+
}
213+
const MY_A: A = A {
214+
func: |check, a, b| {
215+
if check {
216+
let _ = ();
217+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
218+
let _ = ();
219+
}
220+
},
221+
};
222+
}

tests/ui/return_and_then.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,49 @@ fn main() {
9090
};
9191
None
9292
}
93+
94+
#[expect(clippy::diverging_sub_expression)]
95+
fn with_return_in_expression() -> Option<i32> {
96+
_ = (
97+
return Some("").and_then(|x| if x.len() > 2 { Some(3) } else { None }),
98+
//~^ return_and_then
99+
10,
100+
);
101+
}
102+
103+
fn inside_if(a: bool, i: Option<u32>) -> Option<u32> {
104+
if a {
105+
i.and_then(|i| if i > 3 { Some(i) } else { None })
106+
//~^ return_and_then
107+
} else {
108+
Some(42)
109+
}
110+
}
111+
112+
fn inside_match(a: u32, i: Option<u32>) -> Option<u32> {
113+
match a {
114+
1 | 2 => i.and_then(|i| if i > 3 { Some(i) } else { None }),
115+
//~^ return_and_then
116+
3 | 4 => Some(42),
117+
_ => None,
118+
}
119+
}
120+
121+
fn inside_match_and_block_and_if(a: u32, i: Option<u32>) -> Option<u32> {
122+
match a {
123+
1 | 2 => {
124+
let a = a * 3;
125+
if a.is_multiple_of(2) {
126+
i.and_then(|i| if i > 3 { Some(i) } else { None })
127+
//~^ return_and_then
128+
} else {
129+
Some(10)
130+
}
131+
},
132+
3 | 4 => Some(42),
133+
_ => None,
134+
}
135+
}
93136
}
94137

95138
fn gen_option(n: i32) -> Option<i32> {
@@ -115,3 +158,48 @@ mod issue14781 {
115158
Ok(())
116159
}
117160
}
161+
162+
mod issue15111 {
163+
#[derive(Debug)]
164+
struct EvenOdd {
165+
even: Option<u32>,
166+
odd: Option<u32>,
167+
}
168+
169+
impl EvenOdd {
170+
fn new(i: Option<u32>) -> Self {
171+
Self {
172+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
173+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
174+
}
175+
}
176+
}
177+
178+
fn with_if_let(i: Option<u32>) -> u32 {
179+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
180+
x
181+
} else {
182+
std::hint::black_box(0)
183+
}
184+
}
185+
186+
fn main() {
187+
let _ = EvenOdd::new(Some(2));
188+
}
189+
}
190+
191+
mod issue14927 {
192+
use std::path::Path;
193+
struct A {
194+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
195+
}
196+
const MY_A: A = A {
197+
func: |check, a, b| {
198+
if check {
199+
let _ = ();
200+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
201+
let _ = ();
202+
}
203+
},
204+
};
205+
}

tests/ui/return_and_then.stderr

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,5 +146,57 @@ LL + if x.len() > 2 { Some(3) } else { None }
146146
LL ~ };
147147
|
148148

149-
error: aborting due to 10 previous errors
149+
error: use the `?` operator instead of an `and_then` call
150+
--> tests/ui/return_and_then.rs:97:20
151+
|
152+
LL | return Some("").and_then(|x| if x.len() > 2 { Some(3) } else { None }),
153+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
154+
|
155+
help: try
156+
|
157+
LL ~ return {
158+
LL + let x = Some("")?;
159+
LL + if x.len() > 2 { Some(3) } else { None }
160+
LL ~ },
161+
|
162+
163+
error: use the `?` operator instead of an `and_then` call
164+
--> tests/ui/return_and_then.rs:105:13
165+
|
166+
LL | i.and_then(|i| if i > 3 { Some(i) } else { None })
167+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
168+
|
169+
help: try
170+
|
171+
LL ~ let i = i?;
172+
LL + if i > 3 { Some(i) } else { None }
173+
|
174+
175+
error: use the `?` operator instead of an `and_then` call
176+
--> tests/ui/return_and_then.rs:114:22
177+
|
178+
LL | 1 | 2 => i.and_then(|i| if i > 3 { Some(i) } else { None }),
179+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
180+
|
181+
help: try
182+
|
183+
LL ~ 1 | 2 => {
184+
LL + let i = i?;
185+
LL + if i > 3 { Some(i) } else { None }
186+
LL ~ },
187+
|
188+
189+
error: use the `?` operator instead of an `and_then` call
190+
--> tests/ui/return_and_then.rs:126:21
191+
|
192+
LL | i.and_then(|i| if i > 3 { Some(i) } else { None })
193+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
194+
|
195+
help: try
196+
|
197+
LL ~ let i = i?;
198+
LL + if i > 3 { Some(i) } else { None }
199+
|
200+
201+
error: aborting due to 14 previous errors
150202

0 commit comments

Comments
 (0)