Skip to content

Commit cc7b2f5

Browse files
authored
Properly check that an expression might be the one returned (#15115)
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. changelog: [`return_and_then`]: prevent false positives in case of a partially used expression Fixes #15111 Fixes #14927 <!-- TRIAGEBOT_START --> <!-- TRIAGEBOT_SUMMARY_START --> ### Summary Notes - [Beta-nomination](#15115 (comment)) by [samueltardieu](https://github.com/samueltardieu) *Managed by `@rustbot`—see [help](https://forge.rust-lang.org/triagebot/note.html) for details* <!-- TRIAGEBOT_SUMMARY_END --> <!-- TRIAGEBOT_END -->
2 parents 5c51a1d + 89f7a43 commit cc7b2f5

File tree

5 files changed

+422
-9
lines changed

5 files changed

+422
-9
lines changed

clippy_lints/src/methods/return_and_then.rs

Lines changed: 21 additions & 8 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,15 +55,28 @@ 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, opening_paren, closing_paren) = match cx.tcx.parent_hir_node(expr.hir_id) {
61+
Node::Expr(parent_expr) if matches!(parent_expr.kind, hir::ExprKind::Break(..)) => {
62+
(Some(parent_expr.span), "(", ")")
63+
},
64+
Node::Expr(parent_expr) => (Some(parent_expr.span), "", ""),
65+
Node::Arm(match_arm) => (Some(match_arm.span), "", ""),
66+
_ => (None, "", ""),
67+
};
68+
let sugg = if let Some(span) = parent_span_for_indent {
69+
let base_indent = indent_of(cx, span);
6170
let inner_indent = base_indent.map(|i| i + 4);
6271
format!(
6372
"{}\n{}\n{}",
64-
reindent_multiline(&format!("{{\nlet {arg_snip} = {recv_snip}?;"), true, inner_indent),
73+
reindent_multiline(
74+
&format!("{opening_paren}{{\nlet {arg_snip} = {recv_snip}?;"),
75+
true,
76+
inner_indent
77+
),
6578
reindent_multiline(inner, false, inner_indent),
66-
reindent_multiline("}", false, base_indent),
79+
reindent_multiline(&format!("}}{closing_paren}"), false, base_indent),
6780
)
6881
} else {
6982
format!(

clippy_utils/src/lib.rs

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

tests/ui/return_and_then.fixed

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,92 @@ 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+
}
153+
154+
#[expect(clippy::never_loop)]
155+
fn with_break(i: Option<u32>) -> Option<u32> {
156+
match i {
157+
Some(1) => loop {
158+
break ({
159+
let i = i?;
160+
if i > 3 { Some(i) } else { None }
161+
});
162+
//~^ return_and_then
163+
},
164+
Some(2) => 'foo: loop {
165+
loop {
166+
break 'foo ({
167+
let i = i?;
168+
if i > 3 { Some(i) } else { None }
169+
});
170+
//~^ return_and_then
171+
}
172+
},
173+
Some(3) => 'bar: {
174+
break 'bar ({
175+
let i = i?;
176+
if i > 3 { Some(i) } else { None }
177+
});
178+
//~^ return_and_then
179+
},
180+
Some(4) => 'baz: loop {
181+
_ = loop {
182+
break i.and_then(|i| if i > 3 { Some(i) } else { None });
183+
};
184+
},
185+
_ => None,
186+
}
187+
}
102188
}
103189

104190
fn gen_option(n: i32) -> Option<i32> {
@@ -124,3 +210,48 @@ mod issue14781 {
124210
Ok(())
125211
}
126212
}
213+
214+
mod issue15111 {
215+
#[derive(Debug)]
216+
struct EvenOdd {
217+
even: Option<u32>,
218+
odd: Option<u32>,
219+
}
220+
221+
impl EvenOdd {
222+
fn new(i: Option<u32>) -> Self {
223+
Self {
224+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
225+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
226+
}
227+
}
228+
}
229+
230+
fn with_if_let(i: Option<u32>) -> u32 {
231+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
232+
x
233+
} else {
234+
std::hint::black_box(0)
235+
}
236+
}
237+
238+
fn main() {
239+
let _ = EvenOdd::new(Some(2));
240+
}
241+
}
242+
243+
mod issue14927 {
244+
use std::path::Path;
245+
struct A {
246+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
247+
}
248+
const MY_A: A = A {
249+
func: |check, a, b| {
250+
if check {
251+
let _ = ();
252+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
253+
let _ = ();
254+
}
255+
},
256+
};
257+
}

tests/ui/return_and_then.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,75 @@ 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+
}
136+
137+
#[expect(clippy::never_loop)]
138+
fn with_break(i: Option<u32>) -> Option<u32> {
139+
match i {
140+
Some(1) => loop {
141+
break i.and_then(|i| if i > 3 { Some(i) } else { None });
142+
//~^ return_and_then
143+
},
144+
Some(2) => 'foo: loop {
145+
loop {
146+
break 'foo i.and_then(|i| if i > 3 { Some(i) } else { None });
147+
//~^ return_and_then
148+
}
149+
},
150+
Some(3) => 'bar: {
151+
break 'bar i.and_then(|i| if i > 3 { Some(i) } else { None });
152+
//~^ return_and_then
153+
},
154+
Some(4) => 'baz: loop {
155+
_ = loop {
156+
break i.and_then(|i| if i > 3 { Some(i) } else { None });
157+
};
158+
},
159+
_ => None,
160+
}
161+
}
93162
}
94163

95164
fn gen_option(n: i32) -> Option<i32> {
@@ -115,3 +184,48 @@ mod issue14781 {
115184
Ok(())
116185
}
117186
}
187+
188+
mod issue15111 {
189+
#[derive(Debug)]
190+
struct EvenOdd {
191+
even: Option<u32>,
192+
odd: Option<u32>,
193+
}
194+
195+
impl EvenOdd {
196+
fn new(i: Option<u32>) -> Self {
197+
Self {
198+
even: i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }),
199+
odd: i.and_then(|i| if i.is_multiple_of(2) { None } else { Some(i) }),
200+
}
201+
}
202+
}
203+
204+
fn with_if_let(i: Option<u32>) -> u32 {
205+
if let Some(x) = i.and_then(|i| if i.is_multiple_of(2) { Some(i) } else { None }) {
206+
x
207+
} else {
208+
std::hint::black_box(0)
209+
}
210+
}
211+
212+
fn main() {
213+
let _ = EvenOdd::new(Some(2));
214+
}
215+
}
216+
217+
mod issue14927 {
218+
use std::path::Path;
219+
struct A {
220+
pub func: fn(check: bool, a: &Path, b: Option<&Path>),
221+
}
222+
const MY_A: A = A {
223+
func: |check, a, b| {
224+
if check {
225+
let _ = ();
226+
} else if let Some(parent) = b.and_then(|p| p.parent()) {
227+
let _ = ();
228+
}
229+
},
230+
};
231+
}

0 commit comments

Comments
 (0)