|
1 | 1 | use clippy_utils::attrs::is_doc_hidden; |
2 | 2 | use clippy_utils::diagnostics::span_lint_and_then; |
3 | 3 | use clippy_utils::source::snippet_opt; |
4 | | -use clippy_utils::{meets_msrv, msrvs}; |
5 | | -use if_chain::if_chain; |
6 | | -use rustc_ast::ast::{FieldDef, Item, ItemKind, Variant, VariantData, VisibilityKind}; |
| 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 | 7 | use rustc_errors::Applicability; |
8 | | -use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; |
| 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; |
9 | 12 | use rustc_semver::RustcVersion; |
10 | 13 | use rustc_session::{declare_tool_lint, impl_lint_pass}; |
| 14 | +use rustc_span::def_id::{DefId, LocalDefId}; |
11 | 15 | use rustc_span::{sym, Span}; |
12 | 16 |
|
13 | 17 | declare_clippy_lint! { |
@@ -58,129 +62,159 @@ declare_clippy_lint! { |
58 | 62 | "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]" |
59 | 63 | } |
60 | 64 |
|
61 | | -#[derive(Clone)] |
62 | | -pub struct ManualNonExhaustive { |
| 65 | +#[allow(clippy::module_name_repetitions)] |
| 66 | +pub struct ManualNonExhaustiveStruct { |
63 | 67 | msrv: Option<RustcVersion>, |
64 | 68 | } |
65 | 69 |
|
66 | | -impl ManualNonExhaustive { |
| 70 | +impl ManualNonExhaustiveStruct { |
67 | 71 | #[must_use] |
68 | 72 | pub fn new(msrv: Option<RustcVersion>) -> Self { |
69 | 73 | Self { msrv } |
70 | 74 | } |
71 | 75 | } |
72 | 76 |
|
73 | | -impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]); |
| 77 | +impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]); |
74 | 78 |
|
75 | | -impl EarlyLintPass for ManualNonExhaustive { |
76 | | - fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { |
77 | | - if !meets_msrv(self.msrv.as_ref(), &msrvs::NON_EXHAUSTIVE) { |
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 | | - } |
| 79 | +#[allow(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 | +} |
89 | 85 |
|
90 | | - check_manual_non_exhaustive_struct(cx, item, variant_data); |
91 | | - }, |
92 | | - _ => {}, |
| 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 | 93 | } |
94 | 94 | } |
95 | | - |
96 | | - extract_msrv_attr!(EarlyContext); |
97 | 95 | } |
98 | 96 |
|
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 | | - && is_doc_hidden(&variant.attrs) |
104 | | - } |
| 97 | +impl_lint_pass!(ManualNonExhaustiveEnum => [MANUAL_NON_EXHAUSTIVE]); |
105 | 98 |
|
106 | | - let mut markers = variants.iter().filter(|v| is_non_exhaustive_marker(v)); |
107 | | - if_chain! { |
108 | | - if let Some(marker) = markers.next(); |
109 | | - if markers.count() == 0 && variants.len() > 1; |
110 | | - then { |
111 | | - span_lint_and_then( |
112 | | - cx, |
113 | | - MANUAL_NON_EXHAUSTIVE, |
114 | | - item.span, |
115 | | - "this seems like a manual implementation of the non-exhaustive pattern", |
116 | | - |diag| { |
117 | | - if_chain! { |
118 | | - if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive)); |
119 | | - let header_span = cx.sess().source_map().span_until_char(item.span, '{'); |
120 | | - if let Some(snippet) = snippet_opt(cx, header_span); |
121 | | - then { |
| 99 | +impl EarlyLintPass for ManualNonExhaustiveStruct { |
| 100 | + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { |
| 101 | + if !meets_msrv(self.msrv.as_ref(), &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 | + { |
122 | 134 | diag.span_suggestion( |
123 | 135 | header_span, |
124 | 136 | "add the attribute", |
125 | 137 | format!("#[non_exhaustive] {}", snippet), |
126 | 138 | Applicability::Unspecified, |
127 | 139 | ); |
128 | 140 | } |
| 141 | + diag.span_help(field.span, "remove this field"); |
129 | 142 | } |
130 | | - diag.span_help(marker.span, "remove this variant"); |
131 | | - }); |
| 143 | + ); |
| 144 | + } |
132 | 145 | } |
133 | 146 | } |
| 147 | + |
| 148 | + extract_msrv_attr!(EarlyContext); |
134 | 149 | } |
135 | 150 |
|
136 | | -fn check_manual_non_exhaustive_struct(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) { |
137 | | - fn is_private(field: &FieldDef) -> bool { |
138 | | - matches!(field.vis.kind, VisibilityKind::Inherited) |
139 | | - } |
| 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.as_ref(), &msrvs::NON_EXHAUSTIVE) { |
| 154 | + return; |
| 155 | + } |
140 | 156 |
|
141 | | - fn is_non_exhaustive_marker(field: &FieldDef) -> bool { |
142 | | - is_private(field) && field.ty.kind.is_unit() && field.ident.map_or(true, |n| n.as_str().starts_with('_')) |
| 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 | + } |
143 | 173 | } |
144 | 174 |
|
145 | | - fn find_header_span(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) -> Span { |
146 | | - let delimiter = match data { |
147 | | - VariantData::Struct(..) => '{', |
148 | | - VariantData::Tuple(..) => '(', |
149 | | - VariantData::Unit(_) => unreachable!("`VariantData::Unit` is already handled above"), |
150 | | - }; |
151 | | - |
152 | | - cx.sess().source_map().span_until_char(item.span, delimiter) |
| 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 | + && let Some(variant_id) = cx.tcx.parent(id) |
| 181 | + && let Some(enum_id) = cx.tcx.parent(variant_id) |
| 182 | + { |
| 183 | + self.constructed_enum_variants.insert((enum_id, variant_id)); |
| 184 | + } |
153 | 185 | } |
154 | 186 |
|
155 | | - let fields = data.fields(); |
156 | | - let private_fields = fields.iter().filter(|f| is_private(f)).count(); |
157 | | - let public_fields = fields.iter().filter(|f| f.vis.kind.is_pub()).count(); |
158 | | - |
159 | | - if_chain! { |
160 | | - if private_fields == 1 && public_fields >= 1 && public_fields == fields.len() - 1; |
161 | | - if let Some(marker) = fields.iter().find(|f| is_non_exhaustive_marker(f)); |
162 | | - then { |
| 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 | + && !is_lint_allowed(cx, MANUAL_NON_EXHAUSTIVE, cx.tcx.hir().local_def_id_to_hir_id(enum_id)) |
| 194 | + }) |
| 195 | + { |
163 | 196 | span_lint_and_then( |
164 | 197 | cx, |
165 | 198 | MANUAL_NON_EXHAUSTIVE, |
166 | | - item.span, |
| 199 | + enum_span, |
167 | 200 | "this seems like a manual implementation of the non-exhaustive pattern", |
168 | 201 | |diag| { |
169 | | - if_chain! { |
170 | | - if !item.attrs.iter().any(|attr| attr.has_name(sym::non_exhaustive)); |
171 | | - let header_span = find_header_span(cx, item, data); |
172 | | - if let Some(snippet) = snippet_opt(cx, header_span); |
173 | | - then { |
| 202 | + if !cx.tcx.adt_def(enum_id).is_variant_list_non_exhaustive() |
| 203 | + && let header_span = cx.sess().source_map().span_until_char(enum_span, '{') |
| 204 | + && let Some(snippet) = snippet_opt(cx, header_span) |
| 205 | + { |
174 | 206 | diag.span_suggestion( |
175 | 207 | header_span, |
176 | 208 | "add the attribute", |
177 | 209 | format!("#[non_exhaustive] {}", snippet), |
178 | 210 | Applicability::Unspecified, |
179 | 211 | ); |
180 | | - } |
181 | 212 | } |
182 | | - diag.span_help(marker.span, "remove this field"); |
183 | | - }); |
| 213 | + diag.span_help(variant_span, "remove this variant"); |
| 214 | + }, |
| 215 | + ); |
184 | 216 | } |
185 | 217 | } |
| 218 | + |
| 219 | + extract_msrv_attr!(LateContext); |
186 | 220 | } |
0 commit comments