Skip to content

Extend is_case_difference to handle digit-letter confusables #144691

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 1 commit into from
Aug 1, 2025
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
135 changes: 105 additions & 30 deletions compiler/rustc_errors/src/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,19 +262,11 @@ pub trait Emitter {
format!("help: {msg}")
} else {
// Show the default suggestion text with the substitution
format!(
"help: {}{}: `{}`",
msg,
if self
.source_map()
.is_some_and(|sm| is_case_difference(sm, snippet, part.span,))
{
" (notice the capitalization)"
} else {
""
},
snippet,
)
let confusion_type = self
.source_map()
.map(|sm| detect_confusion_type(sm, snippet, part.span))
.unwrap_or(ConfusionType::None);
format!("help: {}{}: `{}`", msg, confusion_type.label_text(), snippet,)
};
primary_span.push_span_label(part.span, msg);

Expand Down Expand Up @@ -2028,12 +2020,12 @@ impl HumanEmitter {
buffer.append(0, ": ", Style::HeaderMsg);

let mut msg = vec![(suggestion.msg.to_owned(), Style::NoStyle)];
if suggestions
.iter()
.take(MAX_SUGGESTIONS)
.any(|(_, _, _, only_capitalization)| *only_capitalization)
if let Some(confusion_type) =
suggestions.iter().take(MAX_SUGGESTIONS).find_map(|(_, _, _, confusion_type)| {
if confusion_type.has_confusion() { Some(*confusion_type) } else { None }
})
{
msg.push((" (notice the capitalization difference)".into(), Style::NoStyle));
msg.push((confusion_type.label_text().into(), Style::NoStyle));
}
self.msgs_to_buffer(
&mut buffer,
Expand Down Expand Up @@ -3528,24 +3520,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
}

/// Whether the original and suggested code are visually similar enough to warrant extra wording.
pub fn is_case_difference(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
// FIXME: this should probably be extended to also account for `FO0` → `FOO` and unicode.
pub fn detect_confusion_type(sm: &SourceMap, suggested: &str, sp: Span) -> ConfusionType {
let found = match sm.span_to_snippet(sp) {
Ok(snippet) => snippet,
Err(e) => {
warn!(error = ?e, "Invalid span {:?}", sp);
return false;
return ConfusionType::None;
}
};
let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z'];
// All the chars that differ in capitalization are confusable (above):
let confusable = iter::zip(found.chars(), suggested.chars())
.filter(|(f, s)| f != s)
.all(|(f, s)| ascii_confusables.contains(&f) || ascii_confusables.contains(&s));
confusable && found.to_lowercase() == suggested.to_lowercase()
// FIXME: We sometimes suggest the same thing we already have, which is a
// bug, but be defensive against that here.
&& found != suggested

let mut has_case_confusion = false;
let mut has_digit_letter_confusion = false;

if found.len() == suggested.len() {
let mut has_case_diff = false;
let mut has_digit_letter_confusable = false;
let mut has_other_diff = false;

let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z'];

let digit_letter_confusables = [('0', 'O'), ('1', 'l'), ('5', 'S'), ('8', 'B'), ('9', 'g')];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep simple and use reverse

Copy link

@roadrunnerto100 roadrunnerto100 Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 1 and capital i?

And lowercase L and capital i?


for (f, s) in iter::zip(found.chars(), suggested.chars()) {
if f != s {
if f.to_lowercase().to_string() == s.to_lowercase().to_string() {
// Check for case differences (any character that differs only in case)
if ascii_confusables.contains(&f) || ascii_confusables.contains(&s) {
has_case_diff = true;
} else {
has_other_diff = true;
}
} else if digit_letter_confusables.contains(&(f, s))
|| digit_letter_confusables.contains(&(s, f))
{
// Check for digit-letter confusables (like 0 vs O, 1 vs l, etc.)
has_digit_letter_confusable = true;
} else {
has_other_diff = true;
}
}
}

// If we have case differences and no other differences
if has_case_diff && !has_other_diff && found != suggested {
has_case_confusion = true;
}
if has_digit_letter_confusable && !has_other_diff && found != suggested {
has_digit_letter_confusion = true;
}
}

match (has_case_confusion, has_digit_letter_confusion) {
(true, true) => ConfusionType::Both,
(true, false) => ConfusionType::Case,
(false, true) => ConfusionType::DigitLetter,
(false, false) => ConfusionType::None,
}
}

/// Represents the type of confusion detected between original and suggested code.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfusionType {
/// No confusion detected
None,
/// Only case differences (e.g., "hello" vs "Hello")
Case,
/// Only digit-letter confusion (e.g., "0" vs "O", "1" vs "l")
DigitLetter,
/// Both case and digit-letter confusion
Both,
}
Comment on lines +3582 to +3592
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I define a enum to handle different cases.


impl ConfusionType {
/// Returns the appropriate label text for this confusion type.
pub fn label_text(&self) -> &'static str {
match self {
ConfusionType::None => "",
ConfusionType::Case => " (notice the capitalization)",
ConfusionType::DigitLetter => " (notice the digit/letter confusion)",
ConfusionType::Both => " (notice the capitalization and digit/letter confusion)",
}
}

/// Combines two confusion types. If either is `Both`, the result is `Both`.
/// If one is `Case` and the other is `DigitLetter`, the result is `Both`.
/// Otherwise, returns the non-`None` type, or `None` if both are `None`.
pub fn combine(self, other: ConfusionType) -> ConfusionType {
match (self, other) {
(ConfusionType::None, other) => other,
(this, ConfusionType::None) => this,
(ConfusionType::Both, _) | (_, ConfusionType::Both) => ConfusionType::Both,
(ConfusionType::Case, ConfusionType::DigitLetter)
| (ConfusionType::DigitLetter, ConfusionType::Case) => ConfusionType::Both,
(ConfusionType::Case, ConfusionType::Case) => ConfusionType::Case,
(ConfusionType::DigitLetter, ConfusionType::DigitLetter) => ConfusionType::DigitLetter,
}
}

/// Returns true if this confusion type represents any kind of confusion.
pub fn has_confusion(&self) -> bool {
*self != ConfusionType::None
}
}

pub(crate) fn should_show_source_code(
Expand Down
11 changes: 6 additions & 5 deletions compiler/rustc_errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub use diagnostic_impls::{
IndicateAnonymousLifetime, SingleLabelManySpans,
};
pub use emitter::ColorConfig;
use emitter::{DynEmitter, Emitter, is_case_difference, is_different};
use emitter::{ConfusionType, DynEmitter, Emitter, detect_confusion_type, is_different};
use rustc_data_structures::AtomicRef;
use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
use rustc_data_structures::stable_hasher::StableHasher;
Expand Down Expand Up @@ -308,7 +308,7 @@ impl CodeSuggestion {
pub(crate) fn splice_lines(
&self,
sm: &SourceMap,
) -> Vec<(String, Vec<SubstitutionPart>, Vec<Vec<SubstitutionHighlight>>, bool)> {
) -> Vec<(String, Vec<SubstitutionPart>, Vec<Vec<SubstitutionHighlight>>, ConfusionType)> {
// For the `Vec<Vec<SubstitutionHighlight>>` value, the first level of the vector
// corresponds to the output snippet's lines, while the second level corresponds to the
// substrings within that line that should be highlighted.
Expand Down Expand Up @@ -414,14 +414,15 @@ impl CodeSuggestion {
// We need to keep track of the difference between the existing code and the added
// or deleted code in order to point at the correct column *after* substitution.
let mut acc = 0;
let mut only_capitalization = false;
let mut confusion_type = ConfusionType::None;
for part in &mut substitution.parts {
// If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
// suggestion and snippet to look as if we just suggested to add
// `"b"`, which is typically much easier for the user to understand.
part.trim_trivial_replacements(sm);

only_capitalization |= is_case_difference(sm, &part.snippet, part.span);
let part_confusion = detect_confusion_type(sm, &part.snippet, part.span);
confusion_type = confusion_type.combine(part_confusion);
let cur_lo = sm.lookup_char_pos(part.span.lo());
if prev_hi.line == cur_lo.line {
let mut count =
Expand Down Expand Up @@ -511,7 +512,7 @@ impl CodeSuggestion {
if highlights.iter().all(|parts| parts.is_empty()) {
None
} else {
Some((buf, substitution.parts, highlights, only_capitalization))
Some((buf, substitution.parts, highlights, confusion_type))
}
})
.collect()
Expand Down
2 changes: 1 addition & 1 deletion src/tools/clippy/tests/ui/match_str_case_mismatch.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ error: this `match` arm has a differing case than its expression
LL | "~!@#$%^&*()-_=+Foo" => {},
| ^^^^^^^^^^^^^^^^^^^^
|
help: consider changing the case of this arm to respect `to_ascii_lowercase` (notice the capitalization difference)
help: consider changing the case of this arm to respect `to_ascii_lowercase` (notice the capitalization)
|
LL - "~!@#$%^&*()-_=+Foo" => {},
LL + "~!@#$%^&*()-_=+foo" => {},
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/error-codes/E0423.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ help: use struct literal syntax instead
LL - let f = Foo();
LL + let f = Foo { a: val };
|
help: a function with a similar name exists (notice the capitalization difference)
help: a function with a similar name exists (notice the capitalization)
|
LL - let f = Foo();
LL + let f = foo();
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/lint/lint-non-uppercase-usages.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ warning: const parameter `foo` should have an upper case name
LL | fn foo<const foo: u32>() {
| ^^^
|
help: convert the identifier to upper case (notice the capitalization difference)
help: convert the identifier to upper case (notice the capitalization)
|
LL - fn foo<const foo: u32>() {
LL + fn foo<const FOO: u32>() {
Expand Down
6 changes: 3 additions & 3 deletions tests/ui/parser/item-kw-case-mismatch.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: keyword `use` is written in the wrong case
LL | Use std::ptr::read;
| ^^^
|
help: write it in the correct case (notice the capitalization difference)
help: write it in the correct case (notice the capitalization)
|
LL - Use std::ptr::read;
LL + use std::ptr::read;
Expand All @@ -28,7 +28,7 @@ error: keyword `fn` is written in the wrong case
LL | async Fn _a() {}
| ^^
|
help: write it in the correct case (notice the capitalization difference)
help: write it in the correct case (notice the capitalization)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove difference to keep consistency.

|
LL - async Fn _a() {}
LL + async fn _a() {}
Expand All @@ -40,7 +40,7 @@ error: keyword `fn` is written in the wrong case
LL | Fn _b() {}
| ^^
|
help: write it in the correct case (notice the capitalization difference)
help: write it in the correct case (notice the capitalization)
|
LL - Fn _b() {}
LL + fn _b() {}
Expand Down
8 changes: 4 additions & 4 deletions tests/ui/parser/kw-in-trait-bounds.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected identifier, found keyword `fn`
LL | fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
LL + fn _f<F: Fn(), G>(_: impl fn(), _: &dyn fn())
Expand All @@ -16,7 +16,7 @@ error: expected identifier, found keyword `fn`
LL | fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
LL + fn _f<F: fn(), G>(_: impl Fn(), _: &dyn fn())
Expand All @@ -28,7 +28,7 @@ error: expected identifier, found keyword `fn`
LL | fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - fn _f<F: fn(), G>(_: impl fn(), _: &dyn fn())
LL + fn _f<F: fn(), G>(_: impl fn(), _: &dyn Fn())
Expand All @@ -40,7 +40,7 @@ error: expected identifier, found keyword `fn`
LL | G: fn(),
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - G: fn(),
LL + G: Fn(),
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/parser/misspelled-keywords/hrdt.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected one of `!`, `(`, `+`, `::`, `<`, `where`, or `{`, found keyword
LL | Where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
| ^^^ expected one of 7 possible tokens
|
help: write keyword `where` in lowercase (notice the capitalization difference)
help: write keyword `where` in lowercase (notice the capitalization)
|
LL - Where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
LL + where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/parser/misspelled-keywords/impl-return.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected one of `!`, `(`, `+`, `::`, `<`, `where`, or `{`, found `Display
LL | fn code() -> Impl Display {}
| ^^^^^^^ expected one of 7 possible tokens
|
help: write keyword `impl` in lowercase (notice the capitalization difference)
help: write keyword `impl` in lowercase (notice the capitalization)
|
LL - fn code() -> Impl Display {}
LL + fn code() -> impl Display {}
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/parser/misspelled-keywords/static.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected one of `!` or `::`, found `a`
LL | Static a = 0;
| ^ expected one of `!` or `::`
|
help: write keyword `static` in lowercase (notice the capitalization difference)
help: write keyword `static` in lowercase (notice the capitalization)
|
LL - Static a = 0;
LL + static a = 0;
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/parser/misspelled-keywords/struct.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected one of `!` or `::`, found `Foor`
LL | Struct Foor {
| ^^^^ expected one of `!` or `::`
|
help: write keyword `struct` in lowercase (notice the capitalization difference)
help: write keyword `struct` in lowercase (notice the capitalization)
|
LL - Struct Foor {
LL + struct Foor {
Expand Down
4 changes: 2 additions & 2 deletions tests/ui/parser/recover/recover-fn-trait-from-fn-kw.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: expected identifier, found keyword `fn`
LL | fn foo(_: impl fn() -> i32) {}
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - fn foo(_: impl fn() -> i32) {}
LL + fn foo(_: impl Fn() -> i32) {}
Expand All @@ -16,7 +16,7 @@ error: expected identifier, found keyword `fn`
LL | fn foo2<T: fn(i32)>(_: T) {}
| ^^
|
help: use `Fn` to refer to the trait (notice the capitalization difference)
help: use `Fn` to refer to the trait (notice the capitalization)
|
LL - fn foo2<T: fn(i32)>(_: T) {}
LL + fn foo2<T: Fn(i32)>(_: T) {}
Expand Down
8 changes: 4 additions & 4 deletions tests/ui/parser/typod-const-in-const-param-def.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ error: `const` keyword was mistyped as `Const`
LL | pub fn foo<Const N: u8>() {}
| ^^^^^
|
help: use the `const` keyword (notice the capitalization difference)
help: use the `const` keyword (notice the capitalization)
|
LL - pub fn foo<Const N: u8>() {}
LL + pub fn foo<const N: u8>() {}
Expand All @@ -16,7 +16,7 @@ error: `const` keyword was mistyped as `Const`
LL | pub fn baz<Const N: u8, T>() {}
| ^^^^^
|
help: use the `const` keyword (notice the capitalization difference)
help: use the `const` keyword (notice the capitalization)
|
LL - pub fn baz<Const N: u8, T>() {}
LL + pub fn baz<const N: u8, T>() {}
Expand All @@ -28,7 +28,7 @@ error: `const` keyword was mistyped as `Const`
LL | pub fn qux<T, Const N: u8>() {}
| ^^^^^
|
help: use the `const` keyword (notice the capitalization difference)
help: use the `const` keyword (notice the capitalization)
|
LL - pub fn qux<T, Const N: u8>() {}
LL + pub fn qux<T, const N: u8>() {}
Expand All @@ -40,7 +40,7 @@ error: `const` keyword was mistyped as `Const`
LL | pub fn quux<T, Const N: u8, U>() {}
| ^^^^^
|
help: use the `const` keyword (notice the capitalization difference)
help: use the `const` keyword (notice the capitalization)
|
LL - pub fn quux<T, Const N: u8, U>() {}
LL + pub fn quux<T, const N: u8, U>() {}
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/suggestions/assoc-ct-for-assoc-method.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ LL | let x: i32 = MyS::foo;
|
= note: expected type `i32`
found fn item `fn() -> MyS {MyS::foo}`
help: try referring to the associated const `FOO` instead (notice the capitalization difference)
help: try referring to the associated const `FOO` instead (notice the capitalization)
|
LL - let x: i32 = MyS::foo;
LL + let x: i32 = MyS::FOO;
Expand Down
Loading
Loading