|
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