@@ -63,6 +63,7 @@ pub(crate) fn parse_token_trees<'psess, 'src>(
63
63
cursor,
64
64
override_span,
65
65
nbsp_is_whitespace : false ,
66
+ last_lifetime : None ,
66
67
} ;
67
68
let ( stream, res, unmatched_delims) =
68
69
tokentrees:: TokenTreesReader :: parse_all_token_trees ( string_reader) ;
@@ -105,6 +106,10 @@ struct StringReader<'psess, 'src> {
105
106
/// in this file, it's safe to treat further occurrences of the non-breaking
106
107
/// space character as whitespace.
107
108
nbsp_is_whitespace : bool ,
109
+
110
+ /// Track the `Span` for the leading `'` of the last lifetime. Used for
111
+ /// diagnostics to detect possible typo where `"` was meant.
112
+ last_lifetime : Option < Span > ,
108
113
}
109
114
110
115
impl < ' psess , ' src > StringReader < ' psess , ' src > {
@@ -130,6 +135,23 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
130
135
131
136
debug ! ( "next_token: {:?}({:?})" , token. kind, self . str_from( start) ) ;
132
137
138
+ if let rustc_lexer:: TokenKind :: Semi
139
+ | rustc_lexer:: TokenKind :: LineComment { .. }
140
+ | rustc_lexer:: TokenKind :: BlockComment { .. }
141
+ | rustc_lexer:: TokenKind :: Comma
142
+ | rustc_lexer:: TokenKind :: Dot
143
+ | rustc_lexer:: TokenKind :: OpenParen
144
+ | rustc_lexer:: TokenKind :: CloseParen
145
+ | rustc_lexer:: TokenKind :: OpenBrace
146
+ | rustc_lexer:: TokenKind :: CloseBrace
147
+ | rustc_lexer:: TokenKind :: OpenBracket
148
+ | rustc_lexer:: TokenKind :: CloseBracket = token. kind
149
+ {
150
+ // Heuristic: we assume that it is unlikely we're dealing with an unterminated
151
+ // string surrounded by single quotes.
152
+ self . last_lifetime = None ;
153
+ }
154
+
133
155
// Now "cook" the token, converting the simple `rustc_lexer::TokenKind` enum into a
134
156
// rich `rustc_ast::TokenKind`. This turns strings into interned symbols and runs
135
157
// additional validation.
@@ -247,6 +269,7 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
247
269
// expansion purposes. See #12512 for the gory details of why
248
270
// this is necessary.
249
271
let lifetime_name = self . str_from ( start) ;
272
+ self . last_lifetime = Some ( self . mk_sp ( start, start + BytePos ( 1 ) ) ) ;
250
273
if starts_with_number {
251
274
let span = self . mk_sp ( start, self . pos ) ;
252
275
self . dcx ( ) . struct_err ( "lifetimes cannot start with a number" )
@@ -395,10 +418,21 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
395
418
match kind {
396
419
rustc_lexer:: LiteralKind :: Char { terminated } => {
397
420
if !terminated {
398
- self . dcx ( )
421
+ let mut err = self
422
+ . dcx ( )
399
423
. struct_span_fatal ( self . mk_sp ( start, end) , "unterminated character literal" )
400
- . with_code ( E0762 )
401
- . emit ( )
424
+ . with_code ( E0762 ) ;
425
+ if let Some ( lt_sp) = self . last_lifetime {
426
+ err. multipart_suggestion (
427
+ "if you meant to write a `str` literal, use double quotes" ,
428
+ vec ! [
429
+ ( lt_sp, "\" " . to_string( ) ) ,
430
+ ( self . mk_sp( start, start + BytePos ( 1 ) ) , "\" " . to_string( ) ) ,
431
+ ] ,
432
+ Applicability :: MaybeIncorrect ,
433
+ ) ;
434
+ }
435
+ err. emit ( )
402
436
}
403
437
self . cook_unicode ( token:: Char , Mode :: Char , start, end, 1 , 1 ) // ' '
404
438
}
@@ -673,7 +707,16 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
673
707
let sugg = if prefix == "rb" {
674
708
Some ( errors:: UnknownPrefixSugg :: UseBr ( prefix_span) )
675
709
} else if expn_data. is_root ( ) {
676
- Some ( errors:: UnknownPrefixSugg :: Whitespace ( prefix_span. shrink_to_hi ( ) ) )
710
+ if self . cursor . first ( ) == '\''
711
+ && let Some ( start) = self . last_lifetime
712
+ {
713
+ Some ( errors:: UnknownPrefixSugg :: MeantStr {
714
+ start,
715
+ end : self . mk_sp ( self . pos , self . pos + BytePos ( 1 ) ) ,
716
+ } )
717
+ } else {
718
+ Some ( errors:: UnknownPrefixSugg :: Whitespace ( prefix_span. shrink_to_hi ( ) ) )
719
+ }
677
720
} else {
678
721
None
679
722
} ;
0 commit comments