11use super :: WHILE_LET_LOOP ;
22use 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 } ;
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 , LetStmt , MatchSource , Pat , PatKind , Path , QPath , Stmt , StmtKind , Ty ,
11+ } ;
1012use rustc_lint:: LateContext ;
13+ use std:: fmt:: Write ;
14+ use std:: ops:: ControlFlow ;
1115
1216pub ( 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 wasn’ t 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+ }
0 commit comments