Skip to content
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

Fix enum_variants FP on prefixes that are not camel-case #8127

Merged
merged 8 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 65 additions & 59 deletions clippy_lints/src/enum_variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
use clippy_utils::source::is_present_in_source;
use clippy_utils::str_utils::{self, count_match_end, count_match_start};
use rustc_hir::{EnumDef, Item, ItemKind};
use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start};
use rustc_hir::{EnumDef, Item, ItemKind, Variant};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::source_map::Span;
Expand All @@ -18,6 +18,12 @@ declare_clippy_lint! {
/// Enumeration variant names should specify their variant,
/// not repeat the enumeration name.
///
/// ### Limitations
/// Characters with no casing will be considered when comparing prefixes/suffixes
/// This applies to numbers and non-ascii characters without casing
/// e.g. `Foo1` and `Foo2` is considered to have different prefixes
/// (the prefixes are `Foo1` and `Foo2` respectively), as also `Bar螃`, `Bar蟹`
///
/// ### Example
/// ```rust
/// enum Cake {
Expand Down Expand Up @@ -120,72 +126,73 @@ impl_lint_pass!(EnumVariantNames => [
MODULE_INCEPTION
]);

fn check_variant(
cx: &LateContext<'_>,
threshold: u64,
def: &EnumDef<'_>,
item_name: &str,
item_name_chars: usize,
span: Span,
) {
fn check_enum_start(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) {
let name = variant.ident.name.as_str();
let item_name_chars = item_name.chars().count();

if count_match_start(item_name, &name).char_count == item_name_chars
&& name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
&& name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
{
span_lint(
cx,
ENUM_VARIANT_NAMES,
variant.span,
"variant name starts with the enum's name",
);
}
}

fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) {
let name = variant.ident.name.as_str();
let item_name_chars = item_name.chars().count();

if count_match_end(item_name, &name).char_count == item_name_chars {
span_lint(
cx,
ENUM_VARIANT_NAMES,
variant.span,
"variant name ends with the enum's name",
);
}
}

fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) {
if (def.variants.len() as u64) < threshold {
return;
}
for var in def.variants {
let name = var.ident.name.as_str();
if count_match_start(item_name, &name).char_count == item_name_chars
&& name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
&& name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
{
span_lint(
cx,
ENUM_VARIANT_NAMES,
var.span,
"variant name starts with the enum's name",
);
}
if count_match_end(item_name, &name).char_count == item_name_chars {
span_lint(
cx,
ENUM_VARIANT_NAMES,
var.span,
"variant name ends with the enum's name",
);
}
}

let first = &def.variants[0].ident.name.as_str();
let mut pre = &first[..str_utils::camel_case_until(&*first).byte_index];
let mut post = &first[str_utils::camel_case_start(&*first).byte_index..];
let mut pre = camel_case_split(first);
let mut post = pre.clone();
post.reverse();
for var in def.variants {
check_enum_start(cx, item_name, var);
check_enum_end(cx, item_name, var);
let name = var.ident.name.as_str();

let pre_match = count_match_start(pre, &name).byte_count;
pre = &pre[..pre_match];
let pre_camel = str_utils::camel_case_until(pre).byte_index;
pre = &pre[..pre_camel];
while let Some((next, last)) = name[pre.len()..].chars().zip(pre.chars().rev()).next() {
if next.is_numeric() {
return;
}
if next.is_lowercase() {
let last = pre.len() - last.len_utf8();
let last_camel = str_utils::camel_case_until(&pre[..last]);
pre = &pre[..last_camel.byte_index];
} else {
break;
}
}
let variant_split = camel_case_split(&name);

let post_match = count_match_end(post, &name);
let post_end = post.len() - post_match.byte_count;
post = &post[post_end..];
let post_camel = str_utils::camel_case_start(post);
post = &post[post_camel.byte_index..];
pre = pre
.iter()
.zip(variant_split.iter())
.take_while(|(a, b)| a == b)
.map(|e| *e.0)
.collect();
post = post
.iter()
.zip(variant_split.iter().rev())
.take_while(|(a, b)| a == b)
.map(|e| *e.0)
.collect();
}
let (what, value) = match (pre.is_empty(), post.is_empty()) {
(true, true) => return,
(false, _) => ("pre", pre),
(true, false) => ("post", post),
(false, _) => ("pre", pre.join("")),
(true, false) => {
post.reverse();
("post", post.join(""))
},
};
span_lint_and_help(
cx,
Expand Down Expand Up @@ -233,7 +240,6 @@ impl LateLintPass<'_> for EnumVariantNames {
#[allow(clippy::similar_names)]
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
let item_name = item.ident.name.as_str();
let item_name_chars = item_name.chars().count();
let item_camel = to_camel_case(&item_name);
if !item.span.from_expansion() && is_present_in_source(cx, item.span) {
if let Some(&(ref mod_name, ref mod_camel)) = self.modules.last() {
Expand Down Expand Up @@ -283,7 +289,7 @@ impl LateLintPass<'_> for EnumVariantNames {
}
if let ItemKind::Enum(ref def, _) = item.kind {
if !(self.avoid_breaking_exported_api && cx.access_levels.is_exported(item.def_id)) {
check_variant(cx, self.threshold, def, &item_name, item_name_chars, item.span);
check_variant(cx, self.threshold, def, &item_name, item.span);
}
}
self.modules.push((item.ident.name, item_camel));
Expand Down
91 changes: 91 additions & 0 deletions clippy_utils/src/str_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ pub fn camel_case_until(s: &str) -> StrIndex {
/// ```
#[must_use]
pub fn camel_case_start(s: &str) -> StrIndex {
camel_case_start_from_idx(s, 0)
}

/// Returns `StrIndex` of the last camel-case component of `s[idx..]`.
///
/// ```
/// # use clippy_utils::str_utils::{camel_case_start_from_idx, StrIndex};
/// assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
/// assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
/// assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
/// ```
pub fn camel_case_start_from_idx(s: &str, start_idx: usize) -> StrIndex {
let char_count = s.chars().count();
let range = 0..char_count;
let mut iter = range.rev().zip(s.char_indices().rev());
Expand All @@ -78,9 +92,13 @@ pub fn camel_case_start(s: &str) -> StrIndex {
} else {
return StrIndex::new(char_count, s.len());
}

let mut down = true;
let mut last_index = StrIndex::new(char_count, s.len());
for (char_index, (byte_index, c)) in iter {
if byte_index < start_idx {
break;
}
if down {
if c.is_uppercase() {
down = false;
Expand All @@ -98,9 +116,55 @@ pub fn camel_case_start(s: &str) -> StrIndex {
return last_index;
}
}

last_index
}

/// Get the indexes of camel case components of a string `s`
///
/// ```
/// # use clippy_utils::str_utils::{camel_case_indices, StrIndex};
/// assert_eq!(
/// camel_case_indices("AbcDef"),
/// vec![StrIndex::new(0, 0), StrIndex::new(3, 3), StrIndex::new(6, 6)]
/// );
/// assert_eq!(
/// camel_case_indices("abcDef"),
/// vec![StrIndex::new(3, 3), StrIndex::new(6, 6)]
/// );
/// ```
pub fn camel_case_indices(s: &str) -> Vec<StrIndex> {
let mut result = Vec::new();
let mut str_idx = camel_case_start(s);

while str_idx.byte_index < s.len() {
let next_idx = str_idx.byte_index + 1;
result.push(str_idx);
str_idx = camel_case_start_from_idx(s, next_idx);
}
result.push(str_idx);

result
}

/// Split camel case string into a vector of its components
///
/// ```
/// # use clippy_utils::str_utils::{camel_case_split, StrIndex};
/// assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
/// ```
pub fn camel_case_split(s: &str) -> Vec<&str> {
let mut offsets = camel_case_indices(s)
.iter()
.map(|e| e.byte_index)
.collect::<Vec<usize>>();
if offsets[0] != 0 {
offsets.insert(0, 0);
}

offsets.windows(2).map(|w| &s[w[0]..w[1]]).collect()
}

/// Dealing with sting comparison can be complicated, this struct ensures that both the
/// character and byte count are provided for correct indexing.
#[derive(Debug, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -231,4 +295,31 @@ mod test {
fn until_caps() {
assert_eq!(camel_case_until("ABCD"), StrIndex::new(0, 0));
}

#[test]
fn camel_case_start_from_idx_full() {
assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
assert_eq!(camel_case_start_from_idx("AbcDef", 4), StrIndex::new(6, 6));
assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
}

#[test]
fn camel_case_indices_full() {
assert_eq!(camel_case_indices("Abc\u{f6}\u{f6}DD"), vec![StrIndex::new(7, 9)]);
}

#[test]
fn camel_case_split_full() {
assert_eq!(camel_case_split("A"), vec!["A"]);
assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
assert_eq!(camel_case_split("Abc"), vec!["Abc"]);
assert_eq!(camel_case_split("abcDef"), vec!["abc", "Def"]);
assert_eq!(
camel_case_split("\u{f6}\u{f6}AabABcd"),
vec!["\u{f6}\u{f6}", "Aab", "A", "Bcd"]
);
}
}
6 changes: 6 additions & 0 deletions tests/ui/enum_variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,10 @@ enum HIDataRequest {
DeleteUnpubHIData(String),
}

enum North {
Normal,
NoLeft,
NoRight,
}

fn main() {}
38 changes: 25 additions & 13 deletions tests/ui/enum_variants.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ LL | cFoo,
|
= note: `-D clippy::enum-variant-names` implied by `-D warnings`

error: all variants have the same prefix: `c`
--> $DIR/enum_variants.rs:14:1
|
LL | / enum Foo {
LL | | cFoo,
LL | | cBar,
LL | | cBaz,
LL | | }
| |_^
|
= help: remove the prefixes and use full paths to the variants instead of glob imports

error: variant name starts with the enum's name
--> $DIR/enum_variants.rs:26:5
|
Expand Down Expand Up @@ -60,25 +72,25 @@ LL | | }
|
= help: remove the prefixes and use full paths to the variants instead of glob imports

error: all variants have the same prefix: `WithOut`
--> $DIR/enum_variants.rs:81:1
error: all variants have the same prefix: `C`
--> $DIR/enum_variants.rs:59:1
|
LL | / enum Seallll {
LL | | WithOutCake,
LL | | WithOutTea,
LL | | WithOut,
LL | / enum Something {
LL | | CCall,
LL | | CCreate,
LL | | CCryogenize,
LL | | }
| |_^
|
= help: remove the prefixes and use full paths to the variants instead of glob imports

error: all variants have the same prefix: `Prefix`
--> $DIR/enum_variants.rs:87:1
error: all variants have the same prefix: `WithOut`
--> $DIR/enum_variants.rs:81:1
|
LL | / enum NonCaps {
LL | | Prefix的,
xFrednet marked this conversation as resolved.
Show resolved Hide resolved
LL | | PrefixTea,
LL | | PrefixCake,
LL | / enum Seallll {
LL | | WithOutCake,
LL | | WithOutTea,
LL | | WithOut,
LL | | }
| |_^
|
Expand Down Expand Up @@ -108,5 +120,5 @@ LL | | }
|
= help: remove the postfixes and use full paths to the variants instead of glob imports

error: aborting due to 11 previous errors
error: aborting due to 12 previous errors