]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/manual_non_exhaustive.rs
Auto merge of #8799 - Alexendoo:lintcheck-common, r=giraffate
[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::{is_lint_allowed, meets_msrv, msrvs};
5 use rustc_ast::ast::{self, VisibilityKind};
6 use rustc_data_structures::fx::FxHashSet;
7 use rustc_errors::Applicability;
8 use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
9 use rustc_hir::{self as hir, Expr, ExprKind, QPath};
10 use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
11 use rustc_middle::ty::DefIdTree;
12 use rustc_semver::RustcVersion;
13 use rustc_session::{declare_tool_lint, impl_lint_pass};
14 use rustc_span::def_id::{DefId, LocalDefId};
15 use rustc_span::{sym, Span};
16
17 declare_clippy_lint! {
18     /// ### What it does
19     /// Checks for manual implementations of the non-exhaustive pattern.
20     ///
21     /// ### Why is this bad?
22     /// Using the #[non_exhaustive] attribute expresses better the intent
23     /// and allows possible optimizations when applied to enums.
24     ///
25     /// ### Example
26     /// ```rust
27     /// struct S {
28     ///     pub a: i32,
29     ///     pub b: i32,
30     ///     _c: (),
31     /// }
32     ///
33     /// enum E {
34     ///     A,
35     ///     B,
36     ///     #[doc(hidden)]
37     ///     _C,
38     /// }
39     ///
40     /// struct T(pub i32, pub i32, ());
41     /// ```
42     /// Use instead:
43     /// ```rust
44     /// #[non_exhaustive]
45     /// struct S {
46     ///     pub a: i32,
47     ///     pub b: i32,
48     /// }
49     ///
50     /// #[non_exhaustive]
51     /// enum E {
52     ///     A,
53     ///     B,
54     /// }
55     ///
56     /// #[non_exhaustive]
57     /// struct T(pub i32, pub i32);
58     /// ```
59     #[clippy::version = "1.45.0"]
60     pub MANUAL_NON_EXHAUSTIVE,
61     style,
62     "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
63 }
64
65 #[expect(clippy::module_name_repetitions)]
66 pub struct ManualNonExhaustiveStruct {
67     msrv: Option<RustcVersion>,
68 }
69
70 impl ManualNonExhaustiveStruct {
71     #[must_use]
72     pub fn new(msrv: Option<RustcVersion>) -> Self {
73         Self { msrv }
74     }
75 }
76
77 impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]);
78
79 #[expect(clippy::module_name_repetitions)]
80 pub struct ManualNonExhaustiveEnum {
81     msrv: Option<RustcVersion>,
82     constructed_enum_variants: FxHashSet<(DefId, DefId)>,
83     potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>,
84 }
85
86 impl ManualNonExhaustiveEnum {
87     #[must_use]
88     pub fn new(msrv: Option<RustcVersion>) -> Self {
89         Self {
90             msrv,
91             constructed_enum_variants: FxHashSet::default(),
92             potential_enums: Vec::new(),
93         }
94     }
95 }
96
97 impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]);
98
99 impl EarlyLintPass for ManualNonExhaustiveStruct {
100     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
101         if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) {
102             return;
103         }
104
105         if let ast::ItemKind::Struct(variant_data, _) = &item.kind {
106             let (fields, delimiter) = match variant_data {
107                 ast::VariantData::Struct(fields, _) => (&**fields, '{'),
108                 ast::VariantData::Tuple(fields, _) => (&**fields, '('),
109                 ast::VariantData::Unit(_) => return,
110             };
111             if fields.len() <= 1 {
112                 return;
113             }
114             let mut iter = fields.iter().filter_map(|f| match f.vis.kind {
115                 VisibilityKind::Public => None,
116                 VisibilityKind::Inherited => Some(Ok(f)),
117                 _ => Some(Err(())),
118             });
119             if let Some(Ok(field)) = iter.next()
120                 && iter.next().is_none()
121                 && field.ty.kind.is_unit()
122                 && field.ident.map_or(true, |name| name.as_str().starts_with('_'))
123             {
124                 span_lint_and_then(
125                     cx,
126                     MANUAL_NON_EXHAUSTIVE,
127                     item.span,
128                     "this seems like a manual implementation of the non-exhaustive pattern",
129                     |diag| {
130                         if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive))
131                             && let header_span = cx.sess().source_map().span_until_char(item.span, delimiter)
132                             && let Some(snippet) = snippet_opt(cx, header_span)
133                         {
134                             diag.span_suggestion(
135                                 header_span,
136                                 "add the attribute",
137                                 format!("#[non_exhaustive] {}", snippet),
138                                 Applicability::Unspecified,
139                             );
140                         }
141                         diag.span_help(field.span, "remove this field");
142                     }
143                 );
144             }
145         }
146     }
147
148     extract_msrv_attr!(EarlyContext);
149 }
150
151 impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum {
152     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
153         if !meets_msrv(self.msrv, msrvs::NON_EXHAUSTIVE) {
154             return;
155         }
156
157         if let hir::ItemKind::Enum(def, _) = &item.kind
158             && def.variants.len() > 1
159         {
160             let mut iter = def.variants.iter().filter_map(|v| {
161                 let id = cx.tcx.hir().local_def_id(v.id);
162                 (matches!(v.data, hir::VariantData::Unit(_))
163                     && v.ident.as_str().starts_with('_')
164                     && is_doc_hidden(cx.tcx.get_attrs(id.to_def_id())))
165                 .then(|| (id, v.span))
166             });
167             if let Some((id, span)) = iter.next()
168                 && iter.next().is_none()
169             {
170                 self.potential_enums.push((item.def_id, id, item.span, span));
171             }
172         }
173     }
174
175     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
176         if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind
177             && let [.., name] = p.segments
178             && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = p.res
179             && name.ident.as_str().starts_with('_')
180         {
181             let variant_id = cx.tcx.parent(id);
182             let enum_id = cx.tcx.parent(variant_id);
183
184             self.constructed_enum_variants.insert((enum_id, variant_id));
185         }
186     }
187
188     fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
189         for &(enum_id, _, enum_span, variant_span) in
190             self.potential_enums.iter().filter(|&&(enum_id, variant_id, _, _)| {
191                 !self
192                     .constructed_enum_variants
193                     .contains(&(enum_id.to_def_id(), variant_id.to_def_id()))
194                     && !is_lint_allowed(cx, MANUAL_NON_EXHAUSTIVE, cx.tcx.hir().local_def_id_to_hir_id(enum_id))
195             })
196         {
197             span_lint_and_then(
198                 cx,
199                 MANUAL_NON_EXHAUSTIVE,
200                 enum_span,
201                 "this seems like a manual implementation of the non-exhaustive pattern",
202                 |diag| {
203                     if !cx.tcx.adt_def(enum_id).is_variant_list_non_exhaustive()
204                         && let header_span = cx.sess().source_map().span_until_char(enum_span, '{')
205                         && let Some(snippet) = snippet_opt(cx, header_span)
206                     {
207                             diag.span_suggestion(
208                                 header_span,
209                                 "add the attribute",
210                                 format!("#[non_exhaustive] {}", snippet),
211                                 Applicability::Unspecified,
212                             );
213                     }
214                     diag.span_help(variant_span, "remove this variant");
215                 },
216             );
217         }
218     }
219
220     extract_msrv_attr!(LateContext);
221 }