]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/manual_non_exhaustive.rs
add support for minimum supported rust version.
[rust.git] / clippy_lints / src / manual_non_exhaustive.rs
1 use crate::utils::{get_inner_attr, 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};
10
11 const MANUAL_NON_EXHAUSTIVE_MSRV: Version = Version {
12     major: 1,
13     minor: 40,
14     patch: 0,
15     pre: Vec::new(),
16     build: Vec::new(),
17 };
18
19 declare_clippy_lint! {
20     /// **What it does:** Checks for manual implementations of the non-exhaustive pattern.
21     ///
22     /// **Why is this bad?** Using the #[non_exhaustive] attribute expresses better the intent
23     /// and allows possible optimizations when applied to enums.
24     ///
25     /// **Known problems:** None.
26     ///
27     /// **Example:**
28     ///
29     /// ```rust
30     /// struct S {
31     ///     pub a: i32,
32     ///     pub b: i32,
33     ///     _c: (),
34     /// }
35     ///
36     /// enum E {
37     ///     A,
38     ///     B,
39     ///     #[doc(hidden)]
40     ///     _C,
41     /// }
42     ///
43     /// struct T(pub i32, pub i32, ());
44     /// ```
45     /// Use instead:
46     /// ```rust
47     /// #[non_exhaustive]
48     /// struct S {
49     ///     pub a: i32,
50     ///     pub b: i32,
51     /// }
52     ///
53     /// #[non_exhaustive]
54     /// enum E {
55     ///     A,
56     ///     B,
57     /// }
58     ///
59     /// #[non_exhaustive]
60     /// struct T(pub i32, pub i32);
61     /// ```
62     pub MANUAL_NON_EXHAUSTIVE,
63     style,
64     "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
65 }
66
67 #[derive(Clone)]
68 pub struct ManualNonExhaustive {
69     msrv: Option<VersionReq>,
70 }
71
72 impl ManualNonExhaustive {
73     #[must_use]
74     pub fn new(msrv: Option<VersionReq>) -> Self {
75         Self { msrv }
76     }
77 }
78
79 impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
80
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) {
84             return;
85         }
86
87         match &item.kind {
88             ItemKind::Enum(def, _) => {
89                 check_manual_non_exhaustive_enum(cx, item, &def.variants);
90             },
91             ItemKind::Struct(variant_data, _) => {
92                 if let VariantData::Unit(..) = variant_data {
93                     return;
94                 }
95
96                 check_manual_non_exhaustive_struct(cx, item, variant_data);
97             },
98             _ => {},
99         }
100     }
101
102     extract_msrv_attr!(EarlyContext);
103 }
104
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))
110     }
111
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),
116                 None => false,
117             }
118     }
119
120     if_chain! {
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;
124         then {
125             span_lint_and_then(
126                 cx,
127                 MANUAL_NON_EXHAUSTIVE,
128                 item.span,
129                 "this seems like a manual implementation of the non-exhaustive pattern",
130                 |diag| {
131                     if_chain! {
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);
135                         then {
136                             diag.span_suggestion(
137                                 header_span,
138                                 "add the attribute",
139                                 format!("#[non_exhaustive] {}", snippet),
140                                 Applicability::Unspecified,
141                             );
142                         }
143                     }
144                     diag.span_help(marker.span, "remove this variant");
145                 });
146         }
147     }
148 }
149
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)
153     }
154
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('_'))
157     }
158
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"),
164         };
165
166         cx.sess.source_map().span_until_char(item.span, delimiter)
167     }
168
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();
172
173     if_chain! {
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));
176         then {
177             span_lint_and_then(
178                 cx,
179                 MANUAL_NON_EXHAUSTIVE,
180                 item.span,
181                 "this seems like a manual implementation of the non-exhaustive pattern",
182                 |diag| {
183                     if_chain! {
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);
187                         then {
188                             diag.span_suggestion(
189                                 header_span,
190                                 "add the attribute",
191                                 format!("#[non_exhaustive] {}", snippet),
192                                 Applicability::Unspecified,
193                             );
194                         }
195                     }
196                     diag.span_help(marker.span, "remove this field");
197                 });
198         }
199     }
200 }