@@ -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
@@ -2028,12 +2020,12 @@ impl HumanEmitter {
2028
2020
buffer. append ( 0 , ": " , Style :: HeaderMsg ) ;
2029
2021
2030
2022
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
+ } )
2035
2027
{
2036
- msg. push ( ( " (notice the capitalization difference)" . into ( ) , Style :: NoStyle ) ) ;
2028
+ msg. push ( ( confusion_type . label_text ( ) . into ( ) , Style :: NoStyle ) ) ;
2037
2029
}
2038
2030
self . msgs_to_buffer (
2039
2031
& mut buffer,
@@ -3528,24 +3520,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
3528
3520
}
3529
3521
3530
3522
/// 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 {
3533
3524
let found = match sm. span_to_snippet ( sp) {
3534
3525
Ok ( snippet) => snippet,
3535
3526
Err ( e) => {
3536
3527
warn ! ( error = ?e, "Invalid span {:?}" , sp) ;
3537
- return false ;
3528
+ return ConfusionType :: None ;
3538
3529
}
3539
3530
} ;
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
+ }
3549
3624
}
3550
3625
3551
3626
pub ( crate ) fn should_show_source_code (
0 commit comments