@@ -19,8 +19,9 @@ use rustc_errors::{
19
19
Subdiagnostic ,
20
20
} ;
21
21
use rustc_session:: errors:: ExprParenthesesNeeded ;
22
+ use rustc_span:: edit_distance:: find_best_match_for_name;
22
23
use rustc_span:: source_map:: Spanned ;
23
- use rustc_span:: symbol:: { kw, sym, Ident } ;
24
+ use rustc_span:: symbol:: { kw, sym, AllKeywords , Ident } ;
24
25
use rustc_span:: { BytePos , Span , SpanSnippetError , Symbol , DUMMY_SP } ;
25
26
use thin_vec:: { thin_vec, ThinVec } ;
26
27
use tracing:: { debug, trace} ;
@@ -203,6 +204,37 @@ impl std::fmt::Display for UnaryFixity {
203
204
}
204
205
}
205
206
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
+
206
238
struct MultiSugg {
207
239
msg : String ,
208
240
patches : Vec < ( Span , String ) > ,
@@ -638,9 +670,9 @@ impl<'a> Parser<'a> {
638
670
let concat = Symbol :: intern ( & format ! ( "{prev}{cur}" ) ) ;
639
671
let ident = Ident :: new ( concat, DUMMY_SP ) ;
640
672
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 ) ;
642
674
err. span_suggestion_verbose (
643
- span ,
675
+ concat_span ,
644
676
format ! ( "consider removing the space to spell keyword `{concat}`" ) ,
645
677
concat,
646
678
Applicability :: MachineApplicable ,
@@ -741,9 +773,55 @@ impl<'a> Parser<'a> {
741
773
err. span_label ( sp, label_exp) ;
742
774
err. span_label ( self . token . span , "unexpected token" ) ;
743
775
}
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
+ }
744
781
Err ( err)
745
782
}
746
783
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
+
747
825
/// The user has written `#[attr] expr` which is unsupported. (#106020)
748
826
pub ( super ) fn attr_on_non_tail_expr ( & self , expr : & Expr ) -> ErrorGuaranteed {
749
827
// Missing semicolon typo error.
@@ -846,6 +924,7 @@ impl<'a> Parser<'a> {
846
924
) ;
847
925
}
848
926
}
927
+
849
928
err. emit ( )
850
929
}
851
930
0 commit comments