Skip to content

Commit 56ccd30

Browse files
committed
Auto merge of #8127 - dswij:8090, r=xFrednet
Fix `enum_variants` FP on prefixes that are not camel-case closes #8090 Fix FP on `enum_variants` when prefixes are only a substring of a camel-case word. Also adds some util helpers on `str_utils` to help parsing camel-case strings. This changes how the lint behaves: 1. previously if the Prefix is only a length of 1, it's going to get ignored, i.e. these were previously ignored and now is warned ```rust enum Foo { cFoo, cBar, cBaz, } enum Something { CCall, CCreate, CCryogenize, } ``` 2. non-ascii characters that doesn't have casing will not be split, ```rust enum NonCaps { PrefixXXX, PrefixTea, PrefixCake, } ``` will be considered as `PrefixXXX`, `Prefix`, `Prefix`, so this won't lint as opposed to fired previously. changelog: [`enum_variant_names`] Fix FP when first prefix are only a substring of a camel-case word. --- (Edited by `@xFrednet` removed some non ascii characters)
2 parents 526fb6b + b82c9ce commit 56ccd30

File tree

4 files changed

+187
-72
lines changed

4 files changed

+187
-72
lines changed

clippy_lints/src/enum_variants.rs

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
33
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
44
use clippy_utils::source::is_present_in_source;
5-
use clippy_utils::str_utils::{self, count_match_end, count_match_start};
6-
use rustc_hir::{EnumDef, Item, ItemKind};
5+
use clippy_utils::str_utils::{camel_case_split, count_match_end, count_match_start};
6+
use rustc_hir::{EnumDef, Item, ItemKind, Variant};
77
use rustc_lint::{LateContext, LateLintPass};
88
use rustc_session::{declare_tool_lint, impl_lint_pass};
99
use rustc_span::source_map::Span;
@@ -18,6 +18,12 @@ declare_clippy_lint! {
1818
/// Enumeration variant names should specify their variant,
1919
/// not repeat the enumeration name.
2020
///
21+
/// ### Limitations
22+
/// Characters with no casing will be considered when comparing prefixes/suffixes
23+
/// This applies to numbers and non-ascii characters without casing
24+
/// e.g. `Foo1` and `Foo2` is considered to have different prefixes
25+
/// (the prefixes are `Foo1` and `Foo2` respectively), as also `Bar螃`, `Bar蟹`
26+
///
2127
/// ### Example
2228
/// ```rust
2329
/// enum Cake {
@@ -120,72 +126,73 @@ impl_lint_pass!(EnumVariantNames => [
120126
MODULE_INCEPTION
121127
]);
122128

123-
fn check_variant(
124-
cx: &LateContext<'_>,
125-
threshold: u64,
126-
def: &EnumDef<'_>,
127-
item_name: &str,
128-
item_name_chars: usize,
129-
span: Span,
130-
) {
129+
fn check_enum_start(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) {
130+
let name = variant.ident.name.as_str();
131+
let item_name_chars = item_name.chars().count();
132+
133+
if count_match_start(item_name, &name).char_count == item_name_chars
134+
&& name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
135+
&& name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
136+
{
137+
span_lint(
138+
cx,
139+
ENUM_VARIANT_NAMES,
140+
variant.span,
141+
"variant name starts with the enum's name",
142+
);
143+
}
144+
}
145+
146+
fn check_enum_end(cx: &LateContext<'_>, item_name: &str, variant: &Variant<'_>) {
147+
let name = variant.ident.name.as_str();
148+
let item_name_chars = item_name.chars().count();
149+
150+
if count_match_end(item_name, &name).char_count == item_name_chars {
151+
span_lint(
152+
cx,
153+
ENUM_VARIANT_NAMES,
154+
variant.span,
155+
"variant name ends with the enum's name",
156+
);
157+
}
158+
}
159+
160+
fn check_variant(cx: &LateContext<'_>, threshold: u64, def: &EnumDef<'_>, item_name: &str, span: Span) {
131161
if (def.variants.len() as u64) < threshold {
132162
return;
133163
}
134-
for var in def.variants {
135-
let name = var.ident.name.as_str();
136-
if count_match_start(item_name, &name).char_count == item_name_chars
137-
&& name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
138-
&& name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
139-
{
140-
span_lint(
141-
cx,
142-
ENUM_VARIANT_NAMES,
143-
var.span,
144-
"variant name starts with the enum's name",
145-
);
146-
}
147-
if count_match_end(item_name, &name).char_count == item_name_chars {
148-
span_lint(
149-
cx,
150-
ENUM_VARIANT_NAMES,
151-
var.span,
152-
"variant name ends with the enum's name",
153-
);
154-
}
155-
}
164+
156165
let first = &def.variants[0].ident.name.as_str();
157-
let mut pre = &first[..str_utils::camel_case_until(&*first).byte_index];
158-
let mut post = &first[str_utils::camel_case_start(&*first).byte_index..];
166+
let mut pre = camel_case_split(first);
167+
let mut post = pre.clone();
168+
post.reverse();
159169
for var in def.variants {
170+
check_enum_start(cx, item_name, var);
171+
check_enum_end(cx, item_name, var);
160172
let name = var.ident.name.as_str();
161173

162-
let pre_match = count_match_start(pre, &name).byte_count;
163-
pre = &pre[..pre_match];
164-
let pre_camel = str_utils::camel_case_until(pre).byte_index;
165-
pre = &pre[..pre_camel];
166-
while let Some((next, last)) = name[pre.len()..].chars().zip(pre.chars().rev()).next() {
167-
if next.is_numeric() {
168-
return;
169-
}
170-
if next.is_lowercase() {
171-
let last = pre.len() - last.len_utf8();
172-
let last_camel = str_utils::camel_case_until(&pre[..last]);
173-
pre = &pre[..last_camel.byte_index];
174-
} else {
175-
break;
176-
}
177-
}
174+
let variant_split = camel_case_split(&name);
178175

179-
let post_match = count_match_end(post, &name);
180-
let post_end = post.len() - post_match.byte_count;
181-
post = &post[post_end..];
182-
let post_camel = str_utils::camel_case_start(post);
183-
post = &post[post_camel.byte_index..];
176+
pre = pre
177+
.iter()
178+
.zip(variant_split.iter())
179+
.take_while(|(a, b)| a == b)
180+
.map(|e| *e.0)
181+
.collect();
182+
post = post
183+
.iter()
184+
.zip(variant_split.iter().rev())
185+
.take_while(|(a, b)| a == b)
186+
.map(|e| *e.0)
187+
.collect();
184188
}
185189
let (what, value) = match (pre.is_empty(), post.is_empty()) {
186190
(true, true) => return,
187-
(false, _) => ("pre", pre),
188-
(true, false) => ("post", post),
191+
(false, _) => ("pre", pre.join("")),
192+
(true, false) => {
193+
post.reverse();
194+
("post", post.join(""))
195+
},
189196
};
190197
span_lint_and_help(
191198
cx,
@@ -233,7 +240,6 @@ impl LateLintPass<'_> for EnumVariantNames {
233240
#[allow(clippy::similar_names)]
234241
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
235242
let item_name = item.ident.name.as_str();
236-
let item_name_chars = item_name.chars().count();
237243
let item_camel = to_camel_case(&item_name);
238244
if !item.span.from_expansion() && is_present_in_source(cx, item.span) {
239245
if let Some(&(ref mod_name, ref mod_camel)) = self.modules.last() {
@@ -283,7 +289,7 @@ impl LateLintPass<'_> for EnumVariantNames {
283289
}
284290
if let ItemKind::Enum(ref def, _) = item.kind {
285291
if !(self.avoid_breaking_exported_api && cx.access_levels.is_exported(item.def_id)) {
286-
check_variant(cx, self.threshold, def, &item_name, item_name_chars, item.span);
292+
check_variant(cx, self.threshold, def, &item_name, item.span);
287293
}
288294
}
289295
self.modules.push((item.ident.name, item_camel));

clippy_utils/src/str_utils.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ pub fn camel_case_until(s: &str) -> StrIndex {
6868
/// ```
6969
#[must_use]
7070
pub fn camel_case_start(s: &str) -> StrIndex {
71+
camel_case_start_from_idx(s, 0)
72+
}
73+
74+
/// Returns `StrIndex` of the last camel-case component of `s[idx..]`.
75+
///
76+
/// ```
77+
/// # use clippy_utils::str_utils::{camel_case_start_from_idx, StrIndex};
78+
/// assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
79+
/// assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
80+
/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
81+
/// assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
82+
/// assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
83+
/// ```
84+
pub fn camel_case_start_from_idx(s: &str, start_idx: usize) -> StrIndex {
7185
let char_count = s.chars().count();
7286
let range = 0..char_count;
7387
let mut iter = range.rev().zip(s.char_indices().rev());
@@ -78,9 +92,13 @@ pub fn camel_case_start(s: &str) -> StrIndex {
7892
} else {
7993
return StrIndex::new(char_count, s.len());
8094
}
95+
8196
let mut down = true;
8297
let mut last_index = StrIndex::new(char_count, s.len());
8398
for (char_index, (byte_index, c)) in iter {
99+
if byte_index < start_idx {
100+
break;
101+
}
84102
if down {
85103
if c.is_uppercase() {
86104
down = false;
@@ -98,9 +116,55 @@ pub fn camel_case_start(s: &str) -> StrIndex {
98116
return last_index;
99117
}
100118
}
119+
101120
last_index
102121
}
103122

123+
/// Get the indexes of camel case components of a string `s`
124+
///
125+
/// ```
126+
/// # use clippy_utils::str_utils::{camel_case_indices, StrIndex};
127+
/// assert_eq!(
128+
/// camel_case_indices("AbcDef"),
129+
/// vec![StrIndex::new(0, 0), StrIndex::new(3, 3), StrIndex::new(6, 6)]
130+
/// );
131+
/// assert_eq!(
132+
/// camel_case_indices("abcDef"),
133+
/// vec![StrIndex::new(3, 3), StrIndex::new(6, 6)]
134+
/// );
135+
/// ```
136+
pub fn camel_case_indices(s: &str) -> Vec<StrIndex> {
137+
let mut result = Vec::new();
138+
let mut str_idx = camel_case_start(s);
139+
140+
while str_idx.byte_index < s.len() {
141+
let next_idx = str_idx.byte_index + 1;
142+
result.push(str_idx);
143+
str_idx = camel_case_start_from_idx(s, next_idx);
144+
}
145+
result.push(str_idx);
146+
147+
result
148+
}
149+
150+
/// Split camel case string into a vector of its components
151+
///
152+
/// ```
153+
/// # use clippy_utils::str_utils::{camel_case_split, StrIndex};
154+
/// assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
155+
/// ```
156+
pub fn camel_case_split(s: &str) -> Vec<&str> {
157+
let mut offsets = camel_case_indices(s)
158+
.iter()
159+
.map(|e| e.byte_index)
160+
.collect::<Vec<usize>>();
161+
if offsets[0] != 0 {
162+
offsets.insert(0, 0);
163+
}
164+
165+
offsets.windows(2).map(|w| &s[w[0]..w[1]]).collect()
166+
}
167+
104168
/// Dealing with sting comparison can be complicated, this struct ensures that both the
105169
/// character and byte count are provided for correct indexing.
106170
#[derive(Debug, Default, PartialEq, Eq)]
@@ -231,4 +295,31 @@ mod test {
231295
fn until_caps() {
232296
assert_eq!(camel_case_until("ABCD"), StrIndex::new(0, 0));
233297
}
298+
299+
#[test]
300+
fn camel_case_start_from_idx_full() {
301+
assert_eq!(camel_case_start_from_idx("AbcDef", 0), StrIndex::new(0, 0));
302+
assert_eq!(camel_case_start_from_idx("AbcDef", 1), StrIndex::new(3, 3));
303+
assert_eq!(camel_case_start_from_idx("AbcDef", 4), StrIndex::new(6, 6));
304+
assert_eq!(camel_case_start_from_idx("AbcDefGhi", 0), StrIndex::new(0, 0));
305+
assert_eq!(camel_case_start_from_idx("AbcDefGhi", 1), StrIndex::new(3, 3));
306+
assert_eq!(camel_case_start_from_idx("Abcdefg", 1), StrIndex::new(7, 7));
307+
}
308+
309+
#[test]
310+
fn camel_case_indices_full() {
311+
assert_eq!(camel_case_indices("Abc\u{f6}\u{f6}DD"), vec![StrIndex::new(7, 9)]);
312+
}
313+
314+
#[test]
315+
fn camel_case_split_full() {
316+
assert_eq!(camel_case_split("A"), vec!["A"]);
317+
assert_eq!(camel_case_split("AbcDef"), vec!["Abc", "Def"]);
318+
assert_eq!(camel_case_split("Abc"), vec!["Abc"]);
319+
assert_eq!(camel_case_split("abcDef"), vec!["abc", "Def"]);
320+
assert_eq!(
321+
camel_case_split("\u{f6}\u{f6}AabABcd"),
322+
vec!["\u{f6}\u{f6}", "Aab", "A", "Bcd"]
323+
);
324+
}
234325
}

tests/ui/enum_variants.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,10 @@ enum HIDataRequest {
145145
DeleteUnpubHIData(String),
146146
}
147147

148+
enum North {
149+
Normal,
150+
NoLeft,
151+
NoRight,
152+
}
153+
148154
fn main() {}

tests/ui/enum_variants.stderr

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ LL | cFoo,
66
|
77
= note: `-D clippy::enum-variant-names` implied by `-D warnings`
88

9+
error: all variants have the same prefix: `c`
10+
--> $DIR/enum_variants.rs:14:1
11+
|
12+
LL | / enum Foo {
13+
LL | | cFoo,
14+
LL | | cBar,
15+
LL | | cBaz,
16+
LL | | }
17+
| |_^
18+
|
19+
= help: remove the prefixes and use full paths to the variants instead of glob imports
20+
921
error: variant name starts with the enum's name
1022
--> $DIR/enum_variants.rs:26:5
1123
|
@@ -60,25 +72,25 @@ LL | | }
6072
|
6173
= help: remove the prefixes and use full paths to the variants instead of glob imports
6274

63-
error: all variants have the same prefix: `WithOut`
64-
--> $DIR/enum_variants.rs:81:1
75+
error: all variants have the same prefix: `C`
76+
--> $DIR/enum_variants.rs:59:1
6577
|
66-
LL | / enum Seallll {
67-
LL | | WithOutCake,
68-
LL | | WithOutTea,
69-
LL | | WithOut,
78+
LL | / enum Something {
79+
LL | | CCall,
80+
LL | | CCreate,
81+
LL | | CCryogenize,
7082
LL | | }
7183
| |_^
7284
|
7385
= help: remove the prefixes and use full paths to the variants instead of glob imports
7486

75-
error: all variants have the same prefix: `Prefix`
76-
--> $DIR/enum_variants.rs:87:1
87+
error: all variants have the same prefix: `WithOut`
88+
--> $DIR/enum_variants.rs:81:1
7789
|
78-
LL | / enum NonCaps {
79-
LL | | Prefix的,
80-
LL | | PrefixTea,
81-
LL | | PrefixCake,
90+
LL | / enum Seallll {
91+
LL | | WithOutCake,
92+
LL | | WithOutTea,
93+
LL | | WithOut,
8294
LL | | }
8395
| |_^
8496
|
@@ -108,5 +120,5 @@ LL | | }
108120
|
109121
= help: remove the postfixes and use full paths to the variants instead of glob imports
110122

111-
error: aborting due to 11 previous errors
123+
error: aborting due to 12 previous errors
112124

0 commit comments

Comments
 (0)