diff options
| author | yanglsh <yanglsh@shanghaitech.edu.cn> | 2025-06-15 21:28:14 +0800 |
|---|---|---|
| committer | yanglsh <yanglsh@shanghaitech.edu.cn> | 2025-07-15 21:48:27 +0800 |
| commit | ba6485d61c5dd6c106c5f93b6d581a88f630cbc2 (patch) | |
| tree | b85ce06c6de2ebe77bfb4b27ab9f56f203523a42 | |
| parent | e1be06240d2be922d7cd20226ebb9b6d0051d311 (diff) | |
fix: `match_single_binding` wrongly handles scope
| -rw-r--r-- | clippy_lints/src/matches/match_single_binding.rs | 222 | ||||
| -rw-r--r-- | clippy_utils/src/source.rs | 34 | ||||
| -rw-r--r-- | tests/ui/match_single_binding.fixed | 53 | ||||
| -rw-r--r-- | tests/ui/match_single_binding.rs | 61 | ||||
| -rw-r--r-- | tests/ui/match_single_binding.stderr | 96 |
5 files changed, 429 insertions, 37 deletions
diff --git a/clippy_lints/src/matches/match_single_binding.rs b/clippy_lints/src/matches/match_single_binding.rs index 6a76c6cedd0..9790e63821d 100644 --- a/clippy_lints/src/matches/match_single_binding.rs +++ b/clippy_lints/src/matches/match_single_binding.rs @@ -1,11 +1,18 @@ +use std::ops::ControlFlow; + use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::macros::HirNode; -use clippy_utils::source::{indent_of, snippet, snippet_block_with_context, snippet_with_context}; +use clippy_utils::source::{ + RelativeIndent, indent_of, reindent_multiline_relative, snippet, snippet_block_with_context, snippet_with_context, +}; use clippy_utils::{is_refutable, peel_blocks}; +use rustc_data_structures::fx::FxHashSet; use rustc_errors::Applicability; -use rustc_hir::{Arm, Expr, ExprKind, Node, PatKind, StmtKind}; +use rustc_hir::def::Res; +use rustc_hir::intravisit::{Visitor, walk_block, walk_expr, walk_path, walk_stmt}; +use rustc_hir::{Arm, Block, Expr, ExprKind, HirId, Node, PatKind, Path, Stmt, StmtKind}; use rustc_lint::LateContext; -use rustc_span::Span; +use rustc_span::{Span, Symbol}; use super::MATCH_SINGLE_BINDING; @@ -50,10 +57,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e cx, (ex, expr), (bind_names, matched_vars), - &snippet_body, + snippet_body, &mut app, Some(span), true, + is_var_binding_used_later(cx, expr, &arms[0]), ); span_lint_and_sugg( @@ -83,10 +91,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e cx, (ex, expr), (bind_names, matched_vars), - &snippet_body, + snippet_body, &mut app, None, true, + is_var_binding_used_later(cx, expr, &arms[0]), ); (expr.span, sugg) }, @@ -108,10 +117,11 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e cx, (ex, expr), (bind_names, matched_vars), - &snippet_body, + snippet_body, &mut app, None, false, + true, ); span_lint_and_sugg( @@ -139,6 +149,125 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e } } +struct VarBindingVisitor<'a, 'tcx> { + cx: &'a LateContext<'tcx>, + identifiers: FxHashSet<Symbol>, +} + +impl<'tcx> Visitor<'tcx> for VarBindingVisitor<'_, 'tcx> { + type Result = ControlFlow<()>; + + fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) -> Self::Result { + if let Res::Local(_) = path.res + && let [segment] = path.segments + && self.identifiers.contains(&segment.ident.name) + { + return ControlFlow::Break(()); + } + + walk_path(self, path) + } + + fn visit_block(&mut self, block: &'tcx Block<'tcx>) -> Self::Result { + let before = self.identifiers.clone(); + walk_block(self, block)?; + self.identifiers = before; + ControlFlow::Continue(()) + } + + fn visit_stmt(&mut self, stmt: &'tcx Stmt<'tcx>) -> Self::Result { + if let StmtKind::Let(let_stmt) = stmt.kind { + if let Some(init) = let_stmt.init { + self.visit_expr(init)?; + } + + let_stmt.pat.each_binding(|_, _, _, ident| { + self.identifiers.remove(&ident.name); + }); + } + walk_stmt(self, stmt) + } + + fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result { + match expr.kind { + ExprKind::If( + Expr { + kind: ExprKind::Let(let_expr), + .. + }, + then, + else_, + ) => { + self.visit_expr(let_expr.init)?; + let before = self.identifiers.clone(); + let_expr.pat.each_binding(|_, _, _, ident| { + self.identifiers.remove(&ident.name); + }); + + self.visit_expr(then)?; + self.identifiers = before; + if let Some(else_) = else_ { + self.visit_expr(else_)?; + } + ControlFlow::Continue(()) + }, + ExprKind::Closure(closure) => { + let body = self.cx.tcx.hir_body(closure.body); + let before = self.identifiers.clone(); + for param in body.params { + param.pat.each_binding(|_, _, _, ident| { + self.identifiers.remove(&ident.name); + }); + } + self.visit_expr(body.value)?; + self.identifiers = before; + ControlFlow::Continue(()) + }, + ExprKind::Match(expr, arms, _) => { + self.visit_expr(expr)?; + for arm in arms { + let before = self.identifiers.clone(); + arm.pat.each_binding(|_, _, _, ident| { + self.identifiers.remove(&ident.name); + }); + if let Some(guard) = arm.guard { + self.visit_expr(guard)?; + } + self.visit_expr(arm.body)?; + self.identifiers = before; + } + ControlFlow::Continue(()) + }, + _ => walk_expr(self, expr), + } + } +} + +fn is_var_binding_used_later(cx: &LateContext<'_>, expr: &Expr<'_>, arm: &Arm<'_>) -> bool { + let Node::Stmt(stmt) = cx.tcx.parent_hir_node(expr.hir_id) else { + return false; + }; + let Node::Block(block) = cx.tcx.parent_hir_node(stmt.hir_id) else { + return false; + }; + + let mut identifiers = FxHashSet::default(); + arm.pat.each_binding(|_, _, _, ident| { + identifiers.insert(ident.name); + }); + + let mut visitor = VarBindingVisitor { cx, identifiers }; + block + .stmts + .iter() + .skip_while(|s| s.hir_id != stmt.hir_id) + .skip(1) + .any(|stmt| matches!(visitor.visit_stmt(stmt), ControlFlow::Break(()))) + || block + .expr + .is_some_and(|expr| matches!(visitor.visit_expr(expr), ControlFlow::Break(()))) +} + /// Returns true if the `ex` match expression is in a local (`let`) or assign expression fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<AssignmentExpr> { if let Node::Expr(parent_arm_expr) = cx.tcx.parent_hir_node(ex.hir_id) { @@ -161,47 +290,58 @@ fn opt_parent_assign_span<'a>(cx: &LateContext<'a>, ex: &Expr<'a>) -> Option<Ass None } -fn expr_parent_requires_curlies<'a>(cx: &LateContext<'a>, match_expr: &Expr<'a>) -> bool { +fn expr_in_nested_block(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool { + if let Node::Block(block) = cx.tcx.parent_hir_node(match_expr.hir_id) { + return block + .expr + .map_or_else(|| matches!(block.stmts, [_]), |_| block.stmts.is_empty()); + } + false +} + +fn expr_must_have_curlies(cx: &LateContext<'_>, match_expr: &Expr<'_>) -> bool { let parent = cx.tcx.parent_hir_node(match_expr.hir_id); - matches!( - parent, - Node::Expr(Expr { - kind: ExprKind::Closure { .. }, - .. - }) | Node::AnonConst(..) + if let Node::Expr(Expr { + kind: ExprKind::Closure { .. }, + .. + }) + | Node::AnonConst(..) = parent + { + return true; + } + + if let Node::Arm(arm) = &cx.tcx.parent_hir_node(match_expr.hir_id) + && let ExprKind::Match(..) = arm.body.kind + { + return true; + } + + false +} + +fn reindent_snippet_if_in_block(snippet_body: &str, has_assignment: bool) -> String { + if has_assignment || !snippet_body.starts_with('{') { + return reindent_multiline_relative(snippet_body, true, RelativeIndent::Add(0)); + } + + reindent_multiline_relative( + snippet_body.trim_start_matches('{').trim_end_matches('}').trim(), + false, + RelativeIndent::Sub(4), ) } +#[expect(clippy::too_many_arguments)] fn sugg_with_curlies<'a>( cx: &LateContext<'a>, (ex, match_expr): (&Expr<'a>, &Expr<'a>), (bind_names, matched_vars): (Span, Span), - snippet_body: &str, + mut snippet_body: String, applicability: &mut Applicability, assignment: Option<Span>, needs_var_binding: bool, + is_var_binding_used_later: bool, ) -> String { - let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0)); - - let (mut cbrace_start, mut cbrace_end) = (String::new(), String::new()); - if expr_parent_requires_curlies(cx, match_expr) { - cbrace_end = format!("\n{indent}}}"); - // Fix body indent due to the closure - indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); - cbrace_start = format!("{{\n{indent}"); - } - - // If the parent is already an arm, and the body is another match statement, - // we need curly braces around suggestion - if let Node::Arm(arm) = &cx.tcx.parent_hir_node(match_expr.hir_id) - && let ExprKind::Match(..) = arm.body.kind - { - cbrace_end = format!("\n{indent}}}"); - // Fix body indent due to the match - indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); - cbrace_start = format!("{{\n{indent}"); - } - let assignment_str = assignment.map_or_else(String::new, |span| { let mut s = snippet(cx, span, "..").to_string(); s.push_str(" = "); @@ -221,5 +361,17 @@ fn sugg_with_curlies<'a>( .to_string() }; + let mut indent = " ".repeat(indent_of(cx, ex.span).unwrap_or(0)); + let (mut cbrace_start, mut cbrace_end) = (String::new(), String::new()); + if !expr_in_nested_block(cx, match_expr) + && ((needs_var_binding && is_var_binding_used_later) || expr_must_have_curlies(cx, match_expr)) + { + cbrace_end = format!("\n{indent}}}"); + // Fix body indent due to the closure + indent = " ".repeat(indent_of(cx, bind_names).unwrap_or(0)); + cbrace_start = format!("{{\n{indent}"); + snippet_body = reindent_snippet_if_in_block(&snippet_body, !assignment_str.is_empty()); + } + format!("{cbrace_start}{scrutinee};\n{indent}{assignment_str}{snippet_body}{cbrace_end}") } diff --git a/clippy_utils/src/source.rs b/clippy_utils/src/source.rs index 7f2bf99daff..b490902429f 100644 --- a/clippy_utils/src/source.rs +++ b/clippy_utils/src/source.rs @@ -18,7 +18,7 @@ use rustc_span::{ }; use std::borrow::Cow; use std::fmt; -use std::ops::{Deref, Index, Range}; +use std::ops::{Add, Deref, Index, Range}; pub trait HasSession { fn sess(&self) -> &Session; @@ -476,6 +476,38 @@ pub fn position_before_rarrow(s: &str) -> Option<usize> { }) } +pub enum RelativeIndent { + Add(usize), + Sub(usize), +} + +impl Add<RelativeIndent> for usize { + type Output = usize; + + fn add(self, rhs: RelativeIndent) -> Self::Output { + match rhs { + RelativeIndent::Add(n) => self + n, + RelativeIndent::Sub(n) => self.saturating_sub(n), + } + } +} + +/// Reindents a multiline string with possibility of ignoring the first line and relative +/// indentation. +pub fn reindent_multiline_relative(s: &str, ignore_first: bool, relative_indent: RelativeIndent) -> String { + fn indent_of_string(s: &str) -> usize { + s.find(|c: char| !c.is_whitespace()).unwrap_or(0) + } + + let mut indent = 0; + if let Some(line) = s.lines().nth(usize::from(ignore_first)) { + let line_indent = indent_of_string(line); + indent = line_indent + relative_indent; + } + + reindent_multiline(s, ignore_first, Some(indent)) +} + /// Reindent a multiline string with possibility of ignoring the first line. pub fn reindent_multiline(s: &str, ignore_first: bool, indent: Option<usize>) -> String { let s_space = reindent_multiline_inner(s, ignore_first, indent, ' '); diff --git a/tests/ui/match_single_binding.fixed b/tests/ui/match_single_binding.fixed index e11dea35204..d8d865ee44c 100644 --- a/tests/ui/match_single_binding.fixed +++ b/tests/ui/match_single_binding.fixed @@ -204,3 +204,56 @@ mod issue14991 { }], } } + +mod issue15018 { + fn used_later(a: i32, b: i32, c: i32) { + let x = 1; + { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z); + } + println!("x = {x}"); + } + + fn not_used_later(a: i32, b: i32, c: i32) { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z) + } + + #[allow(irrefutable_let_patterns)] + fn not_used_later_but_shadowed(a: i32, b: i32, c: i32) { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z); + let x = 1; + println!("x = {x}"); + } + + #[allow(irrefutable_let_patterns)] + fn not_used_later_but_shadowed_nested(a: i32, b: i32, c: i32) { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z); + if let (x, y, z) = (a, b, c) { + println!("{} {} {}", x, y, z) + } + + { + let x: i32 = 1; + { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z); + } + if let (x, y, z) = (a, x, c) { + println!("{} {} {}", x, y, z) + } + } + + { + let (x, y, z) = (a, b, c); + println!("{} {} {}", x, y, z); + let fn_ = |y| { + println!("{} {} {}", a, b, y); + }; + fn_(c); + } + } +} diff --git a/tests/ui/match_single_binding.rs b/tests/ui/match_single_binding.rs index d498da30fc8..ecff945fb91 100644 --- a/tests/ui/match_single_binding.rs +++ b/tests/ui/match_single_binding.rs @@ -267,3 +267,64 @@ mod issue14991 { }], } } + +mod issue15018 { + fn used_later(a: i32, b: i32, c: i32) { + let x = 1; + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + println!("x = {x}"); + } + + fn not_used_later(a: i32, b: i32, c: i32) { + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + } + + #[allow(irrefutable_let_patterns)] + fn not_used_later_but_shadowed(a: i32, b: i32, c: i32) { + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + let x = 1; + println!("x = {x}"); + } + + #[allow(irrefutable_let_patterns)] + fn not_used_later_but_shadowed_nested(a: i32, b: i32, c: i32) { + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + if let (x, y, z) = (a, b, c) { + println!("{} {} {}", x, y, z) + } + + { + let x: i32 = 1; + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + if let (x, y, z) = (a, x, c) { + println!("{} {} {}", x, y, z) + } + } + + { + match (a, b, c) { + //~^ match_single_binding + (x, y, z) => println!("{} {} {}", x, y, z), + } + let fn_ = |y| { + println!("{} {} {}", a, b, y); + }; + fn_(c); + } + } +} diff --git a/tests/ui/match_single_binding.stderr b/tests/ui/match_single_binding.stderr index f274f80c81d..789626de603 100644 --- a/tests/ui/match_single_binding.stderr +++ b/tests/ui/match_single_binding.stderr @@ -411,5 +411,99 @@ LL ~ let _n = 1; LL + 42 | -error: aborting due to 29 previous errors +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:274:9 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_________^ + | +help: consider using a `let` statement + | +LL ~ { +LL + let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z); +LL + } + | + +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:282:9 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_________^ + | +help: consider using a `let` statement + | +LL ~ let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z) + | + +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:290:9 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_________^ + | +help: consider using a `let` statement + | +LL ~ let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z); + | + +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:300:9 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_________^ + | +help: consider using a `let` statement + | +LL ~ let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z); + | + +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:310:13 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_____________^ + | +help: consider using a `let` statement + | +LL ~ { +LL + let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z); +LL + } + | + +error: this match could be written as a `let` statement + --> tests/ui/match_single_binding.rs:320:13 + | +LL | / match (a, b, c) { +LL | | +LL | | (x, y, z) => println!("{} {} {}", x, y, z), +LL | | } + | |_____________^ + | +help: consider using a `let` statement + | +LL ~ let (x, y, z) = (a, b, c); +LL + println!("{} {} {}", x, y, z); + | + +error: aborting due to 35 previous errors |
