Skip to content

Commit f4f579f

Browse files
authored
Fix match_single_binding wrongly handles scope (#15060)
Closes #15018 Closes #15269 Continuation of #15017 changelog: [`match_single_binding`] fix wrong handling of scope
2 parents 4bfb6d3 + 48df86f commit f4f579f

File tree

5 files changed

+505
-71
lines changed

5 files changed

+505
-71
lines changed

clippy_lints/src/matches/match_single_binding.rs

Lines changed: 206 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
use std::ops::ControlFlow;
2+
13
use clippy_utils::diagnostics::span_lint_and_sugg;
24
use 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;
58
use 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};
712
use rustc_lint::LateContext;
8-
use rustc_span::Span;
13+
use rustc_span::{Span, Symbol};
914

1015
use 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
143282
fn 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)]
175353
fn 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

Comments
 (0)