Skip to content

Commit 7517ae2

Browse files
committed
Refactored some string handling to prevent ICEs and FNs
1 parent d38fddd commit 7517ae2

File tree

6 files changed

+106
-33
lines changed

6 files changed

+106
-33
lines changed

clippy_lints/src/enum_variants.rs

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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;
5+
use clippy_utils::str_utils::{self, count_match_end, count_match_start};
66
use rustc_hir::{EnumDef, Item, ItemKind};
77
use rustc_lint::{LateContext, LateLintPass};
88
use rustc_session::{declare_tool_lint, impl_lint_pass};
@@ -117,26 +117,6 @@ impl_lint_pass!(EnumVariantNames => [
117117
MODULE_INCEPTION
118118
]);
119119

120-
/// Returns the number of chars that match from the start
121-
#[must_use]
122-
fn partial_match(pre: &str, name: &str) -> usize {
123-
let mut name_iter = name.chars();
124-
let _ = name_iter.next_back(); // make sure the name is never fully matched
125-
pre.chars().zip(name_iter).take_while(|&(l, r)| l == r).count()
126-
}
127-
128-
/// Returns the number of chars that match from the end
129-
#[must_use]
130-
fn partial_rmatch(post: &str, name: &str) -> usize {
131-
let mut name_iter = name.chars();
132-
let _ = name_iter.next(); // make sure the name is never fully matched
133-
post.chars()
134-
.rev()
135-
.zip(name_iter.rev())
136-
.take_while(|&(l, r)| l == r)
137-
.count()
138-
}
139-
140120
fn check_variant(
141121
cx: &LateContext<'_>,
142122
threshold: u64,
@@ -150,7 +130,7 @@ fn check_variant(
150130
}
151131
for var in def.variants {
152132
let name = var.ident.name.as_str();
153-
if partial_match(item_name, &name) == item_name_chars
133+
if count_match_start(item_name, &name).char_count == item_name_chars
154134
&& name.chars().nth(item_name_chars).map_or(false, |c| !c.is_lowercase())
155135
&& name.chars().nth(item_name_chars + 1).map_or(false, |c| !c.is_numeric())
156136
{
@@ -161,7 +141,7 @@ fn check_variant(
161141
"variant name starts with the enum's name",
162142
);
163143
}
164-
if partial_rmatch(item_name, &name) == item_name_chars {
144+
if count_match_end(item_name, &name).char_count == item_name_chars {
165145
span_lint(
166146
cx,
167147
ENUM_VARIANT_NAMES,
@@ -176,7 +156,7 @@ fn check_variant(
176156
for var in def.variants {
177157
let name = var.ident.name.as_str();
178158

179-
let pre_match = partial_match(pre, &name);
159+
let pre_match = count_match_start(pre, &name).byte_count;
180160
pre = &pre[..pre_match];
181161
let pre_camel = str_utils::camel_case_until(pre).byte_index;
182162
pre = &pre[..pre_camel];
@@ -193,8 +173,8 @@ fn check_variant(
193173
}
194174
}
195175

196-
let post_match = partial_rmatch(post, &name);
197-
let post_end = post.len() - post_match;
176+
let post_match = count_match_end(post, &name);
177+
let post_end = post.len() - post_match.byte_count;
198178
post = &post[post_end..];
199179
let post_camel = str_utils::camel_case_start(post);
200180
post = &post[post_camel.byte_index..];
@@ -266,14 +246,16 @@ impl LateLintPass<'_> for EnumVariantNames {
266246
);
267247
}
268248
}
269-
if item.vis.node.is_pub() {
270-
let matching = partial_match(mod_camel, &item_camel);
271-
let rmatching = partial_rmatch(mod_camel, &item_camel);
249+
// The `module_name_repetitions` lint should only trigger if the item has the module in its
250+
// name. Having the same name is accepted.
251+
if item.vis.node.is_pub() && item_camel.len() > mod_camel.len() {
252+
let matching = count_match_start(mod_camel, &item_camel);
253+
let rmatching = count_match_end(mod_camel, &item_camel);
272254
let nchars = mod_camel.chars().count();
273255

274256
let is_word_beginning = |c: char| c == '_' || c.is_uppercase() || c.is_numeric();
275257

276-
if matching == nchars {
258+
if matching.char_count == nchars {
277259
match item_camel.chars().nth(nchars) {
278260
Some(c) if is_word_beginning(c) => span_lint(
279261
cx,
@@ -284,7 +266,7 @@ impl LateLintPass<'_> for EnumVariantNames {
284266
_ => (),
285267
}
286268
}
287-
if rmatching == nchars {
269+
if rmatching.char_count == nchars {
288270
span_lint(
289271
cx,
290272
MODULE_NAME_REPETITIONS,

clippy_utils/src/str_utils.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,75 @@ pub fn camel_case_start(s: &str) -> StrIndex {
9999
last_index
100100
}
101101

102+
/// Dealing with sting comparison can be complicated, this struct ensures that both the
103+
/// character and byte count are provided for correct indexing.
104+
#[derive(Debug, Default, PartialEq, Eq)]
105+
pub struct StrCount {
106+
pub char_count: usize,
107+
pub byte_count: usize,
108+
}
109+
110+
impl StrCount {
111+
pub fn new(char_count: usize, byte_count: usize) -> Self {
112+
Self { char_count, byte_count }
113+
}
114+
}
115+
116+
/// Returns the number of chars that match from the start
117+
///
118+
/// ```
119+
/// assert_eq!(count_match_start("hello_mouse", "hello_penguin"), StrCount::new(6, 6));
120+
/// assert_eq!(count_match_start("hello_clippy", "bye_bugs"), StrCount::new(0, 0));
121+
/// assert_eq!(count_match_start("hello_world", "hello_world"), StrCount::new(11, 11));
122+
/// assert_eq!(count_match_start("T\u{f6}ffT\u{f6}ff", "T\u{f6}ff"), StrCount::new(4, 5));
123+
/// ```
124+
#[must_use]
125+
pub fn count_match_start(str1: &str, str2: &str) -> StrCount {
126+
// (char_index, char1)
127+
let char_count = str1.chars().count();
128+
let iter1 = (0..=char_count).zip(str1.chars());
129+
// (byte_index, char2)
130+
let iter2 = str2.char_indices();
131+
132+
iter1
133+
.zip(iter2)
134+
.take_while(|((_, c1), (_, c2))| c1 == c2)
135+
.last()
136+
.map_or_else(StrCount::default, |((char_index, _), (byte_index, character))| {
137+
StrCount::new(char_index + 1, byte_index + character.len_utf8())
138+
})
139+
}
140+
141+
/// Returns the number of chars and bytes that match from the end
142+
///
143+
/// ```
144+
/// assert_eq!(count_match_end("hello_cat", "bye_cat"), StrCount::new(4, 4));
145+
/// assert_eq!(count_match_end("if_item_thing", "enum_value"), StrCount::new(0, 0));
146+
/// assert_eq!(count_match_end("Clippy", "Clippy"), StrCount::new(6, 6));
147+
/// assert_eq!(count_match_end("MyT\u{f6}ff", "YourT\u{f6}ff"), StrCount::new(4, 5));
148+
/// ```
149+
#[must_use]
150+
pub fn count_match_end(str1: &str, str2: &str) -> StrCount {
151+
let char_count = str1.chars().count();
152+
if char_count == 0 {
153+
return StrCount::default();
154+
}
155+
156+
// (char_index, char1)
157+
let iter1 = (0..char_count).rev().zip(str1.chars().rev());
158+
// (byte_index, char2)
159+
let byte_count = str2.len();
160+
let iter2 = str2.char_indices().rev();
161+
162+
iter1
163+
.zip(iter2)
164+
.take_while(|((_, c1), (_, c2))| c1 == c2)
165+
.last()
166+
.map_or_else(StrCount::default, |((char_index, _), (byte_index, _))| {
167+
StrCount::new(char_count - char_index, byte_count - byte_index)
168+
})
169+
}
170+
102171
#[cfg(test)]
103172
mod test {
104173
use super::*;

tests/ui/crashes/ice-7869.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
enum Tila {
2+
TyöAlkoi,
3+
TyöKeskeytyi,
4+
TyöValmis,
5+
}
6+
7+
fn main() {}

tests/ui/crashes/ice-7869.stderr

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: all variants have the same prefix: `Työ`
2+
--> $DIR/ice-7869.rs:1:1
3+
|
4+
LL | / enum Tila {
5+
LL | | TyöAlkoi,
6+
LL | | TyöKeskeytyi,
7+
LL | | TyöValmis,
8+
LL | | }
9+
| |_^
10+
|
11+
= note: `-D clippy::enum-variant-names` implied by `-D warnings`
12+
= help: remove the prefixes and use full paths to the variants instead of glob imports
13+
14+
error: aborting due to previous error
15+

tests/ui/enum_variants.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ LL | | }
6060
|
6161
= help: remove the prefixes and use full paths to the variants instead of glob imports
6262

63-
error: all variants have the same prefix: `With`
63+
error: all variants have the same prefix: `WithOut`
6464
--> $DIR/enum_variants.rs:81:1
6565
|
6666
LL | / enum Seallll {

tests/ui/match_ref_pats.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![warn(clippy::match_ref_pats)]
2-
#![allow(clippy::equatable_if_let)]
2+
#![allow(clippy::equatable_if_let, clippy::enum_variant_names)]
33

44
fn ref_pats() {
55
{

0 commit comments

Comments
 (0)