@@ -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
@@ -2028,12 +2020,12 @@ impl HumanEmitter {
20282020 buffer. append ( 0 , ": " , Style :: HeaderMsg ) ;
20292021
20302022 let mut msg = vec ! [ ( suggestion. msg. to_owned( ) , Style :: NoStyle ) ] ;
2031- if suggestions
2032- . iter ( )
2033- . take ( MAX_SUGGESTIONS )
2034- . any ( | ( _ , _ , _ , only_capitalization ) | * only_capitalization )
2023+ if let Some ( confusion_type ) =
2024+ suggestions . iter ( ) . take ( MAX_SUGGESTIONS ) . find_map ( | ( _ , _ , _ , confusion_type ) | {
2025+ if confusion_type . has_confusion ( ) { Some ( * confusion_type ) } else { None }
2026+ } )
20352027 {
2036- msg. push ( ( " (notice the capitalization difference)" . into ( ) , Style :: NoStyle ) ) ;
2028+ msg. push ( ( confusion_type . label_text ( ) . into ( ) , Style :: NoStyle ) ) ;
20372029 }
20382030 self . msgs_to_buffer (
20392031 & mut buffer,
@@ -3528,24 +3520,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
35283520}
35293521
35303522/// Whether the original and suggested code are visually similar enough to warrant extra wording.
3531- pub fn is_case_difference ( sm : & SourceMap , suggested : & str , sp : Span ) -> bool {
3532- // FIXME: this should probably be extended to also account for `FO0` → `FOO` and unicode.
3523+ pub fn detect_confusion_type ( sm : & SourceMap , suggested : & str , sp : Span ) -> ConfusionType {
35333524 let found = match sm. span_to_snippet ( sp) {
35343525 Ok ( snippet) => snippet,
35353526 Err ( e) => {
35363527 warn ! ( error = ?e, "Invalid span {:?}" , sp) ;
3537- return false ;
3528+ return ConfusionType :: None ;
35383529 }
35393530 } ;
3540- let ascii_confusables = & [ 'c' , 'f' , 'i' , 'k' , 'o' , 's' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' ] ;
3541- // All the chars that differ in capitalization are confusable (above):
3542- let confusable = iter:: zip ( found. chars ( ) , suggested. chars ( ) )
3543- . filter ( |( f, s) | f != s)
3544- . all ( |( f, s) | ascii_confusables. contains ( & f) || ascii_confusables. contains ( & s) ) ;
3545- confusable && found. to_lowercase ( ) == suggested. to_lowercase ( )
3546- // FIXME: We sometimes suggest the same thing we already have, which is a
3547- // bug, but be defensive against that here.
3548- && found != suggested
3531+
3532+ let mut has_case_confusion = false ;
3533+ let mut has_digit_letter_confusion = false ;
3534+
3535+ if found. len ( ) == suggested. len ( ) {
3536+ let mut has_case_diff = false ;
3537+ let mut has_digit_letter_confusable = false ;
3538+ let mut has_other_diff = false ;
3539+
3540+ let ascii_confusables = & [ 'c' , 'f' , 'i' , 'k' , 'o' , 's' , 'u' , 'v' , 'w' , 'x' , 'y' , 'z' ] ;
3541+
3542+ let digit_letter_confusables = [ ( '0' , 'O' ) , ( '1' , 'l' ) , ( '5' , 'S' ) , ( '8' , 'B' ) , ( '9' , 'g' ) ] ;
3543+
3544+ for ( f, s) in iter:: zip ( found. chars ( ) , suggested. chars ( ) ) {
3545+ if f != s {
3546+ if f. to_lowercase ( ) . to_string ( ) == s. to_lowercase ( ) . to_string ( ) {
3547+ // Check for case differences (any character that differs only in case)
3548+ if ascii_confusables. contains ( & f) || ascii_confusables. contains ( & s) {
3549+ has_case_diff = true ;
3550+ } else {
3551+ has_other_diff = true ;
3552+ }
3553+ } else if digit_letter_confusables. contains ( & ( f, s) )
3554+ || digit_letter_confusables. contains ( & ( s, f) )
3555+ {
3556+ // Check for digit-letter confusables (like 0 vs O, 1 vs l, etc.)
3557+ has_digit_letter_confusable = true ;
3558+ } else {
3559+ has_other_diff = true ;
3560+ }
3561+ }
3562+ }
3563+
3564+ // If we have case differences and no other differences
3565+ if has_case_diff && !has_other_diff && found != suggested {
3566+ has_case_confusion = true ;
3567+ }
3568+ if has_digit_letter_confusable && !has_other_diff && found != suggested {
3569+ has_digit_letter_confusion = true ;
3570+ }
3571+ }
3572+
3573+ match ( has_case_confusion, has_digit_letter_confusion) {
3574+ ( true , true ) => ConfusionType :: Both ,
3575+ ( true , false ) => ConfusionType :: Case ,
3576+ ( false , true ) => ConfusionType :: DigitLetter ,
3577+ ( false , false ) => ConfusionType :: None ,
3578+ }
3579+ }
3580+
3581+ /// Represents the type of confusion detected between original and suggested code.
3582+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
3583+ pub enum ConfusionType {
3584+ /// No confusion detected
3585+ None ,
3586+ /// Only case differences (e.g., "hello" vs "Hello")
3587+ Case ,
3588+ /// Only digit-letter confusion (e.g., "0" vs "O", "1" vs "l")
3589+ DigitLetter ,
3590+ /// Both case and digit-letter confusion
3591+ Both ,
3592+ }
3593+
3594+ impl ConfusionType {
3595+ /// Returns the appropriate label text for this confusion type.
3596+ pub fn label_text ( & self ) -> & ' static str {
3597+ match self {
3598+ ConfusionType :: None => "" ,
3599+ ConfusionType :: Case => " (notice the capitalization)" ,
3600+ ConfusionType :: DigitLetter => " (notice the digit/letter confusion)" ,
3601+ ConfusionType :: Both => " (notice the capitalization and digit/letter confusion)" ,
3602+ }
3603+ }
3604+
3605+ /// Combines two confusion types. If either is `Both`, the result is `Both`.
3606+ /// If one is `Case` and the other is `DigitLetter`, the result is `Both`.
3607+ /// Otherwise, returns the non-`None` type, or `None` if both are `None`.
3608+ pub fn combine ( self , other : ConfusionType ) -> ConfusionType {
3609+ match ( self , other) {
3610+ ( ConfusionType :: None , other) => other,
3611+ ( this, ConfusionType :: None ) => this,
3612+ ( ConfusionType :: Both , _) | ( _, ConfusionType :: Both ) => ConfusionType :: Both ,
3613+ ( ConfusionType :: Case , ConfusionType :: DigitLetter )
3614+ | ( ConfusionType :: DigitLetter , ConfusionType :: Case ) => ConfusionType :: Both ,
3615+ ( ConfusionType :: Case , ConfusionType :: Case ) => ConfusionType :: Case ,
3616+ ( ConfusionType :: DigitLetter , ConfusionType :: DigitLetter ) => ConfusionType :: DigitLetter ,
3617+ }
3618+ }
3619+
3620+ /// Returns true if this confusion type represents any kind of confusion.
3621+ pub fn has_confusion ( & self ) -> bool {
3622+ * self != ConfusionType :: None
3623+ }
35493624}
35503625
35513626pub ( crate ) fn should_show_source_code (
0 commit comments