Skip to content

Commit b4bc45c

Browse files
committed
hoist statements before break out of the loop
add test cases for semicolon_less and multiple stmts extract could_be_while_let fn param into a struct fix clippy warnings
1 parent 60b3ecf commit b4bc45c

4 files changed

Lines changed: 292 additions & 41 deletions

File tree

clippy_lints/src/loops/while_let_loop.rs

Lines changed: 128 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
use super::WHILE_LET_LOOP;
22
use clippy_utils::diagnostics::span_lint_and_sugg;
3-
use clippy_utils::source::{snippet, snippet_indent, snippet_opt};
3+
use clippy_utils::source::{snippet, snippet_indent, snippet_opt, snippet_with_context};
44
use clippy_utils::ty::needs_ordered_drop;
5-
use clippy_utils::visitors::any_temporaries_need_ordered_drop;
5+
use clippy_utils::visitors::{any_temporaries_need_ordered_drop, for_each_expr_without_closures};
66
use clippy_utils::{higher, peel_blocks};
77
use rustc_ast::BindingMode;
88
use rustc_errors::Applicability;
9-
use rustc_hir::{Block, Expr, ExprKind, LetStmt, MatchSource, Pat, PatKind, Path, QPath, StmtKind, Ty};
9+
use rustc_hir::{
10+
Block, Destination, Expr, ExprKind, LetStmt, MatchSource, Pat, PatKind, Path, QPath, Stmt, StmtKind, Ty,
11+
};
1012
use rustc_lint::LateContext;
13+
use std::fmt::Write;
14+
use std::ops::ControlFlow;
1115

1216
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_block: &'tcx Block<'_>) {
1317
let (init, let_info, els) = match (loop_block.stmts, loop_block.expr) {
@@ -34,16 +38,46 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
3438
could_be_while_let(
3539
cx,
3640
expr,
37-
if_let.let_pat,
38-
if_let.let_expr,
39-
has_trailing_exprs,
40-
let_info,
41-
Some(if_let.if_then),
41+
WhileLetInfo {
42+
let_pat: if_let.let_pat,
43+
let_expr: if_let.let_expr,
44+
has_trailing_exprs,
45+
let_info,
46+
inner_expr: Some(if_let.if_then),
47+
hoistable_stmts: None,
48+
},
4249
);
4350
} else if els.is_some_and(is_simple_break_block)
4451
&& let Some((pat, _)) = let_info
4552
{
46-
could_be_while_let(cx, expr, pat, init, has_trailing_exprs, let_info, None);
53+
could_be_while_let(
54+
cx,
55+
expr,
56+
WhileLetInfo {
57+
let_pat: pat,
58+
let_expr: init,
59+
has_trailing_exprs,
60+
let_info,
61+
inner_expr: None,
62+
hoistable_stmts: None,
63+
},
64+
);
65+
} else if let Some(els_block) = els
66+
&& let Some((pat, _)) = let_info
67+
&& let Some(hoistable) = extract_hoistable_stmts(els_block)
68+
{
69+
could_be_while_let(
70+
cx,
71+
expr,
72+
WhileLetInfo {
73+
let_pat: pat,
74+
let_expr: init,
75+
has_trailing_exprs,
76+
let_info,
77+
inner_expr: None,
78+
hoistable_stmts: Some(hoistable),
79+
},
80+
);
4781
} else if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal) = init.kind
4882
&& arm1.guard.is_none()
4983
&& arm2.guard.is_none()
@@ -52,11 +86,14 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
5286
could_be_while_let(
5387
cx,
5488
expr,
55-
arm1.pat,
56-
scrutinee,
57-
has_trailing_exprs,
58-
let_info,
59-
Some(arm1.body),
89+
WhileLetInfo {
90+
let_pat: arm1.pat,
91+
let_expr: scrutinee,
92+
has_trailing_exprs,
93+
let_info,
94+
inner_expr: Some(arm1.body),
95+
hoistable_stmts: None,
96+
},
6097
);
6198
}
6299
}
@@ -81,15 +118,26 @@ fn is_simple_break_expr(expr: &Expr<'_>) -> bool {
81118
}
82119
}
83120

84-
fn could_be_while_let<'tcx>(
85-
cx: &LateContext<'tcx>,
86-
expr: &'tcx Expr<'_>,
87-
let_pat: &'tcx Pat<'_>,
88-
let_expr: &'tcx Expr<'_>,
121+
#[derive(Copy, Clone)]
122+
struct WhileLetInfo<'tcx> {
123+
let_pat: &'tcx Pat<'tcx>,
124+
let_expr: &'tcx Expr<'tcx>,
89125
has_trailing_exprs: bool,
90-
let_info: Option<(&Pat<'_>, Option<&Ty<'_>>)>,
91-
inner_expr: Option<&Expr<'_>>,
92-
) {
126+
let_info: Option<(&'tcx Pat<'tcx>, Option<&'tcx Ty<'tcx>>)>,
127+
inner_expr: Option<&'tcx Expr<'tcx>>,
128+
hoistable_stmts: Option<&'tcx [Stmt<'tcx>]>,
129+
}
130+
131+
fn could_be_while_let<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, info: WhileLetInfo<'tcx>) {
132+
let WhileLetInfo {
133+
let_pat,
134+
let_expr,
135+
has_trailing_exprs,
136+
let_info,
137+
inner_expr,
138+
hoistable_stmts,
139+
} = info;
140+
93141
if has_trailing_exprs
94142
&& (needs_ordered_drop(cx, cx.typeck_results().expr_ty(let_expr))
95143
|| any_temporaries_need_ordered_drop(cx, let_expr))
@@ -98,11 +146,13 @@ fn could_be_while_let<'tcx>(
98146
return;
99147
}
100148

149+
let indent = snippet_indent(cx, expr.span).unwrap_or_default();
150+
101151
// NOTE: we used to build a body here instead of using
102152
// ellipsis, this was removed because:
103153
// 1) it was ugly with big bodies;
104154
// 2) it was not indented properly;
105-
// 3) it wasnt very smart (see #675).
155+
// 3) it wasn't very smart (see #675).
106156
let inner_content = if let Some(((pat, ty), inner_expr)) = let_info.zip(inner_expr)
107157
// Prevent trivial reassignments such as `let x = x;` or `let _ = …;`, but
108158
// keep them if the type has been explicitly specified.
@@ -113,22 +163,37 @@ fn could_be_while_let<'tcx>(
113163
let ty_str = ty
114164
.map(|ty| format!(": {}", snippet(cx, ty.span, "_")))
115165
.unwrap_or_default();
116-
format!(
117-
"\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}",
118-
indent = snippet_indent(cx, expr.span).unwrap_or_default(),
119-
)
166+
format!("\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}")
120167
} else {
121168
" .. ".into()
122169
};
123170

171+
// Build the hoisted statements string in case we have statements to hoist
172+
let hoisted_content = if let Some(stmts) = hoistable_stmts {
173+
let mut hoisted = String::new();
174+
let outer_ctxt = expr.span.ctxt();
175+
for stmt in stmts {
176+
let (stmt_str, _) = snippet_with_context(cx, stmt.span, outer_ctxt, "..", &mut Applicability::Unspecified);
177+
let semi = if matches!(stmt.kind, StmtKind::Semi(_)) {
178+
";"
179+
} else {
180+
""
181+
};
182+
let _ = write!(hoisted, "\n{indent}{stmt_str}{semi}");
183+
}
184+
hoisted
185+
} else {
186+
String::new()
187+
};
188+
124189
span_lint_and_sugg(
125190
cx,
126191
WHILE_LET_LOOP,
127192
expr.span,
128193
"this loop could be written as a `while let` loop",
129194
"try",
130195
format!(
131-
"while let {} = {} {{{inner_content}}}",
196+
"while let {} = {} {{{inner_content}}}{hoisted_content}",
132197
snippet(cx, let_pat.span, ".."),
133198
snippet(cx, let_expr.span, ".."),
134199
),
@@ -146,3 +211,38 @@ fn is_trivial_assignment(pat: &Pat<'_>, init: &Expr<'_>) -> bool {
146211
_ => false,
147212
}
148213
}
214+
215+
/// Checks if a block ends with an unlabeled `break` and returns the statements before it..
216+
/// or `None` if the block doesn't end with a simple break or if any statement before
217+
/// the break could exit the loop (via return, labeled break, etc.).
218+
fn extract_hoistable_stmts<'tcx>(block: &'tcx Block<'tcx>) -> Option<&'tcx [Stmt<'tcx>]> {
219+
let stmts_before_break = match (block.stmts, block.expr) {
220+
(stmts, Some(e)) if is_simple_break_expr(e) => stmts,
221+
(stmts, None) if !stmts.is_empty() => {
222+
let (last, rest) = stmts.split_last()?;
223+
match last.kind {
224+
StmtKind::Expr(e) | StmtKind::Semi(e) if is_simple_break_expr(e) => rest,
225+
_ => return None,
226+
}
227+
},
228+
_ => return None,
229+
};
230+
231+
if stmts_before_break.is_empty() {
232+
return None;
233+
}
234+
235+
// Check that none of the statements before break contain return, labeled break,
236+
// or other control flow that could exit the loop differently
237+
let has_early_exit = stmts_before_break.iter().any(|stmt| {
238+
for_each_expr_without_closures(stmt, |e| match e.kind {
239+
ExprKind::Ret(..)
240+
| ExprKind::Break(Destination { label: Some(_), .. }, _)
241+
| ExprKind::Continue(Destination { label: Some(_), .. }) => ControlFlow::Break(()),
242+
_ => ControlFlow::Continue(()),
243+
})
244+
.is_some()
245+
});
246+
247+
(!has_early_exit).then_some(stmts_before_break)
248+
}

tests/ui/manual_take_nocore.stderr

Whitespace-only changes.

tests/ui/while_let_loop.rs

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fn main() {
2828
}
2929

3030
loop {
31-
// no error, else branch does something other than break
31+
//~^ while_let_loop
3232
let Some(_x) = y else {
3333
let _z = 1;
3434
break;
@@ -255,22 +255,91 @@ fn let_assign() {
255255
}
256256

257257
fn issue16378() {
258-
// This does not lint today because of the extra statement(s)
259-
// before the `break`.
260-
// TODO: When the `break` statement/expr in the `let`/`else` is the
261-
// only way to leave the loop, the lint could trigger and move
262-
// the statements preceeding the `break` after the loop, as in:
263-
// ```rust
264-
// while let Some(x) = std::hint::black_box(None::<i32>) {
265-
// println!("x = {x}");
266-
// }
267-
// println!("fail");
268-
// ```
269258
loop {
259+
//~^ while_let_loop
270260
let Some(x) = std::hint::black_box(None::<i32>) else {
271261
println!("fail");
272262
break;
273263
};
274264
println!("x = {x}");
275265
}
276266
}
267+
268+
fn test_hoist_with_multiple_stmts() {
269+
let y = Some(true);
270+
loop {
271+
//~^ while_let_loop
272+
let Some(x) = y else {
273+
let a = 1;
274+
let b = 2;
275+
let _c = a + b;
276+
println!("sum: {}", a + b);
277+
eprintln!("failed");
278+
break;
279+
};
280+
println!("x = {x}");
281+
}
282+
}
283+
284+
fn test_hoist_with_semicolon_less_stmt() {
285+
let y = Some(true);
286+
loop {
287+
//~^ while_let_loop
288+
let Some(x) = y else {
289+
if std::hint::black_box(true) {
290+
println!("pass");
291+
}
292+
match 42 {
293+
0 => println!("zero"),
294+
_ => println!("non-zero"),
295+
}
296+
break;
297+
};
298+
println!("x = {x}");
299+
}
300+
}
301+
302+
fn no_hoist_with_return() -> Option<i32> {
303+
// Should NOT lint: the else block contains a return statement
304+
loop {
305+
let Some(x) = std::hint::black_box(None::<i32>) else {
306+
if true {
307+
return None;
308+
}
309+
break;
310+
};
311+
println!("x = {x}");
312+
}
313+
Some(42)
314+
}
315+
316+
fn no_hoist_with_labeled_break() {
317+
// Should NOT lint: the else block contains a labeled break to outer loop
318+
'outer: loop {
319+
loop {
320+
let Some(x) = std::hint::black_box(None::<i32>) else {
321+
if true {
322+
break 'outer;
323+
}
324+
break;
325+
};
326+
println!("x = {x}");
327+
}
328+
}
329+
}
330+
331+
fn no_hoist_with_labeled_continue() {
332+
// Should NOT lint: the else block contains a labeled continue to outer loop
333+
'outer: loop {
334+
loop {
335+
let Some(x) = std::hint::black_box(None::<i32>) else {
336+
if true {
337+
continue 'outer;
338+
}
339+
break;
340+
};
341+
println!("x = {x}");
342+
}
343+
break;
344+
}
345+
}

0 commit comments

Comments
 (0)