]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/manual_non_exhaustive.rs
bca193be9e711a520e3e4774c0c55a6af72559b8
[rust.git] / src / tools / clippy / clippy_lints / src / manual_non_exhaustive.rs
1 use clippy_utils::diagnostics::{span_lint_and_then, span_lint_hir_and_then};
2 use clippy_utils::is_doc_hidden;
3 use clippy_utils::msrvs::{self, Msrv};
4 use clippy_utils::source::snippet_opt;
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_session::{declare_tool_lint, impl_lint_pass};
13 use rustc_span::def_id::{DefId, LocalDefId};
14 use rustc_span::{sym, Span};
15
16 declare_clippy_lint! {
17     /// ### What it does
18     /// Checks for manual implementations of the non-exhaustive pattern.
19     ///
20     /// ### Why is this bad?
21     /// Using the #[non_exhaustive] attribute expresses better the intent
22     /// and allows possible optimizations when applied to enums.
23     ///
24     /// ### Example
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     #[clippy::version = "1.45.0"]
59     pub MANUAL_NON_EXHAUSTIVE,
60     style,
61     "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
62 }
63
64 #[expect(clippy::module_name_repetitions)]
65 pub struct ManualNonExhaustiveStruct {
66     msrv: Msrv,
67 }
68
69 impl ManualNonExhaustiveStruct {
70     #[must_use]
71     pub fn new(msrv: Msrv) -> Self {
72         Self { msrv }
73     }
74 }
75
76 impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]);
77
78 #[expect(clippy::module_name_repetitions)]
79 pub struct ManualNonExhaustiveEnum {
80     msrv: Msrv,
81     constructed_enum_variants: FxHashSet<(DefId, DefId)>,
82     potential_enums: Vec<(LocalDefId, LocalDefId, Span, Span)>,
83 }
84
85 impl ManualNonExhaustiveEnum {
86     #[must_use]
87     pub fn new(msrv: Msrv) -> Self {
88         Self {
89             msrv,
90             constructed_enum_variants: FxHashSet::default(),
91             potential_enums: Vec::new(),
92         }
93     }
94 }
95
96 impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]);
97
98 impl EarlyLintPass for ManualNonExhaustiveStruct {
99     fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
100         if !self.msrv.meets(msrvs::NON_EXHAUSTIVE) {
101             return;
102         }
103
104         if let ast::ItemKind::Struct(variant_data, _) = &item.kind {
105             let (fields, delimiter) = match variant_data {
106                 ast::VariantData::Struct(fields, _) => (&**fields, '{'),
107                 ast::VariantData::Tuple(fields, _) => (&**fields, '('),
108                 ast::VariantData::Unit(_) => return,
109             };
110             if fields.len() <= 1 {
111                 return;
112             }
113             let mut iter = fields.iter().filter_map(|f| match f.vis.kind {
114                 VisibilityKind::Public => None,
115                 VisibilityKind::Inherited => Some(Ok(f)),
116                 VisibilityKind::Restricted { .. } => Some(Err(())),
117             });
118             if let Some(Ok(field)) = iter.next()
119                 && iter.next().is_none()
120                 && field.ty.kind.is_unit()
121                 && field.ident.map_or(true, |name| name.as_str().starts_with('_'))
122             {
123                 span_lint_and_then(
124                     cx,
125                     MANUAL_NON_EXHAUSTIVE,
126                     item.span,
127                     "this seems like a manual implementation of the non-exhaustive pattern",
128                     |diag| {
129                         if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive))
130                             && let header_span = cx.sess().source_map().span_until_char(item.span, delimiter)
131                             && let Some(snippet) = snippet_opt(cx, header_span)
132                         {
133                             diag.span_suggestion(
134                                 header_span,
135                                 "add the attribute",
136                                 format!("#[non_exhaustive] {snippet}"),
137                                 Applicability::Unspecified,
138                             );
139                         }
140                         diag.span_help(field.span, "remove this field");
141                     }
142                 );
143             }
144         }
145     }
146
147     extract_msrv_attr!(EarlyContext);
148 }
149
150 impl<'tcx> LateLintPass<'tcx> for ManualNonExhaustiveEnum {
151     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
152         if !self.msrv.meets(msrvs::NON_EXHAUSTIVE) {
153             return;
154         }
155
156         if let hir::ItemKind::Enum(def, _) = &item.kind
157             && def.variants.len() > 1
158         {
159             let mut iter = def.variants.iter().filter_map(|v| {
160                 let id = cx.tcx.hir().local_def_id(v.hir_id);
161                 (matches!(v.data, hir::VariantData::Unit(..))
162                     && v.ident.as_str().starts_with('_')
163                     && is_doc_hidden(cx.tcx.hir().attrs(v.hir_id)))
164                 .then_some((id, v.span))
165             });
166             if let Some((id, span)) = iter.next()
167                 && iter.next().is_none()
168             {
169                 self.potential_enums.push((item.owner_id.def_id, id, item.span, span));
170             }
171         }
172     }
173
174     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
175         if let ExprKind::Path(QPath::Resolved(None, p)) = &e.kind
176             && let [.., name] = p.segments
177             && let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = p.res
178             && name.ident.as_str().starts_with('_')
179         {
180             let variant_id = cx.tcx.parent(id);
181             let enum_id = cx.tcx.parent(variant_id);
182
183             self.constructed_enum_variants.insert((enum_id, variant_id));
184         }
185     }
186
187     fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
188         for &(enum_id, _, enum_span, variant_span) in
189             self.potential_enums.iter().filter(|&&(enum_id, variant_id, _, _)| {
190                 !self
191                     .constructed_enum_variants
192                     .contains(&(enum_id.to_def_id(), variant_id.to_def_id()))
193             })
194         {
195             let hir_id = cx.tcx.hir().local_def_id_to_hir_id(enum_id);
196             span_lint_hir_and_then(
197                 cx,
198                 MANUAL_NON_EXHAUSTIVE,
199                 hir_id,
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 }