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