blob: 0d783fde33131ef4d16b4855cdd14619cb1d35a8 [file] [log] [blame] [edit]
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then};
use clippy_utils::is_doc_hidden;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::source::snippet_indent;
use itertools::Itertools;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::attrs::AttributeKind;
use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
use rustc_hir::{Expr, ExprKind, Item, ItemKind, QPath, TyKind, VariantData, find_attr};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::Span;
use rustc_span::def_id::LocalDefId;
declare_clippy_lint! {
/// ### What it does
/// Checks for manual implementations of the non-exhaustive pattern.
///
/// ### Why is this bad?
/// Using the #[non_exhaustive] attribute expresses better the intent
/// and allows possible optimizations when applied to enums.
///
/// ### Example
/// ```no_run
/// struct S {
/// pub a: i32,
/// pub b: i32,
/// _c: (),
/// }
///
/// enum E {
/// A,
/// B,
/// #[doc(hidden)]
/// _C,
/// }
///
/// struct T(pub i32, pub i32, ());
/// ```
/// Use instead:
/// ```no_run
/// #[non_exhaustive]
/// struct S {
/// pub a: i32,
/// pub b: i32,
/// }
///
/// #[non_exhaustive]
/// enum E {
/// A,
/// B,
/// }
///
/// #[non_exhaustive]
/// struct T(pub i32, pub i32);
/// ```
#[clippy::version = "1.45.0"]
pub MANUAL_NON_EXHAUSTIVE,
style,
"manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
}
pub struct ManualNonExhaustive {
msrv: Msrv,
constructed_enum_variants: FxHashSet<LocalDefId>,
potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>,
}
impl ManualNonExhaustive {
pub fn new(conf: &'static Conf) -> Self {
Self {
msrv: conf.msrv,
constructed_enum_variants: FxHashSet::default(),
potential_enums: Vec::new(),
}
}
}
impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustive {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
if !cx.effective_visibilities.is_exported(item.owner_id.def_id) || !self.msrv.meets(cx, msrvs::NON_EXHAUSTIVE) {
return;
}
match item.kind {
ItemKind::Enum(_, _, def) if def.variants.len() > 1 => {
let iter = def.variants.iter().filter_map(|v| {
(matches!(v.data, VariantData::Unit(_, _)) && is_doc_hidden(cx.tcx.hir_attrs(v.hir_id)))
.then_some((v.def_id, v.span))
});
if let Ok((id, span)) = iter.exactly_one()
&& !find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::NonExhaustive(..))
{
self.potential_enums.push((item.owner_id.def_id, id, item.span, span));
}
},
ItemKind::Struct(_, _, variant_data) => {
let fields = variant_data.fields();
let private_fields = fields
.iter()
.filter(|field| !cx.effective_visibilities.is_exported(field.def_id));
if fields.len() > 1
&& let Ok(field) = private_fields.exactly_one()
&& let TyKind::Tup([]) = field.ty.kind
{
span_lint_and_then(
cx,
MANUAL_NON_EXHAUSTIVE,
item.span,
"this seems like a manual implementation of the non-exhaustive pattern",
|diag| {
if let Some(non_exhaustive_span) =
find_attr!(cx.tcx.hir_attrs(item.hir_id()), AttributeKind::NonExhaustive(span) => *span)
{
diag.span_note(non_exhaustive_span, "the struct is already non-exhaustive");
} else {
let indent = snippet_indent(cx, item.span).unwrap_or_default();
diag.span_suggestion_verbose(
item.span.shrink_to_lo(),
"use the `#[non_exhaustive]` attribute instead",
format!("#[non_exhaustive]\n{indent}"),
Applicability::MaybeIncorrect,
);
}
diag.span_help(field.span, "remove this field");
},
);
}
},
_ => {},
}
}
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind
&& let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), ctor_id) = p.res
&& let Some(local_ctor) = ctor_id.as_local()
{
let variant_id = cx.tcx.local_parent(local_ctor);
self.constructed_enum_variants.insert(variant_id);
}
}
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
for &(enum_id, _, enum_span, variant_span) in self
.potential_enums
.iter()
.filter(|(_, variant_id, _, _)| !self.constructed_enum_variants.contains(variant_id))
{
let hir_id = cx.tcx.local_def_id_to_hir_id(enum_id);
span_lint_hir_and_then(
cx,
MANUAL_NON_EXHAUSTIVE,
hir_id,
enum_span,
"this seems like a manual implementation of the non-exhaustive pattern",
|diag| {
let indent = snippet_indent(cx, enum_span).unwrap_or_default();
diag.span_suggestion_verbose(
enum_span.shrink_to_lo(),
"use the `#[non_exhaustive]` attribute instead",
format!("#[non_exhaustive]\n{indent}"),
Applicability::MaybeIncorrect,
);
diag.span_help(variant_span, "remove this variant");
},
);
}
}
}