@@ -63,6 +63,7 @@ pub(crate) fn parse_token_trees<'psess, 'src>(
6363 cursor,
6464 override_span,
6565 nbsp_is_whitespace : false ,
66+ last_lifetime : None ,
6667 } ;
6768 let ( stream, res, unmatched_delims) =
6869 tokentrees:: TokenTreesReader :: parse_all_token_trees ( string_reader) ;
@@ -105,6 +106,10 @@ struct StringReader<'psess, 'src> {
105106 /// in this file, it's safe to treat further occurrences of the non-breaking
106107 /// space character as whitespace.
107108 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 > ,
108113}
109114
110115impl < ' psess , ' src > StringReader < ' psess , ' src > {
@@ -130,6 +135,23 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
130135
131136 debug ! ( "next_token: {:?}({:?})" , token. kind, self . str_from( start) ) ;
132137
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+
133155 // Now "cook" the token, converting the simple `rustc_lexer::TokenKind` enum into a
134156 // rich `rustc_ast::TokenKind`. This turns strings into interned symbols and runs
135157 // additional validation.
@@ -247,6 +269,7 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
247269 // expansion purposes. See #12512 for the gory details of why
248270 // this is necessary.
249271 let lifetime_name = self . str_from ( start) ;
272+ self . last_lifetime = Some ( self . mk_sp ( start, start + BytePos ( 1 ) ) ) ;
250273 if starts_with_number {
251274 let span = self . mk_sp ( start, self . pos ) ;
252275 self . dcx ( ) . struct_err ( "lifetimes cannot start with a number" )
@@ -395,10 +418,21 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
395418 match kind {
396419 rustc_lexer:: LiteralKind :: Char { terminated } => {
397420 if !terminated {
398- self . dcx ( )
421+ let mut err = self
422+ . dcx ( )
399423 . 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 ( )
402436 }
403437 self . cook_unicode ( token:: Char , Mode :: Char , start, end, 1 , 1 ) // ' '
404438 }
@@ -673,7 +707,16 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
673707 let sugg = if prefix == "rb" {
674708 Some ( errors:: UnknownPrefixSugg :: UseBr ( prefix_span) )
675709 } 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+ }
677720 } else {
678721 None
679722 } ;
0 commit comments