diff --git a/clippy_lints/src/matches/match_single_binding.rs b/clippy_lints/src/matches/match_single_binding.rs index 6a76c6cedd0d..9bbc6042fe1a 100644 --- a/clippy_lints/src/matches/match_single_binding.rs +++ b/clippy_lints/src/matches/match_single_binding.rs @@ -1,11 +1,16 @@ +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::{is_refutable, peel_blocks}; +use clippy_utils::source::{indent_of, reindent_multiline, snippet, snippet_block_with_context, snippet_with_context}; +use clippy_utils::{is_expr_identity_of_pat, 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 +55,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( @@ -78,15 +84,28 @@ pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], e snippet_with_context(cx, pat_span, ctxt, "..", &mut app).0 ), ), + None if is_expr_identity_of_pat(cx, arms[0].pat, ex, false) => { + span_lint_and_sugg( + cx, + MATCH_SINGLE_BINDING, + expr.span, + "this match could be replaced by its body itself", + "consider using the match body instead", + snippet_body, + Applicability::MachineApplicable, + ); + return; + }, None => { let sugg = sugg_with_curlies( 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 +127,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 +159,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, +} + +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 { 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(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(..) | ExprKind::Binary(..), + .. + }) + | 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 indent_of_nth_line(snippet: &str, nth: usize) -> Option { + snippet + .lines() + .nth(nth) + .and_then(|s| s.find(|c: char| !c.is_whitespace())) +} + +fn reindent_snippet_if_in_block(snippet_body: &str, has_assignment: bool) -> String { + if has_assignment || !snippet_body.starts_with('{') { + return reindent_multiline(snippet_body, true, indent_of_nth_line(snippet_body, 1)); + } + + let snippet_body = snippet_body.trim_start_matches('{').trim_end_matches('}').trim(); + reindent_multiline( + snippet_body, + false, + indent_of_nth_line(snippet_body, 0).map(|indent| indent.saturating_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, 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 +379,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/lib.rs b/clippy_utils/src/lib.rs index 8b9cd6a54dd6..febc13d89598 100644 --- a/clippy_utils/src/lib.rs +++ b/clippy_utils/src/lib.rs @@ -1900,39 +1900,6 @@ pub fn is_must_use_func_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { /// /// Consider calling [`is_expr_untyped_identity_function`] or [`is_expr_identity_function`] instead. fn is_body_identity_function(cx: &LateContext<'_>, func: &Body<'_>) -> bool { - fn check_pat(cx: &LateContext<'_>, pat: &Pat<'_>, expr: &Expr<'_>) -> bool { - if cx - .typeck_results() - .pat_binding_modes() - .get(pat.hir_id) - .is_some_and(|mode| matches!(mode.0, ByRef::Yes(_))) - { - // If the parameter is `(x, y)` of type `&(T, T)`, or `[x, y]` of type `&[T; 2]`, then - // due to match ergonomics, the inner patterns become references. Don't consider this - // the identity function as that changes types. - return false; - } - - match (pat.kind, expr.kind) { - (PatKind::Binding(_, id, _, _), _) => { - path_to_local_id(expr, id) && cx.typeck_results().expr_adjustments(expr).is_empty() - }, - (PatKind::Tuple(pats, dotdot), ExprKind::Tup(tup)) - if dotdot.as_opt_usize().is_none() && pats.len() == tup.len() => - { - pats.iter().zip(tup).all(|(pat, expr)| check_pat(cx, pat, expr)) - }, - (PatKind::Slice(before, slice, after), ExprKind::Array(arr)) - if slice.is_none() && before.len() + after.len() == arr.len() => - { - (before.iter().chain(after)) - .zip(arr) - .all(|(pat, expr)| check_pat(cx, pat, expr)) - }, - _ => false, - } - } - let [param] = func.params else { return false; }; @@ -1965,11 +1932,56 @@ fn is_body_identity_function(cx: &LateContext<'_>, func: &Body<'_>) -> bool { return false; } }, - _ => return check_pat(cx, param.pat, expr), + _ => return is_expr_identity_of_pat(cx, param.pat, expr, true), } } } +/// Checks if the given expression is an identity representation of the given pattern: +/// * `x` is the identity representation of `x` +/// * `(x, y)` is the identity representation of `(x, y)` +/// * `[x, y]` is the identity representation of `[x, y]` +/// +/// Note that `by_hir` is used to determine bindings are checked by their `HirId` or by their name. +/// This can be useful when checking patterns in `let` bindings or `match` arms. +pub fn is_expr_identity_of_pat(cx: &LateContext<'_>, pat: &Pat<'_>, expr: &Expr<'_>, by_hir: bool) -> bool { + if cx + .typeck_results() + .pat_binding_modes() + .get(pat.hir_id) + .is_some_and(|mode| matches!(mode.0, ByRef::Yes(_))) + { + // If the parameter is `(x, y)` of type `&(T, T)`, or `[x, y]` of type `&[T; 2]`, then + // due to match ergonomics, the inner patterns become references. Don't consider this + // the identity function as that changes types. + return false; + } + + match (pat.kind, expr.kind) { + (PatKind::Binding(_, id, _, _), _) if by_hir => { + path_to_local_id(expr, id) && cx.typeck_results().expr_adjustments(expr).is_empty() + }, + (PatKind::Binding(_, _, ident, _), ExprKind::Path(QPath::Resolved(_, path))) => { + matches!(path.segments, [ segment] if segment.ident.name == ident.name) + }, + (PatKind::Tuple(pats, dotdot), ExprKind::Tup(tup)) + if dotdot.as_opt_usize().is_none() && pats.len() == tup.len() => + { + pats.iter() + .zip(tup) + .all(|(pat, expr)| is_expr_identity_of_pat(cx, pat, expr, by_hir)) + }, + (PatKind::Slice(before, slice, after), ExprKind::Array(arr)) + if slice.is_none() && before.len() + after.len() == arr.len() => + { + (before.iter().chain(after)) + .zip(arr) + .all(|(pat, expr)| is_expr_identity_of_pat(cx, pat, expr, by_hir)) + }, + _ => false, + } +} + /// This is the same as [`is_expr_identity_function`], but does not consider closures /// with type annotations for its bindings (or similar) as identity functions: /// * `|x: u8| x` diff --git a/tests/ui/match_single_binding.fixed b/tests/ui/match_single_binding.fixed index e11dea352049..e29fb87dbc30 100644 --- a/tests/ui/match_single_binding.fixed +++ b/tests/ui/match_single_binding.fixed @@ -204,3 +204,65 @@ 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); + } + } +} + +#[allow(clippy::short_circuit_statement)] +fn issue15269(a: usize, b: usize, c: usize) -> bool { + a < b + && b < c; + + a < b + && b < c +} diff --git a/tests/ui/match_single_binding.rs b/tests/ui/match_single_binding.rs index d498da30fc87..ede1ab32beb5 100644 --- a/tests/ui/match_single_binding.rs +++ b/tests/ui/match_single_binding.rs @@ -267,3 +267,79 @@ 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); + } + } +} + +#[allow(clippy::short_circuit_statement)] +fn issue15269(a: usize, b: usize, c: usize) -> bool { + a < b + && match b { + //~^ match_single_binding + b => b < c, + }; + + a < b + && match (a, b) { + //~^ match_single_binding + (a, b) => b < c, + } +} diff --git a/tests/ui/match_single_binding.stderr b/tests/ui/match_single_binding.stderr index f274f80c81da..eea71777890e 100644 --- a/tests/ui/match_single_binding.stderr +++ b/tests/ui/match_single_binding.stderr @@ -411,5 +411,119 @@ 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: this match could be replaced by its body itself + --> tests/ui/match_single_binding.rs:335:12 + | +LL | && match b { + | ____________^ +LL | | +LL | | b => b < c, +LL | | }; + | |_________^ help: consider using the match body instead: `b < c` + +error: this match could be replaced by its body itself + --> tests/ui/match_single_binding.rs:341:12 + | +LL | && match (a, b) { + | ____________^ +LL | | +LL | | (a, b) => b < c, +LL | | } + | |_________^ help: consider using the match body instead: `b < c` + +error: aborting due to 37 previous errors