use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then}; use clippy_utils::source::snippet_opt; use clippy_utils::{is_doc_hidden, meets_msrv, msrvs}; use rustc_ast::ast::{self, VisibilityKind}; use rustc_data_structures::fx::FxHashSet; use rustc_errors::Applicability; use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; use rustc_hir::{self as hir, Expr, ExprKind, QPath}; use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext}; use rustc_middle::ty::DefIdTree; use rustc_semver::RustcVersion; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::def_id::{DefId, LocalDefId}; use rustc_span::{sym, Span}; 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 /// ```rust /// struct S { /// pub a: i32, /// pub b: i32, /// _c: (), /// } /// /// enum E { /// A, /// B, /// #[doc(hidden)] /// _C, /// } /// /// struct T(pub i32, pub i32, ()); /// ``` /// Use instead: /// ```rust /// #[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]" } #[expect(clippy::module_name_repetitions)] pub struct ManualNonExhaustiveStruct { msrv: Option, } impl ManualNonExhaustiveStruct { #[must_use] pub fn new(msrv: Option) -> Self { Self { msrv } } } impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]); #[expect(clippy::module_name_repetitions)] pub struct ManualNonExhaustiveEnum { msrv: Option, constructed_enum_variants: FxHashSet<(DefId, DefId)>, potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>, } impl ManualNonExhaustiveEnum { #[must_use] pub fn new(msrv: Option) -> Self { Self { msrv, constructed_enum_variants: FxHashSet::default(), potential_enums: Vec::new(), } } } impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]); impl EarlyLintPass for ManualNonExhaustiveStruct { fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) { return; } if let ast::ItemKind::Struct(variant_data, _) = &item.kind { let (fields, delimiter) = match variant_data { ast::VariantData::Struct(fields, _) => (&**fields, '{'), ast::VariantData::Tuple(fields, _) => (&**fields, '('), ast::VariantData::Unit(_) => return, }; if fields.len() <= 1 { return; } let mut iter = fields.iter().filter_map(|f| match f.vis.kind { VisibilityKind::Public => None, VisibilityKind::Inherited => Some(Ok(f)), VisibilityKind::Restricted { .. } => Some(Err(())), }); if let Some(Ok(field)) = iter.next() && iter.next().is_none() && field.ty.kind.is_unit() && field.ident.map_or(true, |name| name.as_str().starts_with('_')) { span_lint_and_then( cx, MANUAL_NON_EXHAUSTIVE, item.span, "this seems like a manual implementation of the non-exhaustive pattern", |diag| { if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive)) && let header_span = cx.sess().source_map().span_until_char(item.span, delimiter) && let Some(snippet) = snippet_opt(cx, header_span) { diag.span_suggestion( header_span, "add the attribute", format!("#[non_exhaustive] {snippet}"), Applicability::Unspecified, ); } diag.span_help(field.span, "remove this field"); } ); } } } extract_msrv_attr!(EarlyContext); } impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum { fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) { if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) { return; } if let hir::ItemKind::Enum(def, _) = &item.kind && def.variants.len() > 1 { let mut iter = def.variants.iter().filter_map(|v| { let id = cx.tcx.hir().local_def_id(v.hir_id); (matches!(v.data, hir::VariantData::Unit(..)) && v.ident.as_str().starts_with('_') && is_doc_hidden(cx.tcx.hir().attrs(v.hir_id))) .then_some((id, v.span)) }); if let Some((id, span)) = iter.next() && iter.next().is_none() { self.potential_enums.push((item.owner_id.def_id, id, item.span, span)); } } } fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) { if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind && let [.., name] = p.segments && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = p.res && name.ident.as_str().starts_with('_') { let variant_id = cx.tcx.parent(id); let enum_id = cx.tcx.parent(variant_id); self.constructed_enum_variants.insert((enum_id, variant_id)); } } fn check_crate_post(&mut self, cx: &LateContext<'tcx>) { for &(enum_id, _, enum_span, variant_span) in self.potential_enums.iter().filter(|&&(enum_id, variant_id, _, _)| { !self .constructed_enum_variants .contains(&(enum_id.to_def_id(), variant_id.to_def_id())) }) { let hir_id = cx.tcx.hir().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| { if !cx.tcx.adt_def(enum_id).is_variant_list_non_exhaustive() && let header_span = cx.sess().source_map().span_until_char(enum_span, '{') && let Some(snippet) = snippet_opt(cx, header_span) { diag.span_suggestion( header_span, "add the attribute", format!("#[non_exhaustive] {snippet}"), Applicability::Unspecified, ); } diag.span_help(variant_span, "remove this variant"); }, ); } } extract_msrv_attr!(LateContext); }