@@ -262,19 +262,11 @@ pub trait Emitter {
262
262
format ! ( "help: {msg}" )
263
263
} else {
264
264
// 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, )
278
270
} ;
279
271
primary_span. push_span_label ( part. span , msg) ;
280
272
@@ -2031,12 +2023,12 @@ impl HumanEmitter {
2031
2023
buffer. append ( 0 , ": " , Style :: HeaderMsg ) ;
2032
2024
2033
2025
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
+ } )
2038
2030
{
2039
- msg. push ( ( " (notice the capitalization difference)" . into ( ) , Style :: NoStyle ) ) ;
2031
+ msg. push ( ( confusion_type . label_text ( ) . into ( ) , Style :: NoStyle ) ) ;
2040
2032
}
2041
2033
self . msgs_to_buffer (
2042
2034
& mut buffer,
@@ -3531,24 +3523,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
3531
3523
}
3532
3524
3533
3525
/// 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 {
3536
3527
let found = match sm. span_to_snippet ( sp) {
3537
3528
Ok ( snippet) => snippet,
3538
3529
Err ( e) => {
3539
3530
warn ! ( error = ?e, "Invalid span {:?}" , sp) ;
3540
- return false ;
3531
+ return ConfusionType :: None ;
3541
3532
}
3542
3533
} ;
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
+ }
3552
3627
}
3553
3628
3554
3629
pub ( crate ) fn should_show_source_code (
0 commit comments