1 use crate::utils::{meets_msrv, snippet_opt, span_lint_and_then};
2 use if_chain::if_chain;
3 use rustc_ast::ast::{Attribute, Item, ItemKind, StructField, Variant, VariantData, VisibilityKind};
4 use rustc_attr as attr;
5 use rustc_errors::Applicability;
6 use rustc_lint::{EarlyContext, EarlyLintPass};
7 use rustc_session::{declare_tool_lint, impl_lint_pass};
8 use rustc_span::{sym, Span};
9 use semver::{Version, VersionReq};
11 const MANUAL_NON_EXHAUSTIVE_MSRV: Version = Version {
19 declare_clippy_lint! {
20 /// **What it does:** Checks for manual implementations of the non-exhaustive pattern.
22 /// **Why is this bad?** Using the #[non_exhaustive] attribute expresses better the intent
23 /// and allows possible optimizations when applied to enums.
25 /// **Known problems:** None.
43 /// struct T(pub i32, pub i32, ());
60 /// struct T(pub i32, pub i32);
62 pub MANUAL_NON_EXHAUSTIVE,
64 "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
68 pub struct ManualNonExhaustive {
69 msrv: Option<VersionReq>,
72 impl ManualNonExhaustive {
74 pub fn new(msrv: Option<VersionReq>) -> Self {
79 impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
81 impl EarlyLintPass for ManualNonExhaustive {
82 fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
83 if !meets_msrv(self.msrv.as_ref(), &MANUAL_NON_EXHAUSTIVE_MSRV) {
88 ItemKind::Enum(def, _) => {
89 check_manual_non_exhaustive_enum(cx, item, &def.variants);
91 ItemKind::Struct(variant_data, _) => {
92 if let VariantData::Unit(..) = variant_data {
96 check_manual_non_exhaustive_struct(cx, item, variant_data);
102 extract_msrv_attr!(EarlyContext);
105 fn check_manual_non_exhaustive_enum(cx: &EarlyContext<'_>, item: &Item, variants: &[Variant]) {
106 fn is_non_exhaustive_marker(variant: &Variant) -> bool {
107 matches!(variant.data, VariantData::Unit(_))
108 && variant.ident.as_str().starts_with('_')
109 && variant.attrs.iter().any(|a| is_doc_hidden(a))
112 fn is_doc_hidden(attr: &Attribute) -> bool {
113 attr.has_name(sym::doc)
114 && match attr.meta_item_list() {
115 Some(l) => attr::list_contains_name(&l, sym::hidden),
121 let mut markers = variants.iter().filter(|v| is_non_exhaustive_marker(v));
122 if let Some(marker) = markers.next();
123 if markers.count() == 0 && variants.len() > 1;
127 MANUAL_NON_EXHAUSTIVE,
129 "this seems like a manual implementation of the non-exhaustive pattern",
132 if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive));
133 let header_span = cx.sess.source_map().span_until_char(item.span, '{');
134 if let Some(snippet) = snippet_opt(cx, header_span);
136 diag.span_suggestion(
139 format!("#[non_exhaustive] {}", snippet),
140 Applicability::Unspecified,
144 diag.span_help(marker.span, "remove this variant");
150 fn check_manual_non_exhaustive_struct(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) {
151 fn is_private(field: &StructField) -> bool {
152 matches!(field.vis.kind, VisibilityKind::Inherited)
155 fn is_non_exhaustive_marker(field: &StructField) -> bool {
156 is_private(field) && field.ty.kind.is_unit() && field.ident.map_or(true, |n| n.as_str().starts_with('_'))
159 fn find_header_span(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) -> Span {
160 let delimiter = match data {
161 VariantData::Struct(..) => '{',
162 VariantData::Tuple(..) => '(',
163 VariantData::Unit(_) => unreachable!("`VariantData::Unit` is already handled above"),
166 cx.sess.source_map().span_until_char(item.span, delimiter)
169 let fields = data.fields();
170 let private_fields = fields.iter().filter(|f| is_private(f)).count();
171 let public_fields = fields.iter().filter(|f| f.vis.kind.is_pub()).count();
174 if private_fields == 1 && public_fields >= 1 && public_fields == fields.len() - 1;
175 if let Some(marker) = fields.iter().find(|f| is_non_exhaustive_marker(f));
179 MANUAL_NON_EXHAUSTIVE,
181 "this seems like a manual implementation of the non-exhaustive pattern",
184 if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive));
185 let header_span = find_header_span(cx, item, data);
186 if let Some(snippet) = snippet_opt(cx, header_span);
188 diag.span_suggestion(
191 format!("#[non_exhaustive] {}", snippet),
192 Applicability::Unspecified,
196 diag.span_help(marker.span, "remove this field");