]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/manual_non_exhaustive.rs
335ea001ee476e144a7aa1f5206bb3368f41d796
[rust.git] / clippy_lints / src / manual_non_exhaustive.rs
1 use clippy_utils::attrs::is_doc_hidden;
2 use clippy_utils::diagnostics::span_lint_and_then;
3 use clippy_utils::source::snippet_opt;
4 use clippy_utils::{meets_msrv, msrvs};
5 use if_chain::if_chain;
6 use rustc_ast::ast::{FieldDef, Item, ItemKind, Variant, VariantData, VisibilityKind};
7 use rustc_errors::Applicability;
8 use rustc_lint::{EarlyContext, EarlyLintPass};
9 use rustc_semver::RustcVersion;
10 use rustc_session::{declare_tool_lint, impl_lint_pass};
11 use rustc_span::{sym, Span};
12
13 declare_clippy_lint! {
14     /// ### What it does
15     /// Checks for manual implementations of the non-exhaustive pattern.
16     ///
17     /// ### Why is this bad?
18     /// Using the #[non_exhaustive] attribute expresses better the intent
19     /// and allows possible optimizations when applied to enums.
20     ///
21     /// ### Example
22     /// ```rust
23     /// struct S {
24     ///     pub a: i32,
25     ///     pub b: i32,
26     ///     _c: (),
27     /// }
28     ///
29     /// enum E {
30     ///     A,
31     ///     B,
32     ///     #[doc(hidden)]
33     ///     _C,
34     /// }
35     ///
36     /// struct T(pub i32, pub i32, ());
37     /// ```
38     /// Use instead:
39     /// ```rust
40     /// #[non_exhaustive]
41     /// struct S {
42     ///     pub a: i32,
43     ///     pub b: i32,
44     /// }
45     ///
46     /// #[non_exhaustive]
47     /// enum E {
48     ///     A,
49     ///     B,
50     /// }
51     ///
52     /// #[non_exhaustive]
53     /// struct T(pub i32, pub i32);
54     /// ```
55     pub MANUAL_NON_EXHAUSTIVE,
56     style,
57     "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
58 }
59
60 #[derive(Clone)]
61 pub struct ManualNonExhaustive {
62     msrv: Option<RustcVersion>,
63 }
64
65 impl ManualNonExhaustive {
66     #[must_use]
67     pub fn new(msrv: Option<RustcVersion>) -> Self {
68         Self { msrv }
69     }
70 }
71
72 impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
73
74 impl EarlyLintPass for ManualNonExhaustive {
75     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
76         if !meets_msrv(self.msrv.as_ref(), &msrvs::NON_EXHAUSTIVE) {
77             return;
78         }
79
80         match &item.kind {
81             ItemKind::Enum(def, _) => {
82                 check_manual_non_exhaustive_enum(cx, item, &def.variants);
83             },
84             ItemKind::Struct(variant_data, _) => {
85                 if let VariantData::Unit(..) = variant_data {
86                     return;
87                 }
88
89                 check_manual_non_exhaustive_struct(cx, item, variant_data);
90             },
91             _ => {},
92         }
93     }
94
95     extract_msrv_attr!(EarlyContext);
96 }
97
98 fn check_manual_non_exhaustive_enum(cx: &EarlyContext<'_>, item: &Item, variants: &[Variant]) {
99     fn is_non_exhaustive_marker(variant: &Variant) -> bool {
100         matches!(variant.data, VariantData::Unit(_))
101             && variant.ident.as_str().starts_with('_')
102             && is_doc_hidden(&variant.attrs)
103     }
104
105     let mut markers = variants.iter().filter(|v| is_non_exhaustive_marker(v));
106     if_chain! {
107         if let Some(marker) = markers.next();
108         if markers.count() == 0 && variants.len() > 1;
109         then {
110             span_lint_and_then(
111                 cx,
112                 MANUAL_NON_EXHAUSTIVE,
113                 item.span,
114                 "this seems like a manual implementation of the non-exhaustive pattern",
115                 |diag| {
116                     if_chain! {
117                         if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive));
118                         let header_span = cx.sess.source_map().span_until_char(item.span, '{');
119                         if let Some(snippet) = snippet_opt(cx, header_span);
120                         then {
121                             diag.span_suggestion(
122                                 header_span,
123                                 "add the attribute",
124                                 format!("#[non_exhaustive] {}", snippet),
125                                 Applicability::Unspecified,
126                             );
127                         }
128                     }
129                     diag.span_help(marker.span, "remove this variant");
130                 });
131         }
132     }
133 }
134
135 fn check_manual_non_exhaustive_struct(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) {
136     fn is_private(field: &FieldDef) -> bool {
137         matches!(field.vis.kind, VisibilityKind::Inherited)
138     }
139
140     fn is_non_exhaustive_marker(field: &FieldDef) -> bool {
141         is_private(field) && field.ty.kind.is_unit() && field.ident.map_or(true, |n| n.as_str().starts_with('_'))
142     }
143
144     fn find_header_span(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) -> Span {
145         let delimiter = match data {
146             VariantData::Struct(..) => '{',
147             VariantData::Tuple(..) => '(',
148             VariantData::Unit(_) => unreachable!("`VariantData::Unit` is already handled above"),
149         };
150
151         cx.sess.source_map().span_until_char(item.span, delimiter)
152     }
153
154     let fields = data.fields();
155     let private_fields = fields.iter().filter(|f| is_private(f)).count();
156     let public_fields = fields.iter().filter(|f| f.vis.kind.is_pub()).count();
157
158     if_chain! {
159         if private_fields == 1 && public_fields >= 1 && public_fields == fields.len() - 1;
160         if let Some(marker) = fields.iter().find(|f| is_non_exhaustive_marker(f));
161         then {
162             span_lint_and_then(
163                 cx,
164                 MANUAL_NON_EXHAUSTIVE,
165                 item.span,
166                 "this seems like a manual implementation of the non-exhaustive pattern",
167                 |diag| {
168                     if_chain! {
169                         if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive));
170                         let header_span = find_header_span(cx, item, data);
171                         if let Some(snippet) = snippet_opt(cx, header_span);
172                         then {
173                             diag.span_suggestion(
174                                 header_span,
175                                 "add the attribute",
176                                 format!("#[non_exhaustive] {}", snippet),
177                                 Applicability::Unspecified,
178                             );
179                         }
180                     }
181                     diag.span_help(marker.span, "remove this field");
182                 });
183         }
184     }
185 }