1
Fork 0
rust/clippy_lints/src/matches/single_match.rs
2024-08-24 18:33:44 -04:00

369 lines
14 KiB
Rust

use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::{expr_block, snippet, SpanRangeExt};
use clippy_utils::ty::implements_trait;
use clippy_utils::{
is_lint_allowed, is_unit_expr, peel_blocks, peel_hir_pat_refs, peel_middle_ty_refs, peel_n_hir_expr_refs,
};
use core::ops::ControlFlow;
use rustc_arena::DroplessArena;
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::intravisit::{walk_pat, Visitor};
use rustc_hir::{Arm, Expr, ExprKind, HirId, Pat, PatKind, QPath};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, AdtDef, ParamEnv, TyCtxt, TypeckResults, VariantDef};
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) = span.get_source_range(cx)
&& 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<'tcx>(cx: &LateContext<'tcx>, ex: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>], expr: &'tcx Expr<'_>) {
if let [arm1, arm2] = arms
&& arm1.guard.is_none()
&& arm2.guard.is_none()
&& !expr.span.from_expansion()
// don't lint for or patterns for now, this makes
// the lint noisy in unnecessary situations
&& !matches!(arm1.pat.kind, PatKind::Or(..))
{
let els = if is_unit_expr(peel_blocks(arm2.body)) && !empty_arm_has_comment(cx, arm2.body.span) {
None
} else if let ExprKind::Block(block, _) = arm2.body.kind {
if matches!((block.stmts, block.expr), ([], Some(_)) | ([_], None)) {
// single statement/expr "else" block, don't lint
return;
}
// block with 2+ statements or 1 expr and 1+ statement
Some(arm2.body)
} else {
// not a block or an empty block w/ comments, don't lint
return;
};
let typeck = cx.typeck_results();
if *typeck.expr_ty(ex).peel_refs().kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) {
let mut v = PatVisitor {
typeck,
has_enum: false,
};
if v.visit_pat(arm2.pat).is_break() {
return;
}
if v.has_enum {
let cx = PatCtxt {
tcx: cx.tcx,
param_env: cx.param_env,
typeck,
arena: DroplessArena::default(),
};
let mut state = PatState::Other;
if !(state.add_pat(&cx, arm2.pat) || state.add_pat(&cx, arm1.pat)) {
// Don't lint if the pattern contains an enum which doesn't have a wild match.
return;
}
}
report_single_pattern(cx, ex, arm1, expr, els);
}
}
}
fn report_single_pattern(cx: &LateContext<'_>, ex: &Expr<'_>, arm: &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::MachineApplicable;
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(arm.pat);
let (msg, sugg) = if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind
&& let (ty, ty_ref_count) = peel_middle_ty_refs(cx.typeck_results().expr_ty(ex))
&& let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait()
&& let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait()
&& (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()])))
{
// 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, arm.pat.span, ".."),
expr_block(cx, arm.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, arm.pat.span, ".."),
snippet(cx, ex.span, ".."),
expr_block(cx, arm.body, ctxt, "..", Some(expr.span), &mut app),
);
(msg, sugg)
};
span_lint_and_sugg(cx, lint, expr.span, msg, "try", sugg, app);
}
struct PatVisitor<'tcx> {
typeck: &'tcx TypeckResults<'tcx>,
has_enum: bool,
}
impl<'tcx> Visitor<'tcx> for PatVisitor<'tcx> {
type Result = ControlFlow<()>;
fn visit_pat(&mut self, pat: &'tcx Pat<'_>) -> Self::Result {
if matches!(pat.kind, PatKind::Binding(..)) {
ControlFlow::Break(())
} else {
self.has_enum |= self.typeck.pat_ty(pat).ty_adt_def().map_or(false, AdtDef::is_enum);
walk_pat(self, pat)
}
}
}
/// The context needed to manipulate a `PatState`.
struct PatCtxt<'tcx> {
tcx: TyCtxt<'tcx>,
param_env: ParamEnv<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
arena: DroplessArena,
}
/// State for tracking whether a match can become non-exhaustive by adding a variant to a contained
/// enum.
///
/// This treats certain std enums as if they will never be extended.
enum PatState<'a> {
/// Either a wild match or an uninteresting type. Uninteresting types include:
/// * builtin types (e.g. `i32` or `!`)
/// * A struct/tuple/array containing only uninteresting types.
/// * A std enum containing only uninteresting types.
Wild,
/// A std enum we know won't be extended. Tracks the states of each variant separately.
///
/// This is not used for `Option` since it uses the current pattern to track it's state.
StdEnum(&'a mut [PatState<'a>]),
/// Either the initial state for a pattern or a non-std enum. There is currently no need to
/// distinguish these cases.
///
/// For non-std enums there's no need to track the state of sub-patterns as the state of just
/// this pattern on it's own is enough for linting. Consider two cases:
/// * This enum has no wild match. This case alone is enough to determine we can lint.
/// * This enum has a wild match and therefore all sub-patterns also have a wild match.
///
/// In both cases the sub patterns are not needed to determine whether to lint.
Other,
}
impl<'a> PatState<'a> {
/// Adds a set of patterns as a product type to the current state. Returns whether or not the
/// current state is a wild match after the merge.
fn add_product_pat<'tcx>(
&mut self,
cx: &'a PatCtxt<'tcx>,
pats: impl IntoIterator<Item = &'tcx Pat<'tcx>>,
) -> bool {
// Ideally this would actually keep track of the state separately for each pattern. Doing so would
// require implementing something similar to exhaustiveness checking which is a significant increase
// in complexity.
//
// For now treat this as a wild match only if all the sub-patterns are wild
let is_wild = pats.into_iter().all(|p| {
let mut state = Self::Other;
state.add_pat(cx, p)
});
if is_wild {
*self = Self::Wild;
}
is_wild
}
/// Attempts to get the state for the enum variant, initializing the current state if necessary.
fn get_std_enum_variant<'tcx>(
&mut self,
cx: &'a PatCtxt<'tcx>,
adt: AdtDef<'tcx>,
path: &'tcx QPath<'_>,
hir_id: HirId,
) -> Option<(&mut Self, &'tcx VariantDef)> {
let states = match self {
Self::Wild => return None,
Self::Other => {
*self = Self::StdEnum(cx.arena.alloc_from_iter((0..adt.variants().len()).map(|_| Self::Other)));
let Self::StdEnum(x) = self else {
unreachable!();
};
x
},
Self::StdEnum(x) => x,
};
let i = match cx.typeck.qpath_res(path, hir_id) {
Res::Def(DefKind::Ctor(..), id) => adt.variant_index_with_ctor_id(id),
Res::Def(DefKind::Variant, id) => adt.variant_index_with_id(id),
_ => return None,
};
Some((&mut states[i.as_usize()], adt.variant(i)))
}
fn check_all_wild_enum(&mut self) -> bool {
if let Self::StdEnum(states) = self
&& states.iter().all(|s| matches!(s, Self::Wild))
{
*self = Self::Wild;
true
} else {
false
}
}
#[expect(clippy::similar_names)]
fn add_struct_pats<'tcx>(
&mut self,
cx: &'a PatCtxt<'tcx>,
pat: &'tcx Pat<'tcx>,
path: &'tcx QPath<'tcx>,
single_pat: Option<&'tcx Pat<'tcx>>,
pats: impl IntoIterator<Item = &'tcx Pat<'tcx>>,
) -> bool {
let ty::Adt(adt, _) = *cx.typeck.pat_ty(pat).kind() else {
// Should never happen
*self = Self::Wild;
return true;
};
if adt.is_struct() {
return if let Some(pat) = single_pat
&& adt.non_enum_variant().fields.len() == 1
{
self.add_pat(cx, pat)
} else {
self.add_product_pat(cx, pats)
};
}
match cx.tcx.get_diagnostic_name(adt.did()) {
Some(sym::Option) => {
if let Some(pat) = single_pat {
self.add_pat(cx, pat)
} else {
*self = Self::Wild;
true
}
},
Some(sym::Result | sym::Cow) => {
let Some((state, variant)) = self.get_std_enum_variant(cx, adt, path, pat.hir_id) else {
return matches!(self, Self::Wild);
};
let is_wild = if let Some(pat) = single_pat
&& variant.fields.len() == 1
{
state.add_pat(cx, pat)
} else {
state.add_product_pat(cx, pats)
};
is_wild && self.check_all_wild_enum()
},
_ => matches!(self, Self::Wild),
}
}
/// Adds the pattern into the current state. Returns whether or not the current state is a wild
/// match after the merge.
#[expect(clippy::similar_names)]
fn add_pat<'tcx>(&mut self, cx: &'a PatCtxt<'tcx>, pat: &'tcx Pat<'_>) -> bool {
match pat.kind {
PatKind::Path(_)
if match *cx.typeck.pat_ty(pat).peel_refs().kind() {
ty::Adt(adt, _) => adt.is_enum() || (adt.is_struct() && !adt.non_enum_variant().fields.is_empty()),
ty::Tuple(tys) => !tys.is_empty(),
ty::Array(_, len) => len.try_eval_target_usize(cx.tcx, cx.param_env) != Some(1),
ty::Slice(..) => true,
_ => false,
} =>
{
matches!(self, Self::Wild)
},
// Patterns for things which can only contain a single sub-pattern.
PatKind::Binding(_, _, _, Some(pat)) | PatKind::Ref(pat, _) | PatKind::Box(pat) | PatKind::Deref(pat) => {
self.add_pat(cx, pat)
},
PatKind::Tuple([sub_pat], pos)
if pos.as_opt_usize().is_none() || cx.typeck.pat_ty(pat).tuple_fields().len() == 1 =>
{
self.add_pat(cx, sub_pat)
},
PatKind::Slice([sub_pat], _, []) | PatKind::Slice([], _, [sub_pat])
if let ty::Array(_, len) = *cx.typeck.pat_ty(pat).kind()
&& len.try_eval_target_usize(cx.tcx, cx.param_env) == Some(1) =>
{
self.add_pat(cx, sub_pat)
},
PatKind::Or(pats) => pats.iter().any(|p| self.add_pat(cx, p)),
PatKind::Tuple(pats, _) => self.add_product_pat(cx, pats),
PatKind::Slice(head, _, tail) => self.add_product_pat(cx, head.iter().chain(tail)),
PatKind::TupleStruct(ref path, pats, _) => self.add_struct_pats(
cx,
pat,
path,
if let [pat] = pats { Some(pat) } else { None },
pats.iter(),
),
PatKind::Struct(ref path, pats, _) => self.add_struct_pats(
cx,
pat,
path,
if let [pat] = pats { Some(pat.pat) } else { None },
pats.iter().map(|p| p.pat),
),
PatKind::Wild
| PatKind::Binding(_, _, _, None)
| PatKind::Lit(_)
| PatKind::Range(..)
| PatKind::Path(_)
| PatKind::Never
| PatKind::Err(_) => {
*self = PatState::Wild;
true
},
}
}
}