| use std::ops::ControlFlow; |
| |
| use clippy_utils::diagnostics::span_lint_and_sugg; |
| use clippy_utils::macros::HirNode; |
| 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::def::Res; |
| use rustc_hir::intravisit::{Visitor, walk_block, walk_expr, walk_path, walk_stmt}; |
| use rustc_hir::{Arm, Block, Expr, ExprKind, HirId, Item, ItemKind, Node, PatKind, Path, Stmt, StmtKind}; |
| use rustc_lint::LateContext; |
| use rustc_span::{Span, Symbol}; |
| |
| use super::MATCH_SINGLE_BINDING; |
| |
| #[derive(Debug)] |
| enum AssignmentExpr { |
| Assign { span: Span, match_span: Span }, |
| Local { span: Span, pat_span: Span }, |
| } |
| |
| #[expect(clippy::too_many_lines)] |
| pub(crate) fn check<'a>(cx: &LateContext<'a>, ex: &Expr<'a>, arms: &[Arm<'_>], expr: &Expr<'a>) { |
| if expr.span.from_expansion() || arms.len() != 1 || is_refutable(cx, arms[0].pat) { |
| return; |
| } |
| |
| let matched_vars = ex.span; |
| let bind_names = arms[0].pat.span; |
| let match_body = peel_blocks(arms[0].body); |
| let mut app = Applicability::MaybeIncorrect; |
| let ctxt = expr.span.ctxt(); |
| let mut snippet_body = snippet_block_with_context(cx, match_body.span, ctxt, "..", Some(expr.span), &mut app).0; |
| |
| // Do we need to add ';' to suggestion ? |
| if let Node::Stmt(stmt) = cx.tcx.parent_hir_node(expr.hir_id) |
| && let StmtKind::Expr(_) = stmt.kind |
| && match match_body.kind { |
| // We don't need to add a ; to blocks, unless that block is from a macro expansion |
| ExprKind::Block(block, _) => block.span.from_expansion(), |
| _ => true, |
| } |
| { |
| snippet_body.push(';'); |
| } |
| |
| match arms[0].pat.kind { |
| PatKind::Binding(..) | PatKind::Tuple(_, _) | PatKind::Struct(..) => { |
| let (target_span, sugg) = match opt_parent_assign_span(cx, ex) { |
| Some(AssignmentExpr::Assign { span, match_span }) => { |
| let sugg = sugg_with_curlies( |
| cx, |
| (ex, expr), |
| (bind_names, matched_vars), |
| snippet_body, |
| &mut app, |
| Some(span), |
| true, |
| is_var_binding_used_later(cx, expr, &arms[0]), |
| ); |
| |
| span_lint_and_sugg( |
| cx, |
| MATCH_SINGLE_BINDING, |
| span.to(match_span), |
| "this assignment could be simplified", |
| "consider removing the `match` expression", |
| sugg, |
| app, |
| ); |
| |
| return; |
| }, |
| Some(AssignmentExpr::Local { span, pat_span }) => ( |
| span, |
| format!( |
| "let {} = {};\n{}let {} = {snippet_body};", |
| snippet_with_context(cx, bind_names, ctxt, "..", &mut app).0, |
| snippet_with_context(cx, matched_vars, ctxt, "..", &mut app).0, |
| " ".repeat(indent_of(cx, expr.span).unwrap_or(0)), |
| 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, |
| &mut app, |
| None, |
| true, |
| is_var_binding_used_later(cx, expr, &arms[0]), |
| ); |
| (expr.span, sugg) |
| }, |
| }; |
| |
| span_lint_and_sugg( |
| cx, |
| MATCH_SINGLE_BINDING, |
| target_span, |
| "this match could be written as a `let` statement", |
| "consider using a `let` statement", |
| sugg, |
| app, |
| ); |
| }, |
| PatKind::Wild => { |
| if ex.can_have_side_effects() { |
| let sugg = sugg_with_curlies( |
| cx, |
| (ex, expr), |
| (bind_names, matched_vars), |
| snippet_body, |
| &mut app, |
| None, |
| false, |
| true, |
| ); |
| |
| span_lint_and_sugg( |
| cx, |
| MATCH_SINGLE_BINDING, |
| expr.span, |
| "this match could be replaced by its scrutinee and body", |
| "consider using the scrutinee and body instead", |
| sugg, |
| app, |
| ); |
| } else { |
| 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, |
| ); |
| } |
| }, |
| _ => (), |
| } |
| } |
| |
| 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) { |
| return match cx.tcx.parent_hir_node(parent_arm_expr.hir_id) { |
| Node::LetStmt(parent_let_expr) => Some(AssignmentExpr::Local { |
| span: parent_let_expr.span, |
| pat_span: parent_let_expr.pat.span(), |
| }), |
| Node::Expr(Expr { |
| kind: ExprKind::Assign(parent_assign_expr, match_expr, _), |
| .. |
| }) => Some(AssignmentExpr::Assign { |
| span: parent_assign_expr.span, |
| match_span: match_expr.span, |
| }), |
| _ => None, |
| }; |
| } |
| |
| None |
| } |
| |
| 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 indent_of_nth_line(snippet: &str, nth: usize) -> Option<usize> { |
| 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), |
| mut snippet_body: String, |
| applicability: &mut Applicability, |
| assignment: Option<Span>, |
| needs_var_binding: bool, |
| is_var_binding_used_later: bool, |
| ) -> String { |
| let assignment_str = assignment.map_or_else(String::new, |span| { |
| let mut s = snippet(cx, span, "..").to_string(); |
| s.push_str(" = "); |
| s |
| }); |
| |
| let ctxt = match_expr.span.ctxt(); |
| let scrutinee = if needs_var_binding { |
| format!( |
| "let {} = {}", |
| snippet_with_context(cx, bind_names, ctxt, "..", applicability).0, |
| snippet_with_context(cx, matched_vars, ctxt, "..", applicability).0 |
| ) |
| } else { |
| snippet_with_context(cx, matched_vars, ctxt, "..", applicability) |
| .0 |
| .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()); |
| let mut add_curlies = || { |
| 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()); |
| }; |
| |
| if !expr_in_nested_block(cx, match_expr) { |
| let mut parent = cx.tcx.parent_hir_node(match_expr.hir_id); |
| if let Node::Expr(Expr { |
| kind: ExprKind::Assign(..), |
| hir_id, |
| .. |
| }) = parent |
| { |
| parent = cx.tcx.parent_hir_node(*hir_id); |
| } |
| if let Node::Stmt(stmt) = parent { |
| parent = cx.tcx.parent_hir_node(stmt.hir_id); |
| } |
| |
| match parent { |
| Node::Block(..) |
| | Node::Expr(Expr { |
| kind: ExprKind::Block(..) | ExprKind::ConstBlock(..), |
| .. |
| }) => { |
| if needs_var_binding && is_var_binding_used_later { |
| add_curlies(); |
| } |
| }, |
| Node::Expr(..) |
| | Node::AnonConst(..) |
| | Node::Item(Item { |
| kind: ItemKind::Const(..), |
| .. |
| }) => add_curlies(), |
| Node::Arm(arm) if let ExprKind::Match(..) = arm.body.kind => add_curlies(), |
| _ => {}, |
| } |
| } |
| |
| format!("{cbrace_start}{scrutinee};\n{indent}{assignment_str}{snippet_body}{cbrace_end}") |
| } |