Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions clippy_lints/src/loops/while_let_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use clippy_utils::source::{snippet, snippet_indent, snippet_opt};
use clippy_utils::ty::needs_ordered_drop;
use clippy_utils::visitors::any_temporaries_need_ordered_drop;
use clippy_utils::{higher, peel_blocks};
use rustc_ast::BindingMode;
use rustc_ast::{BindingMode, Label};
use rustc_errors::Applicability;
use rustc_hir::{Block, Expr, ExprKind, LetStmt, MatchSource, Pat, PatKind, Path, QPath, StmtKind, Ty};
use rustc_lint::LateContext;
Expand All @@ -26,10 +26,14 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
_ => return,
};
let has_trailing_exprs = loop_block.stmts.len() + usize::from(loop_block.expr.is_some()) > 1;

let loop_label = if let ExprKind::Loop(_, label, ..) = expr.kind {
label
} else {
None
};
if let Some(if_let) = higher::IfLet::hir(cx, init)
&& let Some(else_expr) = if_let.if_else
&& is_simple_break_expr(else_expr)
&& is_simple_break_expr(else_expr, loop_label)
{
could_be_while_let(
cx,
Expand All @@ -39,15 +43,16 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
has_trailing_exprs,
let_info,
Some(if_let.if_then),
loop_label,
);
} else if els.is_some_and(is_simple_break_block)
} else if els.is_some_and(|b| is_simple_break_block(b, loop_label))
&& let Some((pat, _)) = let_info
{
could_be_while_let(cx, expr, pat, init, has_trailing_exprs, let_info, None);
could_be_while_let(cx, expr, pat, init, has_trailing_exprs, let_info, None, loop_label);
} else if let ExprKind::Match(scrutinee, [arm1, arm2], MatchSource::Normal) = init.kind
&& arm1.guard.is_none()
&& arm2.guard.is_none()
&& is_simple_break_expr(arm2.body)
&& is_simple_break_expr(arm2.body, loop_label)
{
could_be_while_let(
cx,
Expand All @@ -57,30 +62,32 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, loop_blo
has_trailing_exprs,
let_info,
Some(arm1.body),
loop_label,
);
}
}

/// Checks if `block` contains a single unlabeled `break` expression or statement, possibly embedded
/// inside other blocks.
fn is_simple_break_block(block: &Block<'_>) -> bool {
/// Checks if `block` contains a single (labeled or unlabeled) `break`
/// expression or statement, possibly embedded inside other blocks.
fn is_simple_break_block(block: &Block<'_>, looplabel: Option<Label>) -> bool {
match (block.stmts, block.expr) {
([s], None) => matches!(s.kind, StmtKind::Expr(e) | StmtKind::Semi(e) if is_simple_break_expr(e)),
([], Some(e)) => is_simple_break_expr(e),
([s], None) => matches!(s.kind, StmtKind::Expr(e) | StmtKind::Semi(e) if is_simple_break_expr(e, looplabel)),
([], Some(e)) => is_simple_break_expr(e, looplabel),
_ => false,
}
}

/// Checks if `expr` contains a single unlabeled `break` expression or statement, possibly embedded
/// inside other blocks.
fn is_simple_break_expr(expr: &Expr<'_>) -> bool {
/// Checks if `expr` contains a single (labeled or unlabeled) `break`
/// expression or statement, possibly embedded inside other blocks.
fn is_simple_break_expr(expr: &Expr<'_>, looplabel: Option<Label>) -> bool {
match expr.kind {
ExprKind::Block(b, _) => is_simple_break_block(b),
ExprKind::Break(dest, None) => dest.label.is_none(),
ExprKind::Block(b, _) => is_simple_break_block(b, looplabel),
ExprKind::Break(dest, None) => dest.label.is_none() || dest.label == looplabel,
_ => false,
}
}

#[allow(clippy::too_many_arguments)]
fn could_be_while_let<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'_>,
Expand All @@ -89,6 +96,7 @@ fn could_be_while_let<'tcx>(
has_trailing_exprs: bool,
let_info: Option<(&Pat<'_>, Option<&Ty<'_>>)>,
inner_expr: Option<&Expr<'_>>,
label: Option<Label>,
) {
if has_trailing_exprs
&& (needs_ordered_drop(cx, cx.typeck_results().expr_ty(let_expr))
Expand Down Expand Up @@ -120,15 +128,17 @@ fn could_be_while_let<'tcx>(
} else {
" .. ".into()
};

let looplabelstr = label.map(|label| format!("{}: ", label.ident.name)).unwrap_or_default();
// use the label not silenty dropping it in the suggestion when u have a labeled loop
span_lint_and_sugg(
cx,
WHILE_LET_LOOP,
expr.span,
"this loop could be written as a `while let` loop",
"try",
format!(
"while let {} = {} {{{inner_content}}}",
"{}while let {} = {} {{{inner_content}}}",
looplabelstr,
snippet(cx, let_pat.span, ".."),
snippet(cx, let_expr.span, ".."),
),
Expand Down
32 changes: 32 additions & 0 deletions tests/ui/while_let_loop.rs

@blyxyas blyxyas Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add tests with blocks inside those loops? And with those blocks labeled? What about if they share the same label? And only breaking from the inner/outer block?

Try to find the weirdest examples that you can think of. Lints don't usually break from run-of-the-mill common expressions, but from weird, sometimes macro-generated code. (=^-ω-^=)

View changes since the review

Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,35 @@ fn issue16378() {
println!("x = {x}");
}
}

fn issue17590_labeled_loop() {
let mut it = [1, 2, 3].iter();
'cool: loop {
//~^ while_let_loop
match it.next() {
Some(_) => {},
None => break 'cool,
}
}
}

fn issue17590_labeled_if_let() {
let mut it = [1, 2, 3].iter();
'outer: loop {
//~^ while_let_loop
if let Some(x) = it.next() {
println!("{x}");
} else {
break 'outer;
}
}
}

fn issue17590_labeled_let_else() {
let mut it = [1, 2, 3].iter();
'outer: loop {
//~^ while_let_loop
let Some(x) = it.next() else { break 'outer };
println!("{x}");
}
}
34 changes: 33 additions & 1 deletion tests/ui/while_let_loop.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,37 @@ LL + ..
LL + }
|

error: aborting due to 12 previous errors
error: this loop could be written as a `while let` loop
--> tests/ui/while_let_loop.rs:280:5
|
LL | / 'cool: loop {
LL | |
LL | | match it.next() {
LL | | Some(_) => {},
... |
LL | | }
| |_____^ help: try: `'cool: while let Some(_) = it.next() { .. }`

error: this loop could be written as a `while let` loop
--> tests/ui/while_let_loop.rs:291:5
|
LL | / 'outer: loop {
LL | |
LL | | if let Some(x) = it.next() {
LL | | println!("{x}");
... |
LL | | }
| |_____^ help: try: `'outer: while let Some(x) = it.next() { .. }`

error: this loop could be written as a `while let` loop
--> tests/ui/while_let_loop.rs:303:5
|
LL | / 'outer: loop {
LL | |
LL | | let Some(x) = it.next() else { break 'outer };
LL | | println!("{x}");
LL | | }
| |_____^ help: try: `'outer: while let Some(x) = it.next() { .. }`

error: aborting due to 15 previous errors