blob: a92bfd45df0ceef0d7fa6095c71f39e02ed9c78f [file] [log] [blame] [edit]
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::is_in_const_context;
use clippy_utils::source::snippet_with_context;
use clippy_utils::ty::implements_trait;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, Pat, PatKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::Ty;
use rustc_session::declare_lint_pass;
declare_clippy_lint! {
/// ### What it does
/// Checks for pattern matchings that can be expressed using equality.
///
/// ### Why is this bad?
///
/// * It reads better and has less cognitive load because equality won't cause binding.
/// * It is a [Yoda condition](https://en.wikipedia.org/wiki/Yoda_conditions). Yoda conditions are widely
/// criticized for increasing the cognitive load of reading the code.
/// * Equality is a simple bool expression and can be merged with `&&` and `||` and
/// reuse if blocks
///
/// ### Example
/// ```rust,ignore
/// if let Some(2) = x {
/// do_thing();
/// }
/// ```
/// Use instead:
/// ```rust,ignore
/// if x == Some(2) {
/// do_thing();
/// }
/// ```
#[clippy::version = "1.57.0"]
pub EQUATABLE_IF_LET,
nursery,
"using pattern matching instead of equality"
}
declare_lint_pass!(PatternEquality => [EQUATABLE_IF_LET]);
/// detects if pattern matches just one thing
fn is_unary_pattern(pat: &Pat<'_>) -> bool {
fn array_rec(pats: &[Pat<'_>]) -> bool {
pats.iter().all(is_unary_pattern)
}
match &pat.kind {
PatKind::Missing => unreachable!(),
PatKind::Slice(_, _, _)
| PatKind::Range(_, _, _)
| PatKind::Binding(..)
| PatKind::Wild
| PatKind::Never
| PatKind::Or(_)
| PatKind::Err(_) => false,
PatKind::Struct(_, a, etc) => etc.is_none() && a.iter().all(|x| is_unary_pattern(x.pat)),
PatKind::Tuple(a, etc) | PatKind::TupleStruct(_, a, etc) => etc.as_opt_usize().is_none() && array_rec(a),
PatKind::Ref(x, _, _) | PatKind::Box(x) | PatKind::Deref(x) | PatKind::Guard(x, _) => is_unary_pattern(x),
PatKind::Expr(_) => true,
}
}
fn is_structural_partial_eq<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>, other: Ty<'tcx>) -> bool {
if let Some(def_id) = cx.tcx.lang_items().eq_trait() {
implements_trait(cx, ty, def_id, &[other.into()])
} else {
false
}
}
/// Check if the pattern has any type mismatch that would prevent it from being used in an equality
/// check. This can happen if the expr has a reference type and the corresponding pattern is a
/// literal.
fn contains_type_mismatch(cx: &LateContext<'_>, pat: &Pat<'_>) -> bool {
let mut result = false;
pat.walk(|p| {
if result {
return false;
}
if p.span.in_external_macro(cx.sess().source_map()) {
return true;
}
let adjust_pat = match p.kind {
PatKind::Or([p, ..]) => p,
_ => p,
};
if let Some(adjustments) = cx.typeck_results().pat_adjustments().get(adjust_pat.hir_id)
&& adjustments.first().is_some_and(|first| first.source.is_ref())
{
result = true;
return false;
}
true
});
result
}
impl<'tcx> LateLintPass<'tcx> for PatternEquality {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
if let ExprKind::Let(let_expr) = expr.kind
&& is_unary_pattern(let_expr.pat)
&& !expr.span.in_external_macro(cx.sess().source_map())
&& !let_expr.pat.span.from_expansion()
&& !let_expr.init.span.from_expansion()
{
let exp_ty = cx.typeck_results().expr_ty(let_expr.init);
let pat_ty = cx.typeck_results().pat_ty(let_expr.pat);
let mut app = Applicability::MachineApplicable;
let ctxt = expr.span.ctxt();
if is_structural_partial_eq(cx, exp_ty, pat_ty)
&& !contains_type_mismatch(cx, let_expr.pat)
// Calls to trait methods (`PartialEq::eq` in this case) aren't stable yet. We could _technically_
// try looking at whether:
// 1) features `const_trait_impl` and `const_cmp` are enabled
// 2) implementation of `PartialEq<Rhs=PatTy> for ExpTy` has `fn eq` that is `const`
//
// but that didn't quite work out (see #15482), so we just reject outright in this case
&& !is_in_const_context(cx)
{
span_lint_and_then(
cx,
EQUATABLE_IF_LET,
expr.span,
"this pattern matching can be expressed using equality",
|diag| {
let pat_str = {
let str = snippet_with_context(cx, let_expr.pat.span, ctxt, "..", &mut app).0;
if let PatKind::Struct(..) = let_expr.pat.kind {
format!("({str})").into()
} else {
str
}
};
let sugg = format!(
"{} == {pat_str}",
snippet_with_context(cx, let_expr.init.span, ctxt, "..", &mut app).0,
);
diag.span_suggestion(expr.span, "try", sugg, app);
},
);
} else {
span_lint_and_then(
cx,
EQUATABLE_IF_LET,
expr.span,
"this pattern matching can be expressed using `matches!`",
|diag| {
let sugg = format!(
"matches!({}, {})",
snippet_with_context(cx, let_expr.init.span, ctxt, "..", &mut app).0,
snippet_with_context(cx, let_expr.pat.span, ctxt, "..", &mut app).0,
);
diag.span_suggestion(expr.span, "try", sugg, app);
},
);
}
}
}
}