blob: 8642c7e349b1abab047192d88b54bfc23815f439 [file] [log] [blame]
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::{
SpanRangeExt, expr_block, snippet, snippet_block_with_context, snippet_with_applicability, snippet_with_context,
};
use clippy_utils::ty::{implements_trait, peel_and_count_ty_refs};
use clippy_utils::{is_lint_allowed, is_unit_expr, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs};
use core::ops::ControlFlow;
use rustc_arena::DroplessArena;
use rustc_errors::{Applicability, Diag};
use rustc_hir::def::{DefKind, Res};
use rustc_hir::intravisit::{Visitor, walk_pat};
use rustc_hir::{Arm, Expr, ExprKind, HirId, Node, Pat, PatExpr, PatExprKind, PatKind, QPath, StmtKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, AdtDef, TyCtxt, TypeckResults, VariantDef};
use rustc_span::{Span, sym};
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 {
span.check_source_text(cx, |text| text.as_bytes().windows(2).any(|w| w == b"//" || w == b"/*"))
}
pub(crate) fn check<'tcx>(
cx: &LateContext<'tcx>,
ex: &'tcx Expr<'_>,
arms: &'tcx [Arm<'_>],
expr: &'tcx Expr<'_>,
contains_comments: bool,
) {
if let [arm1, arm2] = arms
&& !arms.iter().any(|arm| arm.guard.is_some() || arm.pat.span.from_expansion())
&& !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,
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, contains_comments);
}
}
}
fn report_single_pattern(
cx: &LateContext<'_>,
ex: &Expr<'_>,
arm: &Arm<'_>,
expr: &Expr<'_>,
els: Option<&Expr<'_>>,
contains_comments: bool,
) {
let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH };
let ctxt = expr.span.ctxt();
let note = |diag: &mut Diag<'_, ()>| {
if contains_comments {
diag.note("you might want to preserve the comments from inside the `match`");
}
};
let mut app = if contains_comments {
Applicability::MaybeIncorrect
} else {
Applicability::MachineApplicable
};
let els_str = els.map_or(String::new(), |els| {
format!(" else {}", expr_block(cx, els, ctxt, "..", Some(expr.span), &mut app))
});
if ex.span.eq_ctxt(expr.span) && snippet(cx, ex.span, "..") == snippet(cx, arm.pat.span, "..") {
let msg = "this pattern is irrefutable, `match` is useless";
let (sugg, help) = if is_unit_expr(arm.body) {
(String::new(), "`match` expression can be removed")
} else {
let mut sugg = snippet_block_with_context(cx, arm.body.span, ctxt, "..", Some(expr.span), &mut app).0;
if let Node::Stmt(stmt) = cx.tcx.parent_hir_node(expr.hir_id)
&& let StmtKind::Expr(_) = stmt.kind
&& match arm.body.kind {
ExprKind::Block(block, _) => block.span.from_expansion(),
_ => true,
}
{
sugg.push(';');
}
(sugg, "try")
};
span_lint_and_then(cx, lint, expr.span, msg, |diag| {
diag.span_suggestion(expr.span, help, sugg, app);
note(diag);
});
return;
}
let (pat, pat_ref_count) = peel_hir_pat_refs(arm.pat);
let (msg, sugg) = if let PatKind::Expr(_) = pat.kind
&& let (ty, ty_ref_count, _) = peel_and_count_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::Expr(PatExpr {
kind: PatExprKind::Lit { lit, negated: false },
..
}) if lit.node.is_str() || lit.node.is_bytestr() => pat_ref_count + 1,
_ => pat_ref_count,
};
// References are implicitly removed when `deref_patterns` are used.
// They are implicitly added when match ergonomics are used.
let (ex, ref_or_deref_adjust) = if ty_ref_count > pat_ref_count {
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);
(ex, String::from(if ref_count_diff == removed { "" } else { "&" }))
} else {
(ex, "*".repeat(pat_ref_count - ty_ref_count))
};
let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`";
let sugg = format!(
"if {} == {}{} {}{els_str}",
snippet_with_context(cx, ex.span, ctxt, "..", &mut app).0,
// PartialEq for different reference counts may not exist.
ref_or_deref_adjust,
snippet_with_applicability(cx, arm.pat.span, "..", &mut app),
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_with_applicability(cx, arm.pat.span, "..", &mut app),
snippet_with_context(cx, ex.span, ctxt, "..", &mut app).0,
expr_block(cx, arm.body, ctxt, "..", Some(expr.span), &mut app),
);
(msg, sugg)
};
span_lint_and_then(cx, lint, expr.span, msg, |diag| {
diag.span_suggestion(expr.span, "try", sugg, app);
note(diag);
});
}
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().is_some_and(AdtDef::is_enum);
walk_pat(self, pat)
}
}
}
/// The context needed to manipulate a `PatState`.
struct PatCtxt<'tcx> {
tcx: TyCtxt<'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 its state.
StdEnum(&'a mut [Self]),
/// 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 its 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(std::iter::repeat_with(|| Self::Other).take(adt.variants().len())),
);
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::Expr(PatExpr {
kind: PatExprKind::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_to_target_usize(cx.tcx) != Some(1),
ty::Slice(..) => true,
_ => false,
} =>
{
matches!(self, Self::Wild)
},
PatKind::Guard(..) => {
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)
// `pat` looks like `(sub_pat)`, without a `..` -- has only one sub-pattern
if pos.as_opt_usize().is_none()
// `pat` looks like `(sub_pat, ..)` or `(.., sub_pat)`, but its type is a unary tuple,
// so it still only has one sub-pattern
|| 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_to_target_usize(cx.tcx) == 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::Missing => unreachable!(),
PatKind::Wild
| PatKind::Binding(_, _, _, None)
| PatKind::Expr(_)
| PatKind::Range(..)
| PatKind::Never
| PatKind::Err(_) => {
*self = PatState::Wild;
true
},
}
}
}