Skip to content

Commit d8c97e6

Browse files
committed
Auto merge of #8645 - Jarcho:manual_non_exhaustive_5714, r=Jarcho
Don't lint `manual_non_exhaustive` when the enum variant is used fixes #5714 changelog: Don't lint `manual_non_exhaustive` when the enum variant is used
2 parents 849668a + b0f8a31 commit d8c97e6

6 files changed

+247
-194
lines changed

clippy_lints/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
573573
store.register_late_pass(move || Box::new(approx_const::ApproxConstant::new(msrv)));
574574
store.register_late_pass(move || Box::new(methods::Methods::new(avoid_breaking_exported_api, msrv)));
575575
store.register_late_pass(move || Box::new(matches::Matches::new(msrv)));
576-
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustive::new(msrv)));
576+
store.register_early_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveStruct::new(msrv)));
577+
store.register_late_pass(move || Box::new(manual_non_exhaustive::ManualNonExhaustiveEnum::new(msrv)));
577578
store.register_late_pass(move || Box::new(manual_strip::ManualStrip::new(msrv)));
578579
store.register_early_pass(move || Box::new(redundant_static_lifetimes::RedundantStaticLifetimes::new(msrv)));
579580
store.register_early_pass(move || Box::new(redundant_field_names::RedundantFieldNames::new(msrv)));
Lines changed: 116 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
use clippy_utils::attrs::is_doc_hidden;
22
use clippy_utils::diagnostics::span_lint_and_then;
33
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;
77
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;
912
use rustc_semver::RustcVersion;
1013
use rustc_session::{declare_tool_lint, impl_lint_pass};
14+
use rustc_span::def_id::{DefId, LocalDefId};
1115
use rustc_span::{sym, Span};
1216

1317
declare_clippy_lint! {
@@ -58,129 +62,159 @@ declare_clippy_lint! {
5862
"manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
5963
}
6064

61-
#[derive(Clone)]
62-
pub struct ManualNonExhaustive {
65+
#[allow(clippy::module_name_repetitions)]
66+
pub struct ManualNonExhaustiveStruct {
6367
msrv: Option<RustcVersion>,
6468
}
6569

66-
impl ManualNonExhaustive {
70+
impl ManualNonExhaustiveStruct {
6771
#[must_use]
6872
pub fn new(msrv: Option<RustcVersion>) -> Self {
6973
Self { msrv }
7074
}
7175
}
7276

73-
impl_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
77+
impl_lint_pass!(ManualNonExhaustiveStruct => [MANUAL_NON_EXHAUSTIVE]);
7478

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+
}
8985

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(),
9393
}
9494
}
95-
96-
extract_msrv_attr!(EarlyContext);
9795
}
9896

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]);
10598

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+
{
122134
diag.span_suggestion(
123135
header_span,
124136
"add the attribute",
125137
format!("#[non_exhaustive] {}", snippet),
126138
Applicability::Unspecified,
127139
);
128140
}
141+
diag.span_help(field.span, "remove this field");
129142
}
130-
diag.span_help(marker.span, "remove this variant");
131-
});
143+
);
144+
}
132145
}
133146
}
147+
148+
extract_msrv_attr!(EarlyContext);
134149
}
135150

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+
}
140156

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+
}
143173
}
144174

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+
}
153185
}
154186

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+
{
163196
span_lint_and_then(
164197
cx,
165198
MANUAL_NON_EXHAUSTIVE,
166-
item.span,
199+
enum_span,
167200
"this seems like a manual implementation of the non-exhaustive pattern",
168201
|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+
{
174206
diag.span_suggestion(
175207
header_span,
176208
"add the attribute",
177209
format!("#[non_exhaustive] {}", snippet),
178210
Applicability::Unspecified,
179211
);
180-
}
181212
}
182-
diag.span_help(marker.span, "remove this field");
183-
});
213+
diag.span_help(variant_span, "remove this variant");
214+
},
215+
);
184216
}
185217
}
218+
219+
extract_msrv_attr!(LateContext);
186220
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#![warn(clippy::manual_non_exhaustive)]
2+
#![allow(unused)]
3+
4+
enum E {
5+
A,
6+
B,
7+
#[doc(hidden)]
8+
_C,
9+
}
10+
11+
// user forgot to remove the marker
12+
#[non_exhaustive]
13+
enum Ep {
14+
A,
15+
B,
16+
#[doc(hidden)]
17+
_C,
18+
}
19+
20+
// marker variant does not have doc hidden attribute, should be ignored
21+
enum NoDocHidden {
22+
A,
23+
B,
24+
_C,
25+
}
26+
27+
// name of variant with doc hidden does not start with underscore, should be ignored
28+
enum NoUnderscore {
29+
A,
30+
B,
31+
#[doc(hidden)]
32+
C,
33+
}
34+
35+
// variant with doc hidden is not unit, should be ignored
36+
enum NotUnit {
37+
A,
38+
B,
39+
#[doc(hidden)]
40+
_C(bool),
41+
}
42+
43+
// variant with doc hidden is the only one, should be ignored
44+
enum OnlyMarker {
45+
#[doc(hidden)]
46+
_A,
47+
}
48+
49+
// variant with multiple markers, should be ignored
50+
enum MultipleMarkers {
51+
A,
52+
#[doc(hidden)]
53+
_B,
54+
#[doc(hidden)]
55+
_C,
56+
}
57+
58+
// already non_exhaustive and no markers, should be ignored
59+
#[non_exhaustive]
60+
enum NonExhaustive {
61+
A,
62+
B,
63+
}
64+
65+
// marked is used, don't lint
66+
enum UsedHidden {
67+
#[doc(hidden)]
68+
_A,
69+
B,
70+
C,
71+
}
72+
fn foo(x: &mut UsedHidden) {
73+
if matches!(*x, UsedHidden::B) {
74+
*x = UsedHidden::_A;
75+
}
76+
}
77+
78+
fn main() {}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
error: this seems like a manual implementation of the non-exhaustive pattern
2+
--> $DIR/manual_non_exhaustive_enum.rs:4:1
3+
|
4+
LL | enum E {
5+
| ^-----
6+
| |
7+
| _help: add the attribute: `#[non_exhaustive] enum E`
8+
| |
9+
LL | | A,
10+
LL | | B,
11+
LL | | #[doc(hidden)]
12+
LL | | _C,
13+
LL | | }
14+
| |_^
15+
|
16+
= note: `-D clippy::manual-non-exhaustive` implied by `-D warnings`
17+
help: remove this variant
18+
--> $DIR/manual_non_exhaustive_enum.rs:8:5
19+
|
20+
LL | _C,
21+
| ^^
22+
23+
error: this seems like a manual implementation of the non-exhaustive pattern
24+
--> $DIR/manual_non_exhaustive_enum.rs:13:1
25+
|
26+
LL | / enum Ep {
27+
LL | | A,
28+
LL | | B,
29+
LL | | #[doc(hidden)]
30+
LL | | _C,
31+
LL | | }
32+
| |_^
33+
|
34+
help: remove this variant
35+
--> $DIR/manual_non_exhaustive_enum.rs:17:5
36+
|
37+
LL | _C,
38+
| ^^
39+
40+
error: aborting due to 2 previous errors
41+

0 commit comments

Comments
 (0)