Skip to content

Commit fce7326

Browse files
committed
fix: match_single_binding wrongly handles scope
1 parent 60a978d commit fce7326

File tree

5 files changed

+428
-37
lines changed

5 files changed

+428
-37
lines changed

clippy_lints/src/matches/match_single_binding.rs

Lines changed: 186 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
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};
5+
use clippy_utils::source::{
6+
RelativeIndent, indent_of, reindent_multiline_relative, snippet, snippet_block_with_context, snippet_with_context,
7+
};
48
use clippy_utils::{is_refutable, peel_blocks};
9+
use rustc_data_structures::fx::FxHashSet;
510
use rustc_errors::Applicability;
6-
use rustc_hir::{Arm, Expr, ExprKind, Node, PatKind, StmtKind};
11+
use rustc_hir::def::Res;
12+
use rustc_hir::intravisit::{Visitor, walk_block, walk_expr, walk_path, walk_stmt};
13+
use rustc_hir::{Arm, Block, Expr, ExprKind, HirId, Node, PatKind, Path, Stmt, StmtKind};
714
use rustc_lint::LateContext;
8-
use rustc_span::Span;
15+
use rustc_span::{Span, Symbol};
916

1017
use super::MATCH_SINGLE_BINDING;
1118

@@ -50,10 +57,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
5057
cx,
5158
(ex, expr),
5259
(bind_names, matched_vars),
53-
&snippet_body,
60+
snippet_body,
5461
&mut app,
5562
Some(span),
5663
true,
64+
is_var_binding_used_later(cx, expr, &arms[0]),
5765
);
5866

5967
span_lint_and_sugg(
@@ -83,10 +91,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
8391
cx,
8492
(ex, expr),
8593
(bind_names, matched_vars),
86-
&snippet_body,
94+
snippet_body,
8795
&mut app,
8896
None,
8997
true,
98+
is_var_binding_used_later(cx, expr, &arms[0]),
9099
);
91100
(expr.span, sugg)
92101
},
@@ -108,10 +117,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
108117
cx,
109118
(ex, expr),
110119
(bind_names, matched_vars),
111-
&snippet_body,
120+
snippet_body,
112121
&mut app,
113122
None,
114123
false,
124+
true,
115125
);
116126

117127
span_lint_and_sugg(
@@ -139,6 +149,125 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e
139149
}
140150
}
141151

152+
struct VarBindingVisitor<'a, 'tcx> {
153+
cx: &'a LateContext<'tcx>,
154+
identifiers: FxHashSet<Symbol>,
155+
}
156+
157+
impl<'tcx> Visitor<'tcx> for VarBindingVisitor<'_, 'tcx> {
158+
type Result = ControlFlow<()>;
159+
160+
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) -> Self::Result {
161+
if let Res::Local(_) = path.res
162+
&& let [segment] = path.segments
163+
&& self.identifiers.contains(&segment.ident.name)
164+
{
165+
return ControlFlow::Break(());
166+
}
167+
168+
walk_path(self, path)
169+
}
170+
171+
fn visit_block(&mut self, block: &'tcx Block<'tcx>) -> Self::Result {
172+
let before = self.identifiers.clone();
173+
walk_block(self, block)?;
174+
self.identifiers = before;
175+
ControlFlow::Continue(())
176+
}
177+
178+
fn visit_stmt(&mut self, stmt: &'tcx Stmt<'tcx>) -> Self::Result {
179+
if let StmtKind::Let(let_stmt) = stmt.kind {
180+
if let Some(init) = let_stmt.init {
181+
self.visit_expr(init)?;
182+
}
183+
184+
let_stmt.pat.each_binding(|_, _, _, ident| {
185+
self.identifiers.remove(&ident.name);
186+
});
187+
}
188+
walk_stmt(self, stmt)
189+
}
190+
191+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result {
192+
match expr.kind {
193+
ExprKind::If(
194+
Expr {
195+
kind: ExprKind::Let(let_expr),
196+
..
197+
},
198+
then,
199+
else_,
200+
) => {
201+
self.visit_expr(let_expr.init)?;
202+
let before = self.identifiers.clone();
203+
let_expr.pat.each_binding(|_, _, _, ident| {
204+
self.identifiers.remove(&ident.name);
205+
});
206+
207+
self.visit_expr(then)?;
208+
self.identifiers = before;
209+
if let Some(else_) = else_ {
210+
self.visit_expr(else_)?;
211+
}
212+
ControlFlow::Continue(())
213+
},
214+
ExprKind::Closure(closure) => {
215+
let body = self.cx.tcx.hir_body(closure.body);
216+
let before = self.identifiers.clone();
217+
for param in body.params {
218+
param.pat.each_binding(|_, _, _, ident| {
219+
self.identifiers.remove(&ident.name);
220+
});
221+
}
222+
self.visit_expr(&body.value)?;
223+
self.identifiers = before;
224+
ControlFlow::Continue(())
225+
},
226+
ExprKind::Match(expr, arms, _) => {
227+
self.visit_expr(expr)?;
228+
for arm in arms {
229+
let before = self.identifiers.clone();
230+
arm.pat.each_binding(|_, _, _, ident| {
231+
self.identifiers.remove(&ident.name);
232+
});
233+
if let Some(guard) = arm.guard {
234+
self.visit_expr(guard)?;
235+
}
236+
self.visit_expr(arm.body)?;
237+
self.identifiers = before;
238+
}
239+
ControlFlow::Continue(())
240+
},
241+
_ => walk_expr(self, expr),
242+
}
243+
}
244+
}
245+
246+
fn is_var_binding_used_later(cx: &LateContext<'_>, expr: &Expr<'_>, arm: &Arm<'_>) -> bool {
247+
let Node::Stmt(stmt) = cx.tcx.parent_hir_node(expr.hir_id) else {
248+
return false;
249+
};
250+
let Node::Block(block) = cx.tcx.parent_hir_node(stmt.hir_id) else {
251+
return false;
252+
};
253+
254+
let mut identifiers = FxHashSet::default();
255+
arm.pat.each_binding(|_, _, _, ident| {
256+
identifiers.insert(ident.name);
257+
});
258+
259+
let mut visitor = VarBindingVisitor { cx, identifiers };
260+
block
261+
.stmts
262+
.iter()
263+
.skip_while(|s| s.hir_id != stmt.hir_id)
264+
.skip(1)
265+
.any(|stmt| matches!(visitor.visit_stmt(stmt), ControlFlow::Break(())))
266+
|| block
267+
.expr
268+
.is_some_and(|expr| matches!(visitor.visit_expr(expr), ControlFlow::Break(())))
269+
}
270+
142271
/// Returns true if the `ex` match expression is in a local (`let`) or assign expression
143272
fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<AssignmentExpr> {
144273
if let Node::Expr(parent_arm_expr) = cx.tcx.parent_hir_node(ex.hir_id) {
@@ -161,47 +290,57 @@ fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<Ass
161290
None
162291
}
163292

164-
fn expr_parent_requires_curlies<'a>(cx: &LateContext<'a>, match_expr: &Expr<'a>) -> bool {
293+
fn expr_in_nested_block(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool {
294+
if let Node::Block(block) = cx.tcx.parent_hir_node(match_expr.hir_id) {
295+
return block
296+
.expr
297+
.map_or_else(|| matches!(block.stmts, [_]), |_| block.stmts.is_empty());
298+
}
299+
false
300+
}
301+
302+
fn expr_must_have_curlies(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool {
165303
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(..)
304+
if let Node::Expr(Expr {
305+
kind: ExprKind::Closure { .. },
306+
..
307+
})
308+
| Node::AnonConst(..) = parent
309+
{
310+
return true;
311+
}
312+
313+
if let Node::Arm(arm) = &cx.tcx.parent_hir_node(match_expr.hir_id)
314+
&& let ExprKind::Match(..) = arm.body.kind
315+
{
316+
return true;
317+
}
318+
319+
false
320+
}
321+
322+
fn reindent_snippet_if_in_block(snippet_body: &str, has_assignment: bool) -> String {
323+
if has_assignment || !snippet_body.starts_with('{') {
324+
return reindent_multiline_relative(snippet_body, true, RelativeIndent::Add(0));
325+
}
326+
327+
reindent_multiline_relative(
328+
snippet_body.trim_start_matches('{').trim_end_matches('}').trim(),
329+
false,
330+
RelativeIndent::Sub(4),
172331
)
173332
}
174333

175334
fn sugg_with_curlies<'a>(
176335
cx: &LateContext<'a>,
177336
(ex, match_expr): (&Expr<'a>, &Expr<'a>),
178337
(bind_names, matched_vars): (Span, Span),
179-
snippet_body: &str,
338+
mut snippet_body: String,
180339
applicability: &mut Applicability,
181340
assignment: Option<Span>,
182341
needs_var_binding: bool,
342+
is_var_binding_used_later: bool,
183343
) -> 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-
205344
let assignment_str = assignment.map_or_else(String::new, |span| {
206345
let mut s = snippet(cx, span, "..").to_string();
207346
s.push_str(" = ");
@@ -221,5 +360,17 @@ fn sugg_with_curlies<'a>(
221360
.to_string()
222361
};
223362

363+
let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0));
364+
let (mut cbrace_start, mut cbrace_end) = (String::new(), String::new());
365+
if !expr_in_nested_block(cx, match_expr)
366+
&& ((needs_var_binding && is_var_binding_used_later) || expr_must_have_curlies(cx, match_expr))
367+
{
368+
cbrace_end = format!("\n{indent}}}");
369+
// Fix body indent due to the closure
370+
indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0));
371+
cbrace_start = format!("{{\n{indent}");
372+
snippet_body = reindent_snippet_if_in_block(&snippet_body, !assignment_str.is_empty());
373+
}
374+
224375
format!("{cbrace_start}{scrutinee};\n{indent}{assignment_str}{snippet_body}{cbrace_end}")
225376
}

clippy_utils/src/source.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use rustc_span::{
1818
};
1919
use std::borrow::Cow;
2020
use std::fmt;
21-
use std::ops::{Deref, Index, Range};
21+
use std::ops::{Add, Deref, Index, Range};
2222

2323
pub trait HasSession {
2424
fn sess(&self) -> &Session;
@@ -476,6 +476,38 @@ pub fn position_before_rarrow(s: &str) -> Option<usize> {
476476
})
477477
}
478478

479+
pub enum RelativeIndent {
480+
Add(usize),
481+
Sub(usize),
482+
}
483+
484+
impl Add<RelativeIndent> for usize {
485+
type Output = usize;
486+
487+
fn add(self, rhs: RelativeIndent) -> Self::Output {
488+
match rhs {
489+
RelativeIndent::Add(n) => self + n,
490+
RelativeIndent::Sub(n) => self.saturating_sub(n),
491+
}
492+
}
493+
}
494+
495+
/// Reindents a multiline string with possibility of ignoring the first line and relative
496+
/// indentation.
497+
pub fn reindent_multiline_relative(s: &str, ignore_first: bool, relative_indent: RelativeIndent) -> String {
498+
fn indent_of_string(s: &str) -> usize {
499+
s.find(|c: char| !c.is_whitespace()).unwrap_or(0)
500+
}
501+
502+
let mut indent = 0;
503+
if let Some(line) = s.lines().nth(usize::from(ignore_first)) {
504+
let line_indent = indent_of_string(line);
505+
indent = line_indent + relative_indent;
506+
}
507+
508+
reindent_multiline(s, ignore_first, Some(indent))
509+
}
510+
479511
/// Reindent a multiline string with possibility of ignoring the first line.
480512
pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String {
481513
let s_space = reindent_multiline_inner(s, ignore_first, indent, ' ');

tests/ui/match_single_binding.fixed

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,56 @@ mod issue14991 {
204204
}],
205205
}
206206
}
207+
208+
mod issue15018 {
209+
fn used_later(a: i32, b: i32, c: i32) {
210+
let x = 1;
211+
{
212+
let (x, y, z) = (a, b, c);
213+
println!("{} {} {}", x, y, z);
214+
}
215+
println!("x = {x}");
216+
}
217+
218+
fn not_used_later(a: i32, b: i32, c: i32) {
219+
let (x, y, z) = (a, b, c);
220+
println!("{} {} {}", x, y, z)
221+
}
222+
223+
#[allow(irrefutable_let_patterns)]
224+
fn not_used_later_but_shadowed(a: i32, b: i32, c: i32) {
225+
let (x, y, z) = (a, b, c);
226+
println!("{} {} {}", x, y, z);
227+
let x = 1;
228+
println!("x = {x}");
229+
}
230+
231+
#[allow(irrefutable_let_patterns)]
232+
fn not_used_later_but_shadowed_nested(a: i32, b: i32, c: i32) {
233+
let (x, y, z) = (a, b, c);
234+
println!("{} {} {}", x, y, z);
235+
if let (x, y, z) = (a, b, c) {
236+
println!("{} {} {}", x, y, z)
237+
}
238+
239+
{
240+
let x: i32 = 1;
241+
{
242+
let (x, y, z) = (a, b, c);
243+
println!("{} {} {}", x, y, z);
244+
}
245+
if let (x, y, z) = (a, x, c) {
246+
println!("{} {} {}", x, y, z)
247+
}
248+
}
249+
250+
{
251+
let (x, y, z) = (a, b, c);
252+
println!("{} {} {}", x, y, z);
253+
let fn_ = |y| {
254+
println!("{} {} {}", a, b, y);
255+
};
256+
fn_(c);
257+
}
258+
}
259+
}

0 commit comments

Comments
 (0)