@@ -19,8 +19,9 @@ use rustc_errors::{
1919 Subdiagnostic ,
2020} ;
2121use rustc_session:: errors:: ExprParenthesesNeeded ;
22+ use rustc_span:: edit_distance:: find_best_match_for_name;
2223use rustc_span:: source_map:: Spanned ;
23- use rustc_span:: symbol:: { kw, sym, Ident } ;
24+ use rustc_span:: symbol:: { kw, sym, AllKeywords , Ident } ;
2425use rustc_span:: { BytePos , Span , SpanSnippetError , Symbol , DUMMY_SP } ;
2526use thin_vec:: { thin_vec, ThinVec } ;
2627use tracing:: { debug, trace} ;
@@ -203,6 +204,37 @@ impl std::fmt::Display for UnaryFixity {
203204 }
204205}
205206
207+ #[ derive( Debug , rustc_macros:: Subdiagnostic ) ]
208+ #[ suggestion(
209+ parse_misspelled_kw,
210+ applicability = "machine-applicable" ,
211+ code = "{similar_kw}" ,
212+ style = "verbose"
213+ ) ]
214+ struct MisspelledKw {
215+ similar_kw : String ,
216+ #[ primary_span]
217+ span : Span ,
218+ is_incorrect_case : bool ,
219+ }
220+
221+ /// Checks if the given `lookup` identifier is similar to any keyword symbol in `candidates`.
222+ fn find_similar_kw ( lookup : Ident , candidates : & [ Symbol ] ) -> Option < MisspelledKw > {
223+ let lowercase = lookup. name . as_str ( ) . to_lowercase ( ) ;
224+ let lowercase_sym = Symbol :: intern ( & lowercase) ;
225+ if candidates. contains ( & lowercase_sym) {
226+ Some ( MisspelledKw { similar_kw : lowercase, span : lookup. span , is_incorrect_case : true } )
227+ } else if let Some ( similar_sym) = find_best_match_for_name ( candidates, lookup. name , None ) {
228+ Some ( MisspelledKw {
229+ similar_kw : similar_sym. to_string ( ) ,
230+ span : lookup. span ,
231+ is_incorrect_case : false ,
232+ } )
233+ } else {
234+ None
235+ }
236+ }
237+
206238struct MultiSugg {
207239 msg : String ,
208240 patches : Vec < ( Span , String ) > ,
@@ -638,9 +670,9 @@ impl<'a> Parser<'a> {
638670 let concat = Symbol :: intern ( & format ! ( "{prev}{cur}" ) ) ;
639671 let ident = Ident :: new ( concat, DUMMY_SP ) ;
640672 if ident. is_used_keyword ( ) || ident. is_reserved ( ) || ident. is_raw_guess ( ) {
641- let span = self . prev_token . span . to ( self . token . span ) ;
673+ let concat_span = self . prev_token . span . to ( self . token . span ) ;
642674 err. span_suggestion_verbose (
643- span ,
675+ concat_span ,
644676 format ! ( "consider removing the space to spell keyword `{concat}`" ) ,
645677 concat,
646678 Applicability :: MachineApplicable ,
@@ -741,9 +773,55 @@ impl<'a> Parser<'a> {
741773 err. span_label ( sp, label_exp) ;
742774 err. span_label ( self . token . span , "unexpected token" ) ;
743775 }
776+
777+ // Check for misspelled keywords if there are no suggestions added to the diagnostic.
778+ if err. suggestions . as_ref ( ) . is_ok_and ( |code_suggestions| code_suggestions. is_empty ( ) ) {
779+ self . check_for_misspelled_kw ( & mut err, & expected) ;
780+ }
744781 Err ( err)
745782 }
746783
784+ /// Checks if the current token or the previous token are misspelled keywords
785+ /// and adds a helpful suggestion.
786+ fn check_for_misspelled_kw ( & self , err : & mut Diag < ' _ > , expected : & [ TokenType ] ) {
787+ let Some ( ( curr_ident, _) ) = self . token . ident ( ) else {
788+ return ;
789+ } ;
790+ let expected_tokens: & [ TokenType ] =
791+ expected. len ( ) . checked_sub ( 10 ) . map_or ( & expected, |index| & expected[ index..] ) ;
792+ let expected_keywords: Vec < Symbol > = expected_tokens
793+ . iter ( )
794+ . filter_map ( |token| if let TokenType :: Keyword ( kw) = token { Some ( * kw) } else { None } )
795+ . collect ( ) ;
796+
797+ // When there are a few keywords in the last ten elements of `self.expected_tokens` and the current
798+ // token is an identifier, it's probably a misspelled keyword.
799+ // This handles code like `async Move {}`, misspelled `if` in match guard, misspelled `else` in `if`-`else`
800+ // and mispelled `where` in a where clause.
801+ if !expected_keywords. is_empty ( )
802+ && !curr_ident. is_used_keyword ( )
803+ && let Some ( misspelled_kw) = find_similar_kw ( curr_ident, & expected_keywords)
804+ {
805+ err. subdiagnostic ( misspelled_kw) ;
806+ } else if let Some ( ( prev_ident, _) ) = self . prev_token . ident ( )
807+ && !prev_ident. is_used_keyword ( )
808+ {
809+ // We generate a list of all keywords at runtime rather than at compile time
810+ // so that it gets generated only when the diagnostic needs it.
811+ // Also, it is unlikely that this list is generated multiple times because the
812+ // parser halts after execution hits this path.
813+ let all_keywords = AllKeywords :: new ( ) . collect_used ( || prev_ident. span . edition ( ) ) ;
814+
815+ // Otherwise, check the previous token with all the keywords as possible candidates.
816+ // This handles code like `Struct Human;` and `While a < b {}`.
817+ // We check the previous token only when the current token is an identifier to avoid false
818+ // positives like suggesting keyword `for` for `extern crate foo {}`.
819+ if let Some ( misspelled_kw) = find_similar_kw ( prev_ident, & all_keywords) {
820+ err. subdiagnostic ( misspelled_kw) ;
821+ }
822+ }
823+ }
824+
747825 /// The user has written `#[attr] expr` which is unsupported. (#106020)
748826 pub ( super ) fn attr_on_non_tail_expr ( & self , expr : & Expr ) -> ErrorGuaranteed {
749827 // Missing semicolon typo error.
@@ -846,6 +924,7 @@ impl<'a> Parser<'a> {
846924 ) ;
847925 }
848926 }
927+
849928 err. emit ( )
850929 }
851930
0 commit comments