Skip to content

Commit df49197

Browse files
authored
perf: bail out of unicode lint scans when the snippet is pure ASCII (#17273)
The Unicode pass ran three per-character scans and two NFC normalizations for every string and char literal. All three lints can only fire when the source snippet contains a non-ASCII character, so bail out early with a vectorized `is_ascii` check on the snippet. | crate | instructions base | instructions new | delta | |---|---|---|---| | ripgrep-14.1.0 | 1,906,999,867 | 1,887,953,975 | -1.00% | | regex-1.10.5 | 955,616,270 | 955,487,288 | -0.01% | | serde-1.0.204 | 7,034,722,908 | 7,033,028,960 | -0.02% | | ryu-1.0.18 | 260,641,521 | 260,533,447 | -0.04% | changelog: none
2 parents 4d7caca + 99a5589 commit df49197

4 files changed

Lines changed: 89 additions & 1 deletion

File tree

clippy_lints/src/unicode.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ fn check_str(cx: &LateContext<'_>, span: Span, id: HirId) {
108108
}
109109

110110
let string = snippet(cx, span, "");
111+
// All three lints here can only fire when the snippet contains a non-ASCII character. Note
112+
// that the snippet can contain non-ASCII characters even when the literal's value does not,
113+
// e.g. the literal produced by `file!()` inside a macro spans the whole macro invocation.
114+
if string.is_ascii() {
115+
return;
116+
}
111117
let is_raw = string.starts_with('r');
112118

113119
if string.chars().any(|c| ['\u{200B}', '\u{ad}', '\u{2060}'].contains(&c)) {

tests/ui/unicode.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,39 @@ mod non_ascii_literal {
6060
}
6161
}
6262

63+
mod ascii_macro_with_non_ascii_value {
64+
// The source is pure ASCII, but the value contains non-ASCII, invisible, and
65+
// non-NFC characters. The lints check the source snippet, not the value, so
66+
// none of them may fire here.
67+
#![deny(clippy::invisible_characters, clippy::non_ascii_literal, clippy::unicode_not_nfc)]
68+
69+
macro_rules! non_ascii_value {
70+
() => {
71+
"\u{00E9}\u{200B}a\u{0300}"
72+
};
73+
}
74+
75+
fn no_lint() {
76+
let _ = non_ascii_value!();
77+
}
78+
}
79+
80+
mod ascii_value_from_non_ascii_snippet {
81+
// The reverse: `file!()` produces an ASCII value (the file path), but the
82+
// literal's span covers the whole macro invocation, so the snippet is
83+
// non-ASCII and the lint must fire. Checking the value would miss it.
84+
#![deny(clippy::non_ascii_literal)]
85+
86+
macro_rules! with_location {
87+
($($arg:tt)*) => {
88+
let _ = file!();
89+
};
90+
}
91+
92+
fn lint() {
93+
with_location!("h\u{e9}llo");
94+
//~^ non_ascii_literal
95+
}
96+
}
97+
6398
fn main() {}

tests/ui/unicode.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,39 @@ mod non_ascii_literal {
6060
}
6161
}
6262

63+
mod ascii_macro_with_non_ascii_value {
64+
// The source is pure ASCII, but the value contains non-ASCII, invisible, and
65+
// non-NFC characters. The lints check the source snippet, not the value, so
66+
// none of them may fire here.
67+
#![deny(clippy::invisible_characters, clippy::non_ascii_literal, clippy::unicode_not_nfc)]
68+
69+
macro_rules! non_ascii_value {
70+
() => {
71+
"\u{00E9}\u{200B}a\u{0300}"
72+
};
73+
}
74+
75+
fn no_lint() {
76+
let _ = non_ascii_value!();
77+
}
78+
}
79+
80+
mod ascii_value_from_non_ascii_snippet {
81+
// The reverse: `file!()` produces an ASCII value (the file path), but the
82+
// literal's span covers the whole macro invocation, so the snippet is
83+
// non-ASCII and the lint must fire. Checking the value would miss it.
84+
#![deny(clippy::non_ascii_literal)]
85+
86+
macro_rules! with_location {
87+
($($arg:tt)*) => {
88+
let _ = file!();
89+
};
90+
}
91+
92+
fn lint() {
93+
with_location!("héllo");
94+
//~^ non_ascii_literal
95+
}
96+
}
97+
6398
fn main() {}

tests/ui/unicode.stderr

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,17 @@ note: the lint level is defined here
6464
LL | #![deny(clippy::non_ascii_literal)]
6565
| ^^^^^^^^^^^^^^^^^^^^^^^^^
6666

67-
error: aborting due to 8 previous errors
67+
error: literal non-ASCII character detected
68+
--> tests/ui/unicode.rs:93:9
69+
|
70+
LL | with_location!("héllo");
71+
| ^^^^^^^^^^^^^^^^^^^^^^^ help: consider replacing the string with: `with_location!("h\u{e9}llo")`
72+
|
73+
note: the lint level is defined here
74+
--> tests/ui/unicode.rs:84:13
75+
|
76+
LL | #![deny(clippy::non_ascii_literal)]
77+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
78+
79+
error: aborting due to 9 previous errors
6880

0 commit comments

Comments
 (0)