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