blob: 48efd02301753b287c6e41ea14e83b796cb4c649 [file] [log] [blame]
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::{expr_block, get_source_text, snippet};
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, peel_mid_ty_refs};
use clippy_utils::{is_lint_allowed, is_unit_expr, is_wild, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs};
use core::cmp::max;
use rustc_errors::Applicability;
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, Pat, PatKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, Ty};
use rustc_span::{sym, Span};
use super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE};
/// Checks if there are comments contained within a span.
/// This is a very "naive" check, as it just looks for the literal characters // and /* in the
/// source text. This won't be accurate if there are potentially expressions contained within the
/// span, e.g. a string literal `"//"`, but we know that this isn't the case for empty
/// match arms.
fn empty_arm_has_comment(cx: &LateContext<'_>, span: Span) -> bool {
if let Some(ff) = get_source_text(cx, span)
&& let Some(text) = ff.as_str()
{
text.as_bytes().windows(2).any(|w| w == b"//" || w == b"/*")
} else {
false
}
}
#[rustfmt::skip]
pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) {
if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() {
if expr.span.from_expansion() {
// Don't lint match expressions present in
// macro_rules! block
return;
}
if let PatKind::Or(..) = arms[0].pat.kind {
// don't lint for or patterns for now, this makes
// the lint noisy in unnecessary situations
return;
}
let els = arms[1].body;
let els = if is_unit_expr(peel_blocks(els)) && !empty_arm_has_comment(cx, els.span) {
None
} else if let ExprKind::Block(Block { stmts, expr: block_expr, .. }, _) = els.kind {
if stmts.len() == 1 && block_expr.is_none() || stmts.is_empty() && block_expr.is_some() {
// single statement/expr "else" block, don't lint
return;
}
// block with 2+ statements or 1 expr and 1+ statement
Some(els)
} else {
// not a block or an empty block w/ comments, don't lint
return;
};
let ty = cx.typeck_results().expr_ty(ex);
if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) {
check_single_pattern(cx, ex, arms, expr, els);
check_opt_like(cx, ex, arms, expr, ty, els);
}
}
}
fn check_single_pattern(
cx: &LateContext<'_>,
ex: &Expr<'_>,
arms: &[Arm<'_>],
expr: &Expr<'_>,
els: Option<&Expr<'_>>,
) {
if is_wild(arms[1].pat) {
report_single_pattern(cx, ex, arms, expr, els);
}
}
fn report_single_pattern(
cx: &LateContext<'_>,
ex: &Expr<'_>,
arms: &[Arm<'_>],
expr: &Expr<'_>,
els: Option<&Expr<'_>>,
) {
let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH };
let ctxt = expr.span.ctxt();
let mut app = Applicability::HasPlaceholders;
let els_str = els.map_or(String::new(), |els| {
format!(" else {}", expr_block(cx, els, ctxt, "..", Some(expr.span), &mut app))
});
let (pat, pat_ref_count) = peel_hir_pat_refs(arms[0].pat);
let (msg, sugg) = if_chain! {
if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind;
let (ty, ty_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(ex));
if let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait();
if let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait();
if ty.is_integral() || ty.is_char() || ty.is_str()
|| (implements_trait(cx, ty, spe_trait_id, &[])
&& implements_trait(cx, ty, pe_trait_id, &[ty.into()]));
then {
// scrutinee derives PartialEq and the pattern is a constant.
let pat_ref_count = match pat.kind {
// string literals are already a reference.
PatKind::Lit(Expr { kind: ExprKind::Lit(lit), .. }) if lit.node.is_str() => pat_ref_count + 1,
_ => pat_ref_count,
};
// References are only implicitly added to the pattern, so no overflow here.
// e.g. will work: match &Some(_) { Some(_) => () }
// will not: match Some(_) { &Some(_) => () }
let ref_count_diff = ty_ref_count - pat_ref_count;
// Try to remove address of expressions first.
let (ex, removed) = peel_n_hir_expr_refs(ex, ref_count_diff);
let ref_count_diff = ref_count_diff - removed;
let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`";
let sugg = format!(
"if {} == {}{} {}{els_str}",
snippet(cx, ex.span, ".."),
// PartialEq for different reference counts may not exist.
"&".repeat(ref_count_diff),
snippet(cx, arms[0].pat.span, ".."),
expr_block(cx, arms[0].body, ctxt, "..", Some(expr.span), &mut app),
);
(msg, sugg)
} else {
let msg = "you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`";
let sugg = format!(
"if let {} = {} {}{els_str}",
snippet(cx, arms[0].pat.span, ".."),
snippet(cx, ex.span, ".."),
expr_block(cx, arms[0].body, ctxt, "..", Some(expr.span), &mut app),
);
(msg, sugg)
}
};
span_lint_and_sugg(cx, lint, expr.span, msg, "try", sugg, app);
}
fn check_opt_like<'a>(
cx: &LateContext<'a>,
ex: &Expr<'_>,
arms: &[Arm<'_>],
expr: &Expr<'_>,
ty: Ty<'a>,
els: Option<&Expr<'_>>,
) {
// We don't want to lint if the second arm contains an enum which could
// have more variants in the future.
if form_exhaustive_matches(cx, ty, arms[0].pat, arms[1].pat) {
report_single_pattern(cx, ex, arms, expr, els);
}
}
/// Returns `true` if all of the types in the pattern are enums which we know
/// won't be expanded in the future
fn pat_in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'a>, pat: &Pat<'_>) -> bool {
let mut paths_and_types = Vec::new();
collect_pat_paths(&mut paths_and_types, cx, pat, ty);
paths_and_types.iter().all(|ty| in_candidate_enum(cx, *ty))
}
/// Returns `true` if the given type is an enum we know won't be expanded in the future
fn in_candidate_enum(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
// list of candidate `Enum`s we know will never get any more members
let candidates = [sym::Cow, sym::Option, sym::Result];
for candidate_ty in candidates {
if is_type_diagnostic_item(cx, ty, candidate_ty) {
return true;
}
}
false
}
/// Collects types from the given pattern
fn collect_pat_paths<'a>(acc: &mut Vec<Ty<'a>>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) {
match pat.kind {
PatKind::Tuple(inner, _) => inner.iter().for_each(|p| {
let p_ty = cx.typeck_results().pat_ty(p);
collect_pat_paths(acc, cx, p, p_ty);
}),
PatKind::TupleStruct(..) | PatKind::Binding(BindingAnnotation::NONE, .., None) | PatKind::Path(_) => {
acc.push(ty);
},
_ => {},
}
}
/// Returns true if the given arm of pattern matching contains wildcard patterns.
fn contains_only_wilds(pat: &Pat<'_>) -> bool {
match pat.kind {
PatKind::Wild => true,
PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds),
_ => false,
}
}
/// Returns true if the given patterns forms only exhaustive matches that don't contain enum
/// patterns without a wildcard.
fn form_exhaustive_matches<'a>(cx: &LateContext<'a>, ty: Ty<'a>, left: &Pat<'_>, right: &Pat<'_>) -> bool {
match (&left.kind, &right.kind) {
(PatKind::Wild, _) | (_, PatKind::Wild) => true,
(PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
// We don't actually know the position and the presence of the `..` (dotdot) operator
// in the arms, so we need to evaluate the correct offsets here in order to iterate in
// both arms at the same time.
let left_pos = left_pos.as_opt_usize();
let right_pos = right_pos.as_opt_usize();
let len = max(
left_in.len() + usize::from(left_pos.is_some()),
right_in.len() + usize::from(right_pos.is_some()),
);
let mut left_pos = left_pos.unwrap_or(usize::MAX);
let mut right_pos = right_pos.unwrap_or(usize::MAX);
let mut left_dot_space = 0;
let mut right_dot_space = 0;
for i in 0..len {
let mut found_dotdot = false;
if i == left_pos {
left_dot_space += 1;
if left_dot_space < len - left_in.len() {
left_pos += 1;
}
found_dotdot = true;
}
if i == right_pos {
right_dot_space += 1;
if right_dot_space < len - right_in.len() {
right_pos += 1;
}
found_dotdot = true;
}
if found_dotdot {
continue;
}
if !contains_only_wilds(&left_in[i - left_dot_space])
&& !contains_only_wilds(&right_in[i - right_dot_space])
{
return false;
}
}
true
},
(PatKind::TupleStruct(..), PatKind::Path(_)) => pat_in_candidate_enum(cx, ty, right),
(PatKind::TupleStruct(..), PatKind::TupleStruct(_, inner, _)) => {
pat_in_candidate_enum(cx, ty, right) && inner.iter().all(contains_only_wilds)
},
_ => false,
}
}