@@ -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