11use 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 } ;
44use 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 } ;
66use clippy_utils:: { higher, peel_blocks} ;
77use rustc_ast:: BindingMode ;
88use 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+ } ;
1013use rustc_lint:: LateContext ;
14+ use std:: fmt:: Write ;
15+ use std:: ops:: ControlFlow ;
1116
1217pub ( 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) ]
8499fn 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 wasn’ t 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