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