Skip to content

Commit fc1339b

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 rust-lang#119685.
1 parent 46b180e commit fc1339b

13 files changed

+130
-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

+42-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,18 @@ 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::CloseParen
142+
| rustc_lexer::TokenKind::CloseBrace
143+
| rustc_lexer::TokenKind::CloseBracket = token.kind
144+
{
145+
// Heuristic: we assume that it is unlikely we're dealing with an unterminated
146+
// string surrounded by single quotes.
147+
self.last_lifetime = None;
148+
}
149+
133150
// Now "cook" the token, converting the simple `rustc_lexer::TokenKind` enum into a
134151
// rich `rustc_ast::TokenKind`. This turns strings into interned symbols and runs
135152
// additional validation.
@@ -247,6 +264,7 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
247264
// expansion purposes. See #12512 for the gory details of why
248265
// this is necessary.
249266
let lifetime_name = self.str_from(start);
267+
self.last_lifetime = Some(self.mk_sp(start, start + BytePos(1)));
250268
if starts_with_number {
251269
let span = self.mk_sp(start, self.pos);
252270
self.dcx().struct_err("lifetimes cannot start with a number")
@@ -395,10 +413,21 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
395413
match kind {
396414
rustc_lexer::LiteralKind::Char { terminated } => {
397415
if !terminated {
398-
self.dcx()
416+
let mut err = self
417+
.dcx()
399418
.struct_span_fatal(self.mk_sp(start, end), "unterminated character literal")
400-
.with_code(E0762)
401-
.emit()
419+
.with_code(E0762);
420+
if let Some(lt_sp) = self.last_lifetime {
421+
err.multipart_suggestion(
422+
"if you meant to write a `str` literal, use double quotes",
423+
vec![
424+
(lt_sp, "\"".to_string()),
425+
(self.mk_sp(start, start + BytePos(1)), "\"".to_string()),
426+
],
427+
Applicability::MaybeIncorrect,
428+
);
429+
}
430+
err.emit()
402431
}
403432
self.cook_unicode(token::Char, Mode::Char, start, end, 1, 1) // ' '
404433
}
@@ -673,7 +702,16 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
673702
let sugg = if prefix == "rb" {
674703
Some(errors::UnknownPrefixSugg::UseBr(prefix_span))
675704
} else if expn_data.is_root() {
676-
Some(errors::UnknownPrefixSugg::Whitespace(prefix_span.shrink_to_hi()))
705+
if self.cursor.first() == '\''
706+
&& let Some(start) = self.last_lifetime
707+
{
708+
Some(errors::UnknownPrefixSugg::MeantStr {
709+
start,
710+
end: self.mk_sp(self.pos, self.pos + BytePos(1)),
711+
})
712+
} else {
713+
Some(errors::UnknownPrefixSugg::Whitespace(prefix_span.shrink_to_hi()))
714+
}
677715
} else {
678716
None
679717
};
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)