-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Implement the manual_non_exhaustive lint #5550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
use crate::utils::{snippet_opt, span_lint_and_then}; | ||
use if_chain::if_chain; | ||
use rustc_ast::ast::{Attribute, Ident, Item, ItemKind, StructField, TyKind, Variant, VariantData, VisibilityKind}; | ||
use rustc_attr as attr; | ||
use rustc_errors::Applicability; | ||
use rustc_lint::{EarlyContext, EarlyLintPass}; | ||
use rustc_session::{declare_lint_pass, declare_tool_lint}; | ||
|
||
declare_clippy_lint! { | ||
/// **What it does:** Checks for manual implementations of the non-exhaustive pattern. | ||
/// | ||
/// **Why is this bad?** Using the #[non_exhaustive] attribute expresses better the intent | ||
/// and allows possible optimizations when applied to enums. | ||
/// | ||
/// **Known problems:** None. | ||
/// | ||
/// **Example:** | ||
/// | ||
/// ```rust | ||
/// struct S { | ||
/// pub a: i32, | ||
/// pub b: i32, | ||
/// _c: (), | ||
/// } | ||
/// | ||
/// enum E { | ||
/// A, | ||
/// B, | ||
/// #[doc(hidden)] | ||
/// _C, | ||
/// } | ||
/// | ||
/// struct T(pub i32, pub i32, ()); | ||
/// ``` | ||
/// Use instead: | ||
/// ```rust | ||
/// #[non_exhaustive] | ||
/// struct S { | ||
/// pub a: i32, | ||
/// pub b: i32, | ||
/// } | ||
/// | ||
/// #[non_exhaustive] | ||
/// enum E { | ||
/// A, | ||
/// B, | ||
/// } | ||
/// | ||
/// #[non_exhaustive] | ||
/// struct T(pub i32, pub i32); | ||
/// ``` | ||
pub MANUAL_NON_EXHAUSTIVE, | ||
style, | ||
"manual implementations of the non-exhaustive pattern can be simplified using #[non_exhaustive]" | ||
} | ||
|
||
declare_lint_pass!(ManualNonExhaustive => [MANUAL_NON_EXHAUSTIVE]); | ||
|
||
impl EarlyLintPass for ManualNonExhaustive { | ||
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) { | ||
match &item.kind { | ||
ItemKind::Enum(def, _) => { | ||
check_manual_non_exhaustive_enum(cx, item, &def.variants); | ||
}, | ||
ItemKind::Struct(variant_data, _) => { | ||
if let VariantData::Unit(..) = variant_data { | ||
return; | ||
} | ||
|
||
check_manual_non_exhaustive_struct(cx, item, variant_data); | ||
}, | ||
_ => {}, | ||
} | ||
} | ||
} | ||
|
||
fn check_manual_non_exhaustive_enum(cx: &EarlyContext<'_>, item: &Item, variants: &[Variant]) { | ||
fn is_non_exhaustive_marker(variant: &Variant) -> bool { | ||
matches!(variant.data, VariantData::Unit(_)) | ||
&& variant.ident.as_str().starts_with('_') | ||
&& variant.attrs.iter().any(|a| is_doc_hidden(a)) | ||
} | ||
|
||
fn is_doc_hidden(attr: &Attribute) -> bool { | ||
attr.check_name(sym!(doc)) | ||
&& match attr.meta_item_list() { | ||
Some(l) => attr::list_contains_name(&l, sym!(hidden)), | ||
None => false, | ||
} | ||
} | ||
|
||
if_chain! { | ||
if !attr::contains_name(&item.attrs, sym!(non_exhaustive)); | ||
let markers = variants.iter().filter(|v| is_non_exhaustive_marker(v)).count(); | ||
if markers == 1 && variants.len() > markers; | ||
if let Some(variant) = variants.last(); | ||
if is_non_exhaustive_marker(variant); | ||
then { | ||
span_lint_and_then( | ||
cx, | ||
MANUAL_NON_EXHAUSTIVE, | ||
item.span, | ||
"this seems like a manual implementation of the non-exhaustive pattern", | ||
|diag| { | ||
let header_span = cx.sess.source_map().span_until_char(item.span, '{'); | ||
|
||
if let Some(snippet) = snippet_opt(cx, header_span) { | ||
diag.span_suggestion( | ||
item.span, | ||
ebroto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"add the attribute", | ||
format!("#[non_exhaustive] {}", snippet), | ||
Applicability::Unspecified, | ||
); | ||
diag.span_help(variant.span, "and remove this variant"); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
|
||
fn check_manual_non_exhaustive_struct(cx: &EarlyContext<'_>, item: &Item, data: &VariantData) { | ||
fn is_private(field: &StructField) -> bool { | ||
matches!(field.vis.node, VisibilityKind::Inherited) | ||
} | ||
|
||
fn is_non_exhaustive_marker(name: &Option<Ident>) -> bool { | ||
name.map(|n| n.as_str().starts_with('_')).unwrap_or(true) | ||
} | ||
|
||
let fields = data.fields(); | ||
let private_fields = fields.iter().filter(|f| is_private(f)).count(); | ||
let public_fields = fields.iter().filter(|f| f.vis.node.is_pub()).count(); | ||
|
||
if_chain! { | ||
if !attr::contains_name(&item.attrs, sym!(non_exhaustive)); | ||
if private_fields == 1 && public_fields >= private_fields && public_fields == fields.len() - 1; | ||
ebroto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if let Some(field) = fields.iter().find(|f| is_private(f)); | ||
if is_non_exhaustive_marker(&field.ident); | ||
if let TyKind::Tup(tup_fields) = &field.ty.kind; | ||
if tup_fields.is_empty(); | ||
ebroto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
then { | ||
span_lint_and_then( | ||
cx, | ||
MANUAL_NON_EXHAUSTIVE, | ||
item.span, | ||
"this seems like a manual implementation of the non-exhaustive pattern", | ||
|diag| { | ||
let delimiter = match data { | ||
VariantData::Struct(..) => '{', | ||
VariantData::Tuple(..) => '(', | ||
_ => unreachable!(), | ||
ebroto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
let header_span = cx.sess.source_map().span_until_char(item.span, delimiter); | ||
|
||
if let Some(snippet) = snippet_opt(cx, header_span) { | ||
diag.span_suggestion( | ||
item.span, | ||
ebroto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"add the attribute", | ||
format!("#[non_exhaustive] {}", snippet), | ||
Applicability::Unspecified, | ||
); | ||
diag.span_help(field.span, "and remove this field"); | ||
} | ||
}); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
#![warn(clippy::manual_non_exhaustive)] | ||
#![allow(unused)] | ||
|
||
mod enums { | ||
enum E { | ||
A, | ||
B, | ||
#[doc(hidden)] | ||
_C, | ||
} | ||
|
||
// last variant does not have doc hidden attribute, should be ignored | ||
enum NoDocHidden { | ||
A, | ||
B, | ||
_C, | ||
} | ||
|
||
// name of variant with doc hidden does not start with underscore, should be ignored | ||
enum NoUnderscore { | ||
A, | ||
B, | ||
#[doc(hidden)] | ||
C, | ||
} | ||
|
||
// variant with doc hidden is not unit, should be ignored | ||
enum NotUnit { | ||
A, | ||
B, | ||
#[doc(hidden)] | ||
_C(bool), | ||
} | ||
|
||
// variant with doc hidden is not the last one, should be ignored | ||
enum NotLast { | ||
A, | ||
#[doc(hidden)] | ||
_B, | ||
C, | ||
} | ||
|
||
// variant with doc hidden is the only one, should be ignored | ||
enum OnlyMarker { | ||
#[doc(hidden)] | ||
_A, | ||
} | ||
|
||
// variant with multiple non-exhaustive "markers", should be ignored | ||
enum MultipleMarkers { | ||
A, | ||
#[doc(hidden)] | ||
_B, | ||
#[doc(hidden)] | ||
_C, | ||
} | ||
|
||
// already non_exhaustive, should be ignored | ||
#[non_exhaustive] | ||
enum NonExhaustive { | ||
A, | ||
B, | ||
} | ||
} | ||
|
||
mod structs { | ||
struct S { | ||
pub a: i32, | ||
pub b: i32, | ||
_c: (), | ||
} | ||
|
||
// some other fields are private, should be ignored | ||
struct PrivateFields { | ||
a: i32, | ||
pub b: i32, | ||
_c: (), | ||
} | ||
|
||
// private field name does not start with underscore, should be ignored | ||
struct NoUnderscore { | ||
pub a: i32, | ||
pub b: i32, | ||
c: (), | ||
} | ||
|
||
// private field is not unit type, should be ignored | ||
struct NotUnit { | ||
pub a: i32, | ||
pub b: i32, | ||
_c: i32, | ||
} | ||
|
||
// private field is the only field, should be ignored | ||
struct OnlyMarker { | ||
_a: (), | ||
} | ||
|
||
// already non exhaustive, should be ignored | ||
#[non_exhaustive] | ||
struct NonExhaustive { | ||
pub a: i32, | ||
pub b: i32, | ||
} | ||
} | ||
|
||
mod tuple_structs { | ||
struct T(pub i32, pub i32, ()); | ||
|
||
// some other fields are private, should be ignored | ||
struct PrivateFields(pub i32, i32, ()); | ||
|
||
// private field is not unit type, should be ignored | ||
struct NotUnit(pub i32, pub i32, i32); | ||
|
||
// private field is the only field, should be ignored | ||
struct OnlyMarker(()); | ||
|
||
// already non exhaustive, should be ignored | ||
#[non_exhaustive] | ||
struct NonExhaustive(pub i32, pub i32); | ||
} | ||
|
||
fn main() {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.