blob: e40e21c490f3c7852d0c0b26f7d63ac2d2c06e41 [file] [log] [blame] [edit]
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}")
}