Skip to content

Commit aef8e9a

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 refactor: remove WhileLetInfo struct, flatten match chain use HirId for break/continue targeting, add label support and autofix cargo dev fmt
1 parent da29c3e commit aef8e9a

3 files changed

Lines changed: 493 additions & 64 deletions

File tree

Lines changed: 139 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
use super::WHILE_LET_LOOP;
2-
use clippy_utils::diagnostics::span_lint_and_sugg;
3-
use clippy_utils::source::{snippet, snippet_indent, snippet_opt};
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::source::{reindent_multiline, 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, HirId, LetStmt, LoopSource, MatchSource, Pat, PatKind, Path, QPath, Stmt,
11+
StmtKind, Ty,
12+
};
1013
use rustc_lint::LateContext;
14+
use std::fmt::Write;
15+
use std::ops::ControlFlow;
1116

1217
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_block: &'tcx Block<'_>) {
1318
let (init, let_info, els) = match (loop_block.stmts, loop_block.expr) {
@@ -27,38 +32,47 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
2732
};
2833
let has_trailing_exprs = loop_block.stmts.len() + usize::from(loop_block.expr.is_some()) > 1;
2934

30-
if let Some(if_let) = higher::IfLet::hir(cx, init)
35+
let (let_pat, let_expr, inner_expr, hoistable_stmts) = if let Some(if_let) = higher::IfLet::hir(cx, init)
3136
&& let Some(else_expr) = if_let.if_else
3237
&& is_simple_break_expr(else_expr)
3338
{
34-
could_be_while_let(
35-
cx,
36-
expr,
37-
if_let.let_pat,
38-
if_let.let_expr,
39-
has_trailing_exprs,
40-
let_info,
41-
Some(if_let.if_then),
42-
);
39+
(if_let.let_pat, if_let.let_expr, Some(if_let.if_then), None)
4340
} else if els.is_some_and(is_simple_break_block)
4441
&& let Some((pat, _)) = let_info
4542
{
46-
could_be_while_let(cx, expr, pat, init, has_trailing_exprs, let_info, None);
43+
(pat, init, None, None)
44+
} else if let Some(els_block) = els
45+
&& let Some((pat, _)) = let_info
46+
&& let Some(hoistable) = extract_hoistable_stmts(els_block, expr.hir_id)
47+
{
48+
(pat, init, None, Some(hoistable))
4749
} else if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal) = init.kind
4850
&& arm1.guard.is_none()
4951
&& arm2.guard.is_none()
5052
&& is_simple_break_expr(arm2.body)
5153
{
52-
could_be_while_let(
53-
cx,
54-
expr,
55-
arm1.pat,
56-
scrutinee,
57-
has_trailing_exprs,
58-
let_info,
59-
Some(arm1.body),
60-
);
54+
(arm1.pat, scrutinee, Some(arm1.body), None)
55+
} else {
56+
return;
57+
};
58+
59+
if (has_trailing_exprs || hoistable_stmts.is_some())
60+
&& (needs_ordered_drop(cx, cx.typeck_results().expr_ty(let_expr))
61+
|| any_temporaries_need_ordered_drop(cx, let_expr))
62+
{
63+
return;
6164
}
65+
66+
could_be_while_let(
67+
cx,
68+
expr,
69+
loop_block,
70+
let_info,
71+
let_pat,
72+
let_expr,
73+
inner_expr,
74+
hoistable_stmts,
75+
);
6276
}
6377

6478
/// Checks if `block` contains a single unlabeled `break` expression or statement, possibly embedded
@@ -81,28 +95,30 @@ fn is_simple_break_expr(expr: &Expr<'_>) -> bool {
8195
}
8296
}
8397

98+
#[expect(clippy::too_many_arguments)]
8499
fn could_be_while_let<'tcx>(
85100
cx: &LateContext<'tcx>,
86101
expr: &'tcx Expr<'_>,
87-
let_pat: &'tcx Pat<'_>,
88-
let_expr: &'tcx Expr<'_>,
89-
has_trailing_exprs: bool,
90-
let_info: Option<(&Pat<'_>, Option<&Ty<'_>>)>,
91-
inner_expr: Option<&Expr<'_>>,
102+
loop_block: &'tcx Block<'_>,
103+
let_info: Option<(&'tcx Pat<'tcx>, Option<&'tcx Ty<'tcx>>)>,
104+
let_pat: &'tcx Pat<'tcx>,
105+
let_expr: &'tcx Expr<'tcx>,
106+
inner_expr: Option<&'tcx Expr<'tcx>>,
107+
hoistable_stmts: Option<&'tcx [Stmt<'tcx>]>,
92108
) {
93-
if has_trailing_exprs
94-
&& (needs_ordered_drop(cx, cx.typeck_results().expr_ty(let_expr))
95-
|| any_temporaries_need_ordered_drop(cx, let_expr))
96-
{
97-
// Switching to a `while let` loop will extend the lifetime of some values.
98-
return;
99-
}
109+
let indent = snippet_indent(cx, expr.span).unwrap_or_default();
110+
111+
let label_prefix = if let ExprKind::Loop(_, Some(label), LoopSource::Loop, _) = expr.kind {
112+
format!("{}: ", label.ident)
113+
} else {
114+
String::new()
115+
};
100116

101117
// NOTE: we used to build a body here instead of using
102118
// ellipsis, this was removed because:
103119
// 1) it was ugly with big bodies;
104120
// 2) it was not indented properly;
105-
// 3) it wasnt very smart (see #675).
121+
// 3) it wasn't very smart (see #675).
106122
let inner_content = if let Some(((pat, ty), inner_expr)) = let_info.zip(inner_expr)
107123
// Prevent trivial reassignments such as `let x = x;` or `let _ = …;`, but
108124
// keep them if the type has been explicitly specified.
@@ -113,26 +129,62 @@ fn could_be_while_let<'tcx>(
113129
let ty_str = ty
114130
.map(|ty| format!(": {}", snippet(cx, ty.span, "_")))
115131
.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-
)
132+
format!("\n{indent} let {pat_str}{ty_str} = {init_str};\n{indent} ..\n{indent}")
120133
} else {
121134
" .. ".into()
122135
};
123136

124-
span_lint_and_sugg(
137+
let hoisted_content = if let Some(stmts) = hoistable_stmts {
138+
let mut hoisted = String::new();
139+
let outer_ctxt = expr.span.ctxt();
140+
for stmt in stmts {
141+
let (stmt_str, _) = snippet_with_context(cx, stmt.span, outer_ctxt, "..", &mut Applicability::Unspecified);
142+
let semi = if matches!(stmt.kind, StmtKind::Semi(_)) {
143+
";"
144+
} else {
145+
""
146+
};
147+
let reindented = reindent_multiline(&stmt_str, true, Some(indent.len()));
148+
let _ = write!(hoisted, "\n{indent}{reindented}{semi}");
149+
}
150+
hoisted
151+
} else {
152+
String::new()
153+
};
154+
155+
let human_suggestion = format!(
156+
"{label_prefix}while let {} = {} {{{inner_content}}}{hoisted_content}",
157+
snippet(cx, let_pat.span, ".."),
158+
snippet(cx, let_expr.span, ".."),
159+
);
160+
161+
span_lint_and_then(
125162
cx,
126163
WHILE_LET_LOOP,
127164
expr.span,
128165
"this loop could be written as a `while let` loop",
129-
"try",
130-
format!(
131-
"while let {} = {} {{{inner_content}}}",
132-
snippet(cx, let_pat.span, ".."),
133-
snippet(cx, let_expr.span, ".."),
134-
),
135-
Applicability::HasPlaceholders,
166+
|diag| {
167+
diag.span_suggestion(expr.span, "try", &human_suggestion, Applicability::HasPlaceholders);
168+
169+
if inner_expr.is_none() {
170+
let while_let_header = format!(
171+
"{label_prefix}while let {} = {} {{",
172+
snippet(cx, let_pat.span, ".."),
173+
snippet(cx, let_expr.span, ".."),
174+
);
175+
176+
let first_stmt_span = loop_block.stmts[0].span;
177+
let replace_span = expr.span.with_hi(first_stmt_span.hi());
178+
179+
let mut parts = vec![(replace_span, while_let_header)];
180+
181+
if !hoisted_content.is_empty() {
182+
parts.push((expr.span.shrink_to_hi(), hoisted_content.clone()));
183+
}
184+
185+
diag.tool_only_multipart_suggestion("try", parts, Applicability::MachineApplicable);
186+
}
187+
},
136188
);
137189
}
138190

@@ -146,3 +198,41 @@ fn is_trivial_assignment(pat: &Pat<'_>, init: &Expr<'_>) -> bool {
146198
_ => false,
147199
}
148200
}
201+
202+
/// Checks if a block ends with an unlabeled `break` and returns the statements before it,
203+
/// or `None` if any statement before the break contains a `break` or `continue` targeting
204+
/// the loop identified by `loop_id`.
205+
fn extract_hoistable_stmts<'tcx>(block: &'tcx Block<'tcx>, loop_id: HirId) -> Option<&'tcx [Stmt<'tcx>]> {
206+
let stmts_before_break = match (block.stmts, block.expr) {
207+
(stmts, Some(e)) if is_simple_break_expr(e) => stmts,
208+
(stmts, None) if !stmts.is_empty() => {
209+
let (last, rest) = stmts.split_last()?;
210+
match last.kind {
211+
StmtKind::Expr(e) | StmtKind::Semi(e) if is_simple_break_expr(e) => rest,
212+
_ => return None,
213+
}
214+
},
215+
_ => return None,
216+
};
217+
218+
if stmts_before_break.is_empty() {
219+
return None;
220+
}
221+
222+
// Reject statements containing a `break`/`continue` targeting the loop
223+
// being transformed. Breaks/continues to other loops and returns are fine to hoist.
224+
let has_problematic_control_flow = stmts_before_break.iter().any(|stmt| {
225+
for_each_expr_without_closures(stmt, |e| match e.kind {
226+
ExprKind::Break(Destination { target_id: Ok(id), .. }, _)
227+
| ExprKind::Continue(Destination { target_id: Ok(id), .. })
228+
if id == loop_id =>
229+
{
230+
ControlFlow::Break(())
231+
},
232+
_ => ControlFlow::Continue(()),
233+
})
234+
.is_some()
235+
});
236+
237+
(!has_problematic_control_flow).then_some(stmts_before_break)
238+
}

0 commit comments

Comments
 (0)