blob: fa44a56af182dcb19e6a1b0f5ecc63bee6aea0ee [file] [log] [blame] [edit]
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::res::MaybeDef;
use clippy_utils::source::SpanRangeExt;
use clippy_utils::{is_refutable, peel_hir_pat_refs, recurse_or_patterns};
use rustc_errors::Applicability;
use rustc_hir::def::{CtorKind, DefKind, Res};
use rustc_hir::{Arm, Expr, PatExpr, PatExprKind, PatKind, PathSegment, QPath, Ty, TyKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, VariantDef};
use rustc_span::sym;
use super::{MATCH_WILDCARD_FOR_SINGLE_VARIANTS, WILDCARD_ENUM_MATCH_ARM};
#[expect(clippy::too_many_lines)]
pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) {
let ty = cx.typeck_results().expr_ty(ex).peel_refs();
let adt_def = match ty.kind() {
ty::Adt(adt_def, _)
if adt_def.is_enum() && !(ty.is_diag_item(cx, sym::Option) || ty.is_diag_item(cx, sym::Result)) =>
{
adt_def
},
_ => return,
};
// First pass - check for violation, but don't do much book-keeping because this is hopefully
// the uncommon case, and the book-keeping is slightly expensive.
let mut wildcard_span = None;
let mut wildcard_ident = None;
let mut has_non_wild = false;
for arm in arms {
match peel_hir_pat_refs(arm.pat).0.kind {
PatKind::Wild if arm.guard.is_none() => wildcard_span = Some(arm.pat.span),
PatKind::Binding(_, _, ident, None) => {
wildcard_span = Some(arm.pat.span);
wildcard_ident = Some(ident);
},
_ => has_non_wild = true,
}
}
let wildcard_span = match wildcard_span {
Some(x) if has_non_wild => x,
_ => return,
};
// Accumulate the variants which should be put in place of the wildcard because they're not
// already covered.
let is_external = adt_def.did().as_local().is_none();
let has_external_hidden = is_external && adt_def.variants().iter().any(|x| is_hidden(cx, x));
let mut missing_variants: Vec<_> = adt_def
.variants()
.iter()
.filter(|x| !(is_external && is_hidden(cx, x)))
.collect();
let mut path_prefix = CommonPrefixSearcher::None;
for arm in arms {
// Guards mean that this case probably isn't exhaustively covered. Technically
// this is incorrect, as we should really check whether each variant is exhaustively
// covered by the set of guards that cover it, but that's really hard to do.
recurse_or_patterns(arm.pat, |pat| {
let path = match &peel_hir_pat_refs(pat).0.kind {
PatKind::Expr(PatExpr {
hir_id,
kind: PatExprKind::Path(path),
..
}) => {
// FIXME(clippy): don't you want to use the hir id of the peeled pat?
let id = match cx.qpath_res(path, *hir_id) {
Res::Def(
DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst,
_,
) => return,
Res::Def(_, id) => id,
_ => return,
};
if arm.guard.is_none() {
missing_variants.retain(|e| e.ctor_def_id() != Some(id));
}
path
},
PatKind::TupleStruct(path, patterns, ..) => {
if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id()
&& arm.guard.is_none()
&& patterns.iter().all(|p| !is_refutable(cx, p))
{
missing_variants.retain(|e| e.ctor_def_id() != Some(id));
}
path
},
PatKind::Struct(path, patterns, ..) => {
if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id()
&& arm.guard.is_none()
&& patterns.iter().all(|p| !is_refutable(cx, p.pat))
{
missing_variants.retain(|e| e.def_id != id);
}
path
},
_ => return,
};
match path {
QPath::Resolved(_, path) => path_prefix.with_path(path.segments),
QPath::TypeRelative(
Ty {
kind: TyKind::Path(QPath::Resolved(_, path)),
..
},
_,
) => path_prefix.with_prefix(path.segments),
QPath::TypeRelative(..) => (),
}
});
}
let format_suggestion = |variant: &VariantDef| {
format!(
"{}{}{}{}",
wildcard_ident.map_or(String::new(), |ident| {
ident
.span
.get_source_text(cx)
.map_or_else(|| format!("{} @ ", ident.name), |s| format!("{s} @ "))
}),
if let CommonPrefixSearcher::Path(path_prefix) = path_prefix {
let mut s = String::new();
for seg in path_prefix {
s.push_str(seg.ident.as_str());
s.push_str("::");
}
s
} else {
let mut s = cx.tcx.def_path_str(adt_def.did());
s.push_str("::");
s
},
variant.name,
match variant.ctor_kind() {
Some(CtorKind::Fn) if variant.fields.len() == 1 => "(_)",
Some(CtorKind::Fn) => "(..)",
Some(CtorKind::Const) => "",
None => " { .. }",
}
)
};
match missing_variants.as_slice() {
[] => (),
[x] if !adt_def.is_variant_list_non_exhaustive() && !has_external_hidden => span_lint_and_sugg(
cx,
MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
wildcard_span,
"wildcard matches only a single variant and will also match any future added variants",
"try",
format_suggestion(x),
Applicability::MaybeIncorrect,
),
variants => {
let (message, add_wildcard) = if adt_def.is_variant_list_non_exhaustive() || has_external_hidden {
(
"wildcard matches known variants and will also match future added variants",
true,
)
} else {
("wildcard match will also match any future added variants", false)
};
span_lint_and_then(cx, WILDCARD_ENUM_MATCH_ARM, wildcard_span, message, |diag| {
let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect();
if add_wildcard {
suggestions.push("_".into());
}
diag.span_suggestion(
wildcard_span,
"try",
suggestions.join(" | "),
Applicability::MaybeIncorrect,
);
});
},
}
}
enum CommonPrefixSearcher<'a> {
None,
Path(&'a [PathSegment<'a>]),
Mixed,
}
impl<'a> CommonPrefixSearcher<'a> {
fn with_path(&mut self, path: &'a [PathSegment<'a>]) {
if let [path @ .., _] = path {
self.with_prefix(path);
}
}
fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) {
match self {
Self::None => *self = Self::Path(path),
Self::Path(self_path)
if path
.iter()
.map(|p| p.ident.name)
.eq(self_path.iter().map(|p| p.ident.name)) => {},
Self::Path(_) => *self = Self::Mixed,
Self::Mixed => (),
}
}
}
fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool {
cx.tcx.is_doc_hidden(variant_def.def_id) || cx.tcx.has_attr(variant_def.def_id, sym::unstable)
}