Skip to content

Commit f072ded

Browse files
committed
Implement the manual_non_exhaustive lint
1 parent 0a53ed2 commit f072ded

File tree

6 files changed

+352
-0
lines changed

6 files changed

+352
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,7 @@ Released 2018-09-13
14231423
[`macro_use_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#macro_use_imports
14241424
[`main_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#main_recursion
14251425
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
1426+
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
14261427
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
14271428
[`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
14281429
[`many_single_char_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#many_single_char_names

clippy_lints/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ mod literal_representation;
247247
mod loops;
248248
mod macro_use;
249249
mod main_recursion;
250+
mod manual_non_exhaustive;
250251
mod map_clone;
251252
mod map_unit_fn;
252253
mod match_on_vec_items;
@@ -628,6 +629,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
628629
&loops::WHILE_LET_ON_ITERATOR,
629630
&macro_use::MACRO_USE_IMPORTS,
630631
&main_recursion::MAIN_RECURSION,
632+
&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
631633
&map_clone::MAP_CLONE,
632634
&map_unit_fn::OPTION_MAP_UNIT_FN,
633635
&map_unit_fn::RESULT_MAP_UNIT_FN,
@@ -1064,6 +1066,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10641066
store.register_late_pass(|| box utils::internal_lints::CollapsibleCalls);
10651067
store.register_late_pass(|| box if_let_mutex::IfLetMutex);
10661068
store.register_late_pass(|| box match_on_vec_items::MatchOnVecItems);
1069+
store.register_early_pass(|| box manual_non_exhaustive::ManualNonExhaustive);
10671070

10681071
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
10691072
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1280,6 +1283,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12801283
LintId::of(&loops::WHILE_LET_LOOP),
12811284
LintId::of(&loops::WHILE_LET_ON_ITERATOR),
12821285
LintId::of(&main_recursion::MAIN_RECURSION),
1286+
LintId::of(&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
12831287
LintId::of(&map_clone::MAP_CLONE),
12841288
LintId::of(&map_unit_fn::OPTION_MAP_UNIT_FN),
12851289
LintId::of(&map_unit_fn::RESULT_MAP_UNIT_FN),
@@ -1474,6 +1478,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
14741478
LintId::of(&loops::NEEDLESS_RANGE_LOOP),
14751479
LintId::of(&loops::WHILE_LET_ON_ITERATOR),
14761480
LintId::of(&main_recursion::MAIN_RECURSION),
1481+
LintId::of(&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
14771482
LintId::of(&map_clone::MAP_CLONE),
14781483
LintId::of(&matches::INFALLIBLE_DESTRUCTURING_MATCH),
14791484
LintId::of(&matches::MATCH_OVERLAPPING_ARM),
+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use crate::utils::{snippet_opt, span_lint_and_then};
2+
use if_chain::if_chain;
3+
use rustc_ast::ast::{Attribute, Ident, Item, ItemKind, StructField, TyKind, Variant, VariantData, VisibilityKind};
4+
use rustc_attr as attr;
5+
use rustc_errors::Applicability;
6+
use rustc_lint::{EarlyContext, EarlyLintPass};
7+
use rustc_session::{declare_lint_pass, declare_tool_lint};
8+
9+
declare_clippy_lint! {
10+
/// **What it does:** Checks for manual implementations of the non-exhaustive pattern.
11+
///
12+
/// **Why is this bad?** Using the #[non_exhaustive] attribute expresses better the intent
13+
/// and allows possible optimizations when applied to enums.
14+
///
15+
/// **Known problems:** None.
16+
///
17+
/// **Example:**
18+
///
19+
/// ```rust
20+
/// struct S {
21+
/// pub a: i32,
22+
/// pub b: i32,
23+
/// _c: (),
24+
/// }
25+
///
26+
/// enum E {
27+
/// A,
28+
/// B,
29+
/// #[doc(hidden)]
30+
/// _C,
31+
/// }
32+
///
33+
/// struct T(pub i32, pub i32, ());
34+
/// ```
35+
/// Use instead:
36+
/// ```rust
37+
/// #[non_exhaustive]
38+
/// struct S {
39+
/// pub a: i32,
40+
/// pub b: i32,
41+
/// }
42+
///
43+
/// #[non_exhaustive]
44+
/// enum E {
45+
/// A,
46+
/// B,
47+
/// }
48+
///
49+
/// #[non_exhaustive]
50+
/// struct T(pub i32, pub i32);
51+
/// ```
52+
pub MANUAL_NON_EXHAUSTIVE,
53+
style,
54+
"manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]"
55+
}
56+
57+
declare_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]);
58+
59+
impl EarlyLintPass for ManualNonExhaustive {
60+
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
61+
match &item.kind {
62+
ItemKind::Enum(def, _) => {
63+
check_manual_non_exhaustive_enum(cx, item, &def.variants);
64+
},
65+
ItemKind::Struct(variant_data, _) => {
66+
if let VariantData::Unit(..) = variant_data {
67+
return;
68+
}
69+
70+
check_manual_non_exhaustive_struct(cx, item, variant_data);
71+
},
72+
_ => {},
73+
}
74+
}
75+
}
76+
77+
fn check_manual_non_exhaustive_enum(cx: &EarlyContext<'_>, item: &Item, variants: &[Variant]) {
78+
fn is_non_exhaustive_marker(variant: &Variant) -> bool {
79+
matches!(variant.data, VariantData::Unit(_))
80+
&& variant.ident.as_str().starts_with('_')
81+
&& variant.attrs.iter().any(|a| is_doc_hidden(a))
82+
}
83+
84+
fn is_doc_hidden(attr: &Attribute) -> bool {
85+
attr.check_name(sym!(doc))
86+
&& match attr.meta_item_list() {
87+
Some(l) => attr::list_contains_name(&l, sym!(hidden)),
88+
None => false,
89+
}
90+
}
91+
92+
if_chain! {
93+
if !attr::contains_name(&item.attrs, sym!(non_exhaustive));
94+
let markers = variants.iter().filter(|v| is_non_exhaustive_marker(v)).count();
95+
if markers == 1 && variants.len() > markers;
96+
if let Some(variant) = variants.last();
97+
if is_non_exhaustive_marker(variant);
98+
then {
99+
span_lint_and_then(
100+
cx,
101+
MANUAL_NON_EXHAUSTIVE,
102+
item.span,
103+
"this seems like a manual implementation of the non-exhaustive pattern",
104+
|diag| {
105+
let header_span = cx.sess.source_map().span_until_char(item.span, '{');
106+
107+
if let Some(snippet) = snippet_opt(cx, header_span) {
108+
diag.span_suggestion(
109+
item.span,
110+
"add the attribute",
111+
format!("#[non_exhaustive] {}", snippet),
112+
Applicability::Unspecified,
113+
);
114+
diag.span_help(variant.span, "and remove this variant");
115+
}
116+
});
117+
}
118+
}
119+
}
120+
121+
fn check_manual_non_exhaustive_struct(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) {
122+
fn is_private(field: &StructField) -> bool {
123+
matches!(field.vis.node, VisibilityKind::Inherited)
124+
}
125+
126+
fn is_non_exhaustive_marker(name: &Option<Ident>) -> bool {
127+
name.map(|n| n.as_str().starts_with('_')).unwrap_or(true)
128+
}
129+
130+
let fields = data.fields();
131+
let private_fields = fields.iter().filter(|f| is_private(f)).count();
132+
let public_fields = fields.iter().filter(|f| f.vis.node.is_pub()).count();
133+
134+
if_chain! {
135+
if !attr::contains_name(&item.attrs, sym!(non_exhaustive));
136+
if private_fields == 1 && public_fields >= private_fields && public_fields == fields.len() - 1;
137+
if let Some(field) = fields.iter().find(|f| is_private(f));
138+
if is_non_exhaustive_marker(&field.ident);
139+
if let TyKind::Tup(tup_fields) = &field.ty.kind;
140+
if tup_fields.is_empty();
141+
then {
142+
span_lint_and_then(
143+
cx,
144+
MANUAL_NON_EXHAUSTIVE,
145+
item.span,
146+
"this seems like a manual implementation of the non-exhaustive pattern",
147+
|diag| {
148+
let delimiter = match data {
149+
VariantData::Struct(..) => '{',
150+
VariantData::Tuple(..) => '(',
151+
_ => unreachable!(),
152+
};
153+
let header_span = cx.sess.source_map().span_until_char(item.span, delimiter);
154+
155+
if let Some(snippet) = snippet_opt(cx, header_span) {
156+
diag.span_suggestion(
157+
item.span,
158+
"add the attribute",
159+
format!("#[non_exhaustive] {}", snippet),
160+
Applicability::Unspecified,
161+
);
162+
diag.span_help(field.span, "and remove this field");
163+
}
164+
});
165+
}
166+
}
167+
}

src/lintlist/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,13 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
10881088
deprecation: None,
10891089
module: "loops",
10901090
},
1091+
Lint {
1092+
name: "manual_non_exhaustive",
1093+
group: "style",
1094+
desc: "manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]",
1095+
deprecation: None,
1096+
module: "manual_non_exhaustive",
1097+
},
10911098
Lint {
10921099
name: "manual_saturating_arithmetic",
10931100
group: "style",

tests/ui/manual_non_exhaustive.rs

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#![warn(clippy::manual_non_exhaustive)]
2+
#![allow(unused)]
3+
4+
mod enums {
5+
enum E {
6+
A,
7+
B,
8+
#[doc(hidden)]
9+
_C,
10+
}
11+
12+
// last variant does not have doc hidden attribute, should be ignored
13+
enum NoDocHidden {
14+
A,
15+
B,
16+
_C,
17+
}
18+
19+
// name of variant with doc hidden does not start with underscore, should be ignored
20+
enum NoUnderscore {
21+
A,
22+
B,
23+
#[doc(hidden)]
24+
C,
25+
}
26+
27+
// variant with doc hidden is not unit, should be ignored
28+
enum NotUnit {
29+
A,
30+
B,
31+
#[doc(hidden)]
32+
_C(bool),
33+
}
34+
35+
// variant with doc hidden is not the last one, should be ignored
36+
enum NotLast {
37+
A,
38+
#[doc(hidden)]
39+
_B,
40+
C,
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 non-exhaustive "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, should be ignored
59+
#[non_exhaustive]
60+
enum NonExhaustive {
61+
A,
62+
B,
63+
}
64+
}
65+
66+
mod structs {
67+
struct S {
68+
pub a: i32,
69+
pub b: i32,
70+
_c: (),
71+
}
72+
73+
// some other fields are private, should be ignored
74+
struct PrivateFields {
75+
a: i32,
76+
pub b: i32,
77+
_c: (),
78+
}
79+
80+
// private field name does not start with underscore, should be ignored
81+
struct NoUnderscore {
82+
pub a: i32,
83+
pub b: i32,
84+
c: (),
85+
}
86+
87+
// private field is not unit type, should be ignored
88+
struct NotUnit {
89+
pub a: i32,
90+
pub b: i32,
91+
_c: i32,
92+
}
93+
94+
// private field is the only field, should be ignored
95+
struct OnlyMarker {
96+
_a: (),
97+
}
98+
99+
// already non exhaustive, should be ignored
100+
#[non_exhaustive]
101+
struct NonExhaustive {
102+
pub a: i32,
103+
pub b: i32,
104+
}
105+
}
106+
107+
mod tuple_structs {
108+
struct T(pub i32, pub i32, ());
109+
110+
// some other fields are private, should be ignored
111+
struct PrivateFields(pub i32, i32, ());
112+
113+
// private field is not unit type, should be ignored
114+
struct NotUnit(pub i32, pub i32, i32);
115+
116+
// private field is the only field, should be ignored
117+
struct OnlyMarker(());
118+
119+
// already non exhaustive, should be ignored
120+
#[non_exhaustive]
121+
struct NonExhaustive(pub i32, pub i32);
122+
}
123+
124+
fn main() {}

0 commit comments

Comments
 (0)