Skip to content

Commit b4de21f

Browse files
committed
Handle str literals written with ' lexed as lifetime
Given `'hello world'` and `'1 str', provide a structured suggestion for a valid string literal: ``` error[E0762]: unterminated character literal --> $DIR/lex-bad-str-literal-as-char-3.rs:2:26 | LL | println!('hello world'); | ^^^^ | help: if you meant to write a `str` literal, use double quotes | LL | println!("hello world"); | ~ ~ ``` ``` error[E0762]: unterminated character literal --> $DIR/lex-bad-str-literal-as-char-1.rs:2:20 | LL | println!('1 + 1'); | ^^^^ | help: if you meant to write a `str` literal, use double quotes | LL | println!("1 + 1"); | ~ ~ ``` Fix #119685.
1 parent 46b180e commit b4de21f

13 files changed

+135
-5
lines changed

compiler/rustc_lexer/src/cursor.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ impl<'a> Cursor<'a> {
4646
/// If requested position doesn't exist, `EOF_CHAR` is returned.
4747
/// However, getting `EOF_CHAR` doesn't always mean actual end of file,
4848
/// it should be checked with `is_eof` method.
49-
pub(crate) fn first(&self) -> char {
49+
pub fn first(&self) -> char {
5050
// `.next()` optimizes better than `.nth(0)`
5151
self.chars.clone().next().unwrap_or(EOF_CHAR)
5252
}

compiler/rustc_parse/messages.ftl

+1
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ parse_unknown_prefix = prefix `{$prefix}` is unknown
839839
.label = unknown prefix
840840
.note = prefixed identifiers and literals are reserved since Rust 2021
841841
.suggestion_br = use `br` for a raw byte string
842+
.suggestion_str = if you meant to write a `str` literal, use double quotes
842843
.suggestion_whitespace = consider inserting whitespace here
843844
844845
parse_unknown_start_of_token = unknown start of token: {$escaped}

compiler/rustc_parse/src/errors.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,17 @@ pub enum UnknownPrefixSugg {
19941994
style = "verbose"
19951995
)]
19961996
Whitespace(#[primary_span] Span),
1997+
#[multipart_suggestion(
1998+
parse_suggestion_str,
1999+
applicability = "maybe-incorrect",
2000+
style = "verbose"
2001+
)]
2002+
MeantStr {
2003+
#[suggestion_part(code = "\"")]
2004+
start: Span,
2005+
#[suggestion_part(code = "\"")]
2006+
end: Span,
2007+
},
19972008
}
19982009

19992010
#[derive(Diagnostic)]

compiler/rustc_parse/src/lexer/mod.rs

+47-4
Original file line numberDiff line numberDiff line change
@@ -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

110115
impl<'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
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!("1 + 1");
4+
//~^ ERROR unterminated character literal
5+
//~| ERROR lifetimes cannot start with a number
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!('1 + 1');
4+
//~^ ERROR unterminated character literal
5+
//~| ERROR lifetimes cannot start with a number
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
error[E0762]: unterminated character literal
2+
--> $DIR/lex-bad-str-literal-as-char-1.rs:3:20
3+
|
4+
LL | println!('1 + 1');
5+
| ^^^
6+
|
7+
help: if you meant to write a `str` literal, use double quotes
8+
|
9+
LL | println!("1 + 1");
10+
| ~ ~
11+
12+
error: lifetimes cannot start with a number
13+
--> $DIR/lex-bad-str-literal-as-char-1.rs:3:14
14+
|
15+
LL | println!('1 + 1');
16+
| ^^
17+
18+
error: aborting due to 2 previous errors
19+
20+
For more information about this error, try `rustc --explain E0762`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!(" 1 + 1"); //~ ERROR character literal may only contain one codepoint
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!(' 1 + 1'); //~ ERROR character literal may only contain one codepoint
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
error: character literal may only contain one codepoint
2+
--> $DIR/lex-bad-str-literal-as-char-2.rs:3:14
3+
|
4+
LL | println!(' 1 + 1');
5+
| ^^^^^^^^
6+
|
7+
help: if you meant to write a `str` literal, use double quotes
8+
|
9+
LL | println!(" 1 + 1");
10+
| ~~~~~~~~
11+
12+
error: aborting due to 1 previous error
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!("hello world"); //~ ERROR unterminated character literal
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//@ run-rustfix
2+
fn main() {
3+
println!('hello world'); //~ ERROR unterminated character literal
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error[E0762]: unterminated character literal
2+
--> $DIR/lex-bad-str-literal-as-char-3.rs:3:26
3+
|
4+
LL | println!('hello world');
5+
| ^^^^
6+
|
7+
help: if you meant to write a `str` literal, use double quotes
8+
|
9+
LL | println!("hello world");
10+
| ~ ~
11+
12+
error: aborting due to 1 previous error
13+
14+
For more information about this error, try `rustc --explain E0762`.

0 commit comments

Comments
 (0)