@@ -262,19 +262,11 @@ pub trait Emitter {
262262 format ! ( "help: {msg}" )
263263 } else {
264264 // Show the default suggestion text with the substitution
265- format ! (
266- "help: {}{}: `{}`" ,
267- msg,
268- if self
269- . source_map( )
270- . is_some_and( |sm| is_case_difference( sm, snippet, part. span, ) )
271- {
272- " (notice the capitalization)"
273- } else {
274- ""
275- } ,
276- snippet,
277- )
265+ let confusion_type = self
266+ . source_map ( )
267+ . map ( |sm| detect_confusion_type ( sm, snippet, part. span ) )
268+ . unwrap_or ( ConfusionType :: None ) ;
269+ format ! ( "help: {}{}: `{}`" , msg, confusion_type. label_text( ) , snippet, )
278270 } ;
279271 primary_span. push_span_label ( part. span , msg) ;
280272
@@ -2031,12 +2023,12 @@ impl HumanEmitter {
20312023 buffer. append ( 0 , ": " , Style :: HeaderMsg ) ;
20322024
20332025 let mut msg = vec ! [ ( suggestion. msg. to_owned( ) , Style :: NoStyle ) ] ;
2034- if suggestions
2035- . iter ( )
2036- . take ( MAX_SUGGESTIONS )
2037- . any ( | ( _ , _ , _ , only_capitalization ) | * only_capitalization )
2026+ if let Some ( confusion_type ) =
2027+ suggestions . iter ( ) . take ( MAX_SUGGESTIONS ) . find_map ( | ( _ , _ , _ , confusion_type ) | {
2028+ if confusion_type . has_confusion ( ) { Some ( * confusion_type ) } else { None }
2029+ } )
20382030 {
2039- msg. push ( ( " (notice the capitalization difference)" . into ( ) , Style :: NoStyle ) ) ;
2031+ msg. push ( ( confusion_type . label_text ( ) . into ( ) , Style :: NoStyle ) ) ;
20402032 }
20412033 self . msgs_to_buffer (
20422034 & mut buffer,
@@ -3531,24 +3523,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
35313523}
35323524
35333525/// Whether the original and suggested code are visually similar enough to warrant extra wording.
3534- pub fn is_case_difference ( sm : & SourceMap , suggested : & str , sp : Span ) -> bool {
3535- // FIXME: this should probably be extended to also account for `FO0` → `FOO` and unicode.
3526+ pub fn detect_confusion_type ( sm : & SourceMap , suggested : & str , sp : Span ) -> ConfusionType {
35363527 let found = match sm. span_to_snippet ( sp) {
35373528 Ok ( snippet) => snippet,
35383529 Err ( e) => {
35393530 warn ! ( error = ?e, "Invalid span {:?}" , sp) ;
3540- return false ;
3531+ return ConfusionType :: None ;
35413532 }
35423533 } ;
3543- let ascii_confusables = & [ 'c' , 'f' , 'i' , 'k' , 'o' , 's' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' ] ;
3544- // All the chars that differ in capitalization are confusable (above):
3545- let confusable = iter:: zip ( found. chars ( ) , suggested. chars ( ) )
3546- . filter ( |( f, s) | f != s)
3547- . all ( |( f, s) | ascii_confusables. contains ( & f) || ascii_confusables. contains ( & s) ) ;
3548- confusable && found. to_lowercase ( ) == suggested. to_lowercase ( )
3549- // FIXME: We sometimes suggest the same thing we already have, which is a
3550- // bug, but be defensive against that here.
3551- && found != suggested
3534+
3535+ let mut has_case_confusion = false ;
3536+ let mut has_digit_letter_confusion = false ;
3537+
3538+ if found. len ( ) == suggested. len ( ) {
3539+ let mut has_case_diff = false ;
3540+ let mut has_digit_letter_confusable = false ;
3541+ let mut has_other_diff = false ;
3542+
3543+ let ascii_confusables = & [ 'c' , 'f' , 'i' , 'k' , 'o' , 's' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' ] ;
3544+
3545+ let digit_letter_confusables = [ ( '0' , 'O' ) , ( '1' , 'l' ) , ( '5' , 'S' ) , ( '8' , 'B' ) , ( '9' , 'g' ) ] ;
3546+
3547+ for ( f, s) in iter:: zip ( found. chars ( ) , suggested. chars ( ) ) {
3548+ if f != s {
3549+ if f. to_lowercase ( ) . to_string ( ) == s. to_lowercase ( ) . to_string ( ) {
3550+ // Check for case differences (any character that differs only in case)
3551+ if ascii_confusables. contains ( & f) || ascii_confusables. contains ( & s) {
3552+ has_case_diff = true ;
3553+ } else {
3554+ has_other_diff = true ;
3555+ }
3556+ } else if digit_letter_confusables. contains ( & ( f, s) )
3557+ || digit_letter_confusables. contains ( & ( s, f) )
3558+ {
3559+ // Check for digit-letter confusables (like 0 vs O, 1 vs l, etc.)
3560+ has_digit_letter_confusable = true ;
3561+ } else {
3562+ has_other_diff = true ;
3563+ }
3564+ }
3565+ }
3566+
3567+ // If we have case differences and no other differences
3568+ if has_case_diff && !has_other_diff && found != suggested {
3569+ has_case_confusion = true ;
3570+ }
3571+ if has_digit_letter_confusable && !has_other_diff && found != suggested {
3572+ has_digit_letter_confusion = true ;
3573+ }
3574+ }
3575+
3576+ match ( has_case_confusion, has_digit_letter_confusion) {
3577+ ( true , true ) => ConfusionType :: Both ,
3578+ ( true , false ) => ConfusionType :: Case ,
3579+ ( false , true ) => ConfusionType :: DigitLetter ,
3580+ ( false , false ) => ConfusionType :: None ,
3581+ }
3582+ }
3583+
3584+ /// Represents the type of confusion detected between original and suggested code.
3585+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
3586+ pub enum ConfusionType {
3587+ /// No confusion detected
3588+ None ,
3589+ /// Only case differences (e.g., "hello" vs "Hello")
3590+ Case ,
3591+ /// Only digit-letter confusion (e.g., "0" vs "O", "1" vs "l")
3592+ DigitLetter ,
3593+ /// Both case and digit-letter confusion
3594+ Both ,
3595+ }
3596+
3597+ impl ConfusionType {
3598+ /// Returns the appropriate label text for this confusion type.
3599+ pub fn label_text ( & self ) -> & ' static str {
3600+ match self {
3601+ ConfusionType :: None => "" ,
3602+ ConfusionType :: Case => " (notice the capitalization)" ,
3603+ ConfusionType :: DigitLetter => " (notice the digit/letter confusion)" ,
3604+ ConfusionType :: Both => " (notice the capitalization and digit/letter confusion)" ,
3605+ }
3606+ }
3607+
3608+ /// Combines two confusion types. If either is `Both`, the result is `Both`.
3609+ /// If one is `Case` and the other is `DigitLetter`, the result is `Both`.
3610+ /// Otherwise, returns the non-`None` type, or `None` if both are `None`.
3611+ pub fn combine ( self , other : ConfusionType ) -> ConfusionType {
3612+ match ( self , other) {
3613+ ( ConfusionType :: None , other) => other,
3614+ ( this, ConfusionType :: None ) => this,
3615+ ( ConfusionType :: Both , _) | ( _, ConfusionType :: Both ) => ConfusionType :: Both ,
3616+ ( ConfusionType :: Case , ConfusionType :: DigitLetter )
3617+ | ( ConfusionType :: DigitLetter , ConfusionType :: Case ) => ConfusionType :: Both ,
3618+ ( ConfusionType :: Case , ConfusionType :: Case ) => ConfusionType :: Case ,
3619+ ( ConfusionType :: DigitLetter , ConfusionType :: DigitLetter ) => ConfusionType :: DigitLetter ,
3620+ }
3621+ }
3622+
3623+ /// Returns true if this confusion type represents any kind of confusion.
3624+ pub fn has_confusion ( & self ) -> bool {
3625+ * self != ConfusionType :: None
3626+ }
35523627}
35533628
35543629pub ( crate ) fn should_show_source_code (
0 commit comments