1+ use std:: ops:: ControlFlow ;
2+
13use clippy_utils:: diagnostics:: span_lint_and_sugg;
24use clippy_utils:: macros:: HirNode ;
3- use clippy_utils:: source:: { indent_of, snippet, snippet_block_with_context, snippet_with_context} ;
4- use clippy_utils:: { is_refutable, peel_blocks} ;
5+ use clippy_utils:: source:: { indent_of, reindent_multiline, snippet, snippet_block_with_context, snippet_with_context} ;
6+ use clippy_utils:: { is_expr_identity_of_pat, is_refutable, peel_blocks} ;
7+ use rustc_data_structures:: fx:: FxHashSet ;
58use rustc_errors:: Applicability ;
6- use rustc_hir:: { Arm , Expr , ExprKind , Node , PatKind , StmtKind } ;
9+ use rustc_hir:: def:: Res ;
10+ use rustc_hir:: intravisit:: { Visitor , walk_block, walk_expr, walk_path, walk_stmt} ;
11+ use rustc_hir:: { Arm , Block , Expr , ExprKind , HirId , Node , PatKind , Path , Stmt , StmtKind } ;
712use rustc_lint:: LateContext ;
8- use rustc_span:: Span ;
13+ use rustc_span:: { Span , Symbol } ;
914
1015use super :: MATCH_SINGLE_BINDING ;
1116
@@ -50,10 +55,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
5055 cx,
5156 ( ex, expr) ,
5257 ( bind_names, matched_vars) ,
53- & snippet_body,
58+ snippet_body,
5459 & mut app,
5560 Some ( span) ,
5661 true ,
62+ is_var_binding_used_later ( cx, expr, & arms[ 0 ] ) ,
5763 ) ;
5864
5965 span_lint_and_sugg (
@@ -78,15 +84,28 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
7884 snippet_with_context( cx, pat_span, ctxt, ".." , & mut app) . 0
7985 ) ,
8086 ) ,
87+ None if is_expr_identity_of_pat ( cx, arms[ 0 ] . pat , ex, false ) => {
88+ span_lint_and_sugg (
89+ cx,
90+ MATCH_SINGLE_BINDING ,
91+ expr. span ,
92+ "this match could be replaced by its body itself" ,
93+ "consider using the match body instead" ,
94+ snippet_body,
95+ Applicability :: MachineApplicable ,
96+ ) ;
97+ return ;
98+ } ,
8199 None => {
82100 let sugg = sugg_with_curlies (
83101 cx,
84102 ( ex, expr) ,
85103 ( bind_names, matched_vars) ,
86- & snippet_body,
104+ snippet_body,
87105 & mut app,
88106 None ,
89107 true ,
108+ is_var_binding_used_later ( cx, expr, & arms[ 0 ] ) ,
90109 ) ;
91110 ( expr. span , sugg)
92111 } ,
@@ -108,10 +127,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
108127 cx,
109128 ( ex, expr) ,
110129 ( bind_names, matched_vars) ,
111- & snippet_body,
130+ snippet_body,
112131 & mut app,
113132 None ,
114133 false ,
134+ true ,
115135 ) ;
116136
117137 span_lint_and_sugg (
@@ -139,6 +159,125 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
139159 }
140160}
141161
162+ struct VarBindingVisitor < ' a , ' tcx > {
163+ cx : & ' a LateContext < ' tcx > ,
164+ identifiers : FxHashSet < Symbol > ,
165+ }
166+
167+ impl < ' tcx > Visitor < ' tcx > for VarBindingVisitor < ' _ , ' tcx > {
168+ type Result = ControlFlow < ( ) > ;
169+
170+ fn visit_path ( & mut self , path : & Path < ' tcx > , _: HirId ) -> Self :: Result {
171+ if let Res :: Local ( _) = path. res
172+ && let [ segment] = path. segments
173+ && self . identifiers . contains ( & segment. ident . name )
174+ {
175+ return ControlFlow :: Break ( ( ) ) ;
176+ }
177+
178+ walk_path ( self , path)
179+ }
180+
181+ fn visit_block ( & mut self , block : & ' tcx Block < ' tcx > ) -> Self :: Result {
182+ let before = self . identifiers . clone ( ) ;
183+ walk_block ( self , block) ?;
184+ self . identifiers = before;
185+ ControlFlow :: Continue ( ( ) )
186+ }
187+
188+ fn visit_stmt ( & mut self , stmt : & ' tcx Stmt < ' tcx > ) -> Self :: Result {
189+ if let StmtKind :: Let ( let_stmt) = stmt. kind {
190+ if let Some ( init) = let_stmt. init {
191+ self . visit_expr ( init) ?;
192+ }
193+
194+ let_stmt. pat . each_binding ( |_, _, _, ident| {
195+ self . identifiers . remove ( & ident. name ) ;
196+ } ) ;
197+ }
198+ walk_stmt ( self , stmt)
199+ }
200+
201+ fn visit_expr ( & mut self , expr : & ' tcx Expr < ' tcx > ) -> Self :: Result {
202+ match expr. kind {
203+ ExprKind :: If (
204+ Expr {
205+ kind : ExprKind :: Let ( let_expr) ,
206+ ..
207+ } ,
208+ then,
209+ else_,
210+ ) => {
211+ self . visit_expr ( let_expr. init ) ?;
212+ let before = self . identifiers . clone ( ) ;
213+ let_expr. pat . each_binding ( |_, _, _, ident| {
214+ self . identifiers . remove ( & ident. name ) ;
215+ } ) ;
216+
217+ self . visit_expr ( then) ?;
218+ self . identifiers = before;
219+ if let Some ( else_) = else_ {
220+ self . visit_expr ( else_) ?;
221+ }
222+ ControlFlow :: Continue ( ( ) )
223+ } ,
224+ ExprKind :: Closure ( closure) => {
225+ let body = self . cx . tcx . hir_body ( closure. body ) ;
226+ let before = self . identifiers . clone ( ) ;
227+ for param in body. params {
228+ param. pat . each_binding ( |_, _, _, ident| {
229+ self . identifiers . remove ( & ident. name ) ;
230+ } ) ;
231+ }
232+ self . visit_expr ( body. value ) ?;
233+ self . identifiers = before;
234+ ControlFlow :: Continue ( ( ) )
235+ } ,
236+ ExprKind :: Match ( expr, arms, _) => {
237+ self . visit_expr ( expr) ?;
238+ for arm in arms {
239+ let before = self . identifiers . clone ( ) ;
240+ arm. pat . each_binding ( |_, _, _, ident| {
241+ self . identifiers . remove ( & ident. name ) ;
242+ } ) ;
243+ if let Some ( guard) = arm. guard {
244+ self . visit_expr ( guard) ?;
245+ }
246+ self . visit_expr ( arm. body ) ?;
247+ self . identifiers = before;
248+ }
249+ ControlFlow :: Continue ( ( ) )
250+ } ,
251+ _ => walk_expr ( self , expr) ,
252+ }
253+ }
254+ }
255+
256+ fn is_var_binding_used_later ( cx : & LateContext < ' _ > , expr : & Expr < ' _ > , arm : & Arm < ' _ > ) -> bool {
257+ let Node :: Stmt ( stmt) = cx. tcx . parent_hir_node ( expr. hir_id ) else {
258+ return false ;
259+ } ;
260+ let Node :: Block ( block) = cx. tcx . parent_hir_node ( stmt. hir_id ) else {
261+ return false ;
262+ } ;
263+
264+ let mut identifiers = FxHashSet :: default ( ) ;
265+ arm. pat . each_binding ( |_, _, _, ident| {
266+ identifiers. insert ( ident. name ) ;
267+ } ) ;
268+
269+ let mut visitor = VarBindingVisitor { cx, identifiers } ;
270+ block
271+ . stmts
272+ . iter ( )
273+ . skip_while ( |s| s. hir_id != stmt. hir_id )
274+ . skip ( 1 )
275+ . any ( |stmt| matches ! ( visitor. visit_stmt( stmt) , ControlFlow :: Break ( ( ) ) ) )
276+ || block
277+ . expr
278+ . is_some_and ( |expr| matches ! ( visitor. visit_expr( expr) , ControlFlow :: Break ( ( ) ) ) )
279+ }
280+
142281/// Returns true if the `ex` match expression is in a local (`let`) or assign expression
143282fn opt_parent_assign_span < ' a > ( cx : & LateContext < ' a > , ex : & Expr < ' a > ) -> Option < AssignmentExpr > {
144283 if let Node :: Expr ( parent_arm_expr) = cx. tcx . parent_hir_node ( ex. hir_id ) {
@@ -161,47 +300,66 @@ fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<Ass
161300 None
162301}
163302
164- fn expr_parent_requires_curlies < ' a > ( cx : & LateContext < ' a > , match_expr : & Expr < ' a > ) -> bool {
303+ fn expr_in_nested_block ( cx : & LateContext < ' _ > , match_expr : & Expr < ' _ > ) -> bool {
304+ if let Node :: Block ( block) = cx. tcx . parent_hir_node ( match_expr. hir_id ) {
305+ return block
306+ . expr
307+ . map_or_else ( || matches ! ( block. stmts, [ _] ) , |_| block. stmts . is_empty ( ) ) ;
308+ }
309+ false
310+ }
311+
312+ fn expr_must_have_curlies ( cx : & LateContext < ' _ > , match_expr : & Expr < ' _ > ) -> bool {
165313 let parent = cx. tcx . parent_hir_node ( match_expr. hir_id ) ;
166- matches ! (
167- parent,
168- Node :: Expr ( Expr {
169- kind: ExprKind :: Closure { .. } ,
170- ..
171- } ) | Node :: AnonConst ( ..)
314+ if let Node :: Expr ( Expr {
315+ kind : ExprKind :: Closure ( ..) | ExprKind :: Binary ( ..) ,
316+ ..
317+ } )
318+ | Node :: AnonConst ( ..) = parent
319+ {
320+ return true ;
321+ }
322+
323+ if let Node :: Arm ( arm) = & cx. tcx . parent_hir_node ( match_expr. hir_id )
324+ && let ExprKind :: Match ( ..) = arm. body . kind
325+ {
326+ return true ;
327+ }
328+
329+ false
330+ }
331+
332+ fn indent_of_nth_line ( snippet : & str , nth : usize ) -> Option < usize > {
333+ snippet
334+ . lines ( )
335+ . nth ( nth)
336+ . and_then ( |s| s. find ( |c : char | !c. is_whitespace ( ) ) )
337+ }
338+
339+ fn reindent_snippet_if_in_block ( snippet_body : & str , has_assignment : bool ) -> String {
340+ if has_assignment || !snippet_body. starts_with ( '{' ) {
341+ return reindent_multiline ( snippet_body, true , indent_of_nth_line ( snippet_body, 1 ) ) ;
342+ }
343+
344+ let snippet_body = snippet_body. trim_start_matches ( '{' ) . trim_end_matches ( '}' ) . trim ( ) ;
345+ reindent_multiline (
346+ snippet_body,
347+ false ,
348+ indent_of_nth_line ( snippet_body, 0 ) . map ( |indent| indent. saturating_sub ( 4 ) ) ,
172349 )
173350}
174351
352+ #[ expect( clippy:: too_many_arguments) ]
175353fn sugg_with_curlies < ' a > (
176354 cx : & LateContext < ' a > ,
177355 ( ex, match_expr) : ( & Expr < ' a > , & Expr < ' a > ) ,
178356 ( bind_names, matched_vars) : ( Span , Span ) ,
179- snippet_body : & str ,
357+ mut snippet_body : String ,
180358 applicability : & mut Applicability ,
181359 assignment : Option < Span > ,
182360 needs_var_binding : bool ,
361+ is_var_binding_used_later : bool ,
183362) -> String {
184- let mut indent = " " . repeat ( indent_of ( cx, ex. span ) . unwrap_or ( 0 ) ) ;
185-
186- let ( mut cbrace_start, mut cbrace_end) = ( String :: new ( ) , String :: new ( ) ) ;
187- if expr_parent_requires_curlies ( cx, match_expr) {
188- cbrace_end = format ! ( "\n {indent}}}" ) ;
189- // Fix body indent due to the closure
190- indent = " " . repeat ( indent_of ( cx, bind_names) . unwrap_or ( 0 ) ) ;
191- cbrace_start = format ! ( "{{\n {indent}" ) ;
192- }
193-
194- // If the parent is already an arm, and the body is another match statement,
195- // we need curly braces around suggestion
196- if let Node :: Arm ( arm) = & cx. tcx . parent_hir_node ( match_expr. hir_id )
197- && let ExprKind :: Match ( ..) = arm. body . kind
198- {
199- cbrace_end = format ! ( "\n {indent}}}" ) ;
200- // Fix body indent due to the match
201- indent = " " . repeat ( indent_of ( cx, bind_names) . unwrap_or ( 0 ) ) ;
202- cbrace_start = format ! ( "{{\n {indent}" ) ;
203- }
204-
205363 let assignment_str = assignment. map_or_else ( String :: new, |span| {
206364 let mut s = snippet ( cx, span, ".." ) . to_string ( ) ;
207365 s. push_str ( " = " ) ;
@@ -221,5 +379,17 @@ fn sugg_with_curlies<'a>(
221379 . to_string ( )
222380 } ;
223381
382+ let mut indent = " " . repeat ( indent_of ( cx, ex. span ) . unwrap_or ( 0 ) ) ;
383+ let ( mut cbrace_start, mut cbrace_end) = ( String :: new ( ) , String :: new ( ) ) ;
384+ if !expr_in_nested_block ( cx, match_expr)
385+ && ( ( needs_var_binding && is_var_binding_used_later) || expr_must_have_curlies ( cx, match_expr) )
386+ {
387+ cbrace_end = format ! ( "\n {indent}}}" ) ;
388+ // Fix body indent due to the closure
389+ indent = " " . repeat ( indent_of ( cx, bind_names) . unwrap_or ( 0 ) ) ;
390+ cbrace_start = format ! ( "{{\n {indent}" ) ;
391+ snippet_body = reindent_snippet_if_in_block ( & snippet_body, !assignment_str. is_empty ( ) ) ;
392+ }
393+
224394 format ! ( "{cbrace_start}{scrutinee};\n {indent}{assignment_str}{snippet_body}{cbrace_end}" )
225395}
0 commit comments