Skip to content

Commit 1164c27

Browse files
authored
Rollup merge of #122217 - estebank:issue-119685, r=fmease
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.
2 parents 3d9ee88 + f4d30b1 commit 1164c27

30 files changed

+250
-70
lines changed

Diff for: compiler/rustc_infer/messages.ftl

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ infer_lifetime_param_suggestion_elided = each elided lifetime in input position
169169
170170
infer_meant_byte_literal = if you meant to write a byte literal, prefix with `b`
171171
infer_meant_char_literal = if you meant to write a `char` literal, use single quotes
172-
infer_meant_str_literal = if you meant to write a `str` literal, use double quotes
172+
infer_meant_str_literal = if you meant to write a string literal, use double quotes
173173
infer_mismatched_static_lifetime = incompatible lifetime on type
174174
infer_more_targeted = {$has_param_name ->
175175
[true] `{$param_name}`

Diff for: compiler/rustc_infer/src/errors/mod.rs

+5-8
Original file line numberDiff line numberDiff line change
@@ -1339,15 +1339,12 @@ pub enum TypeErrorAdditionalDiags {
13391339
span: Span,
13401340
code: String,
13411341
},
1342-
#[suggestion(
1343-
infer_meant_str_literal,
1344-
code = "\"{code}\"",
1345-
applicability = "machine-applicable"
1346-
)]
1342+
#[multipart_suggestion(infer_meant_str_literal, applicability = "machine-applicable")]
13471343
MeantStrLiteral {
1348-
#[primary_span]
1349-
span: Span,
1350-
code: String,
1344+
#[suggestion_part(code = "\"")]
1345+
start: Span,
1346+
#[suggestion_part(code = "\"")]
1347+
end: Span,
13511348
},
13521349
#[suggestion(
13531350
infer_consider_specifying_length,

Diff for: compiler/rustc_infer/src/infer/error_reporting/mod.rs

+4-10
Original file line numberDiff line numberDiff line change
@@ -2079,16 +2079,10 @@ impl<'tcx> TypeErrCtxt<'_, 'tcx> {
20792079
// If a string was expected and the found expression is a character literal,
20802080
// perhaps the user meant to write `"s"` to specify a string literal.
20812081
(ty::Ref(_, r, _), ty::Char) if r.is_str() => {
2082-
if let Ok(code) = self.tcx.sess().source_map().span_to_snippet(span) {
2083-
if let Some(code) =
2084-
code.strip_prefix('\'').and_then(|s| s.strip_suffix('\''))
2085-
{
2086-
suggestions.push(TypeErrorAdditionalDiags::MeantStrLiteral {
2087-
span,
2088-
code: escape_literal(code),
2089-
})
2090-
}
2091-
}
2082+
suggestions.push(TypeErrorAdditionalDiags::MeantStrLiteral {
2083+
start: span.with_hi(span.lo() + BytePos(1)),
2084+
end: span.with_lo(span.hi() - BytePos(1)),
2085+
})
20922086
}
20932087
// For code `if Some(..) = expr `, the type mismatch may be expected `bool` but found `()`,
20942088
// we try to suggest to add the missing `let` for `if let Some(..) = expr`

Diff for: compiler/rustc_lexer/src/cursor.rs

+10-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
}
@@ -59,6 +59,15 @@ impl<'a> Cursor<'a> {
5959
iter.next().unwrap_or(EOF_CHAR)
6060
}
6161

62+
/// Peeks the third symbol from the input stream without consuming it.
63+
pub fn third(&self) -> char {
64+
// `.next()` optimizes better than `.nth(1)`
65+
let mut iter = self.chars.clone();
66+
iter.next();
67+
iter.next();
68+
iter.next().unwrap_or(EOF_CHAR)
69+
}
70+
6271
/// Checks if there is nothing more to consume.
6372
pub(crate) fn is_eof(&self) -> bool {
6473
self.chars.as_str().is_empty()

Diff for: compiler/rustc_parse/messages.ftl

+2-1
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ parse_more_than_one_char = character literal may only contain one codepoint
568568
.remove_non = consider removing the non-printing characters
569569
.use_double_quotes = if you meant to write a {$is_byte ->
570570
[true] byte string
571-
*[false] `str`
571+
*[false] string
572572
} literal, use double quotes
573573
574574
parse_multiple_skipped_lines = multiple lines skipped by escaped newline
@@ -833,6 +833,7 @@ parse_unknown_prefix = prefix `{$prefix}` is unknown
833833
.label = unknown prefix
834834
.note = prefixed identifiers and literals are reserved since Rust 2021
835835
.suggestion_br = use `br` for a raw byte string
836+
.suggestion_str = if you meant to write a string literal, use double quotes
836837
.suggestion_whitespace = consider inserting whitespace here
837838
838839
parse_unknown_start_of_token = unknown start of token: {$escaped}

Diff for: compiler/rustc_parse/src/errors.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -1987,6 +1987,17 @@ pub enum UnknownPrefixSugg {
19871987
style = "verbose"
19881988
)]
19891989
Whitespace(#[primary_span] Span),
1990+
#[multipart_suggestion(
1991+
parse_suggestion_str,
1992+
applicability = "maybe-incorrect",
1993+
style = "verbose"
1994+
)]
1995+
MeantStr {
1996+
#[suggestion_part(code = "\"")]
1997+
start: Span,
1998+
#[suggestion_part(code = "\"")]
1999+
end: Span,
2000+
},
19902001
}
19912002

19922003
#[derive(Diagnostic)]
@@ -2198,12 +2209,21 @@ pub enum MoreThanOneCharSugg {
21982209
ch: String,
21992210
},
22002211
#[suggestion(parse_use_double_quotes, code = "{sugg}", applicability = "machine-applicable")]
2201-
Quotes {
2212+
QuotesFull {
22022213
#[primary_span]
22032214
span: Span,
22042215
is_byte: bool,
22052216
sugg: String,
22062217
},
2218+
#[multipart_suggestion(parse_use_double_quotes, applicability = "machine-applicable")]
2219+
Quotes {
2220+
#[suggestion_part(code = "{prefix}\"")]
2221+
start: Span,
2222+
#[suggestion_part(code = "\"")]
2223+
end: Span,
2224+
is_byte: bool,
2225+
prefix: &'static str,
2226+
},
22072227
}
22082228

22092229
#[derive(Subdiagnostic)]

Diff for: compiler/rustc_parse/src/lexer/mod.rs

+52-5
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 string 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
}
@@ -669,15 +698,33 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
669698
let expn_data = prefix_span.ctxt().outer_expn_data();
670699

671700
if expn_data.edition >= Edition::Edition2021 {
701+
let mut silence = false;
672702
// In Rust 2021, this is a hard error.
673703
let sugg = if prefix == "rb" {
674704
Some(errors::UnknownPrefixSugg::UseBr(prefix_span))
675705
} else if expn_data.is_root() {
676-
Some(errors::UnknownPrefixSugg::Whitespace(prefix_span.shrink_to_hi()))
706+
if self.cursor.first() == '\''
707+
&& let Some(start) = self.last_lifetime
708+
&& self.cursor.third() != '\''
709+
{
710+
// An "unclosed `char`" error will be emitted already, silence redundant error.
711+
silence = true;
712+
Some(errors::UnknownPrefixSugg::MeantStr {
713+
start,
714+
end: self.mk_sp(self.pos, self.pos + BytePos(1)),
715+
})
716+
} else {
717+
Some(errors::UnknownPrefixSugg::Whitespace(prefix_span.shrink_to_hi()))
718+
}
677719
} else {
678720
None
679721
};
680-
self.dcx().emit_err(errors::UnknownPrefix { span: prefix_span, prefix, sugg });
722+
let err = errors::UnknownPrefix { span: prefix_span, prefix, sugg };
723+
if silence {
724+
self.dcx().create_err(err).delay_as_bug();
725+
} else {
726+
self.dcx().emit_err(err);
727+
}
681728
} else {
682729
// Before Rust 2021, only emit a lint for migration.
683730
self.psess.buffer_lint_with_diagnostic(

Diff for: compiler/rustc_parse/src/lexer/unescape_error_reporting.rs

+15-5
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,21 @@ pub(crate) fn emit_unescape_error(
9595
}
9696
escaped.push(c);
9797
}
98-
let sugg = format!("{prefix}\"{escaped}\"");
99-
MoreThanOneCharSugg::Quotes {
100-
span: full_lit_span,
101-
is_byte: mode == Mode::Byte,
102-
sugg,
98+
if escaped.len() != lit.len() || full_lit_span.is_empty() {
99+
let sugg = format!("{prefix}\"{escaped}\"");
100+
MoreThanOneCharSugg::QuotesFull {
101+
span: full_lit_span,
102+
is_byte: mode == Mode::Byte,
103+
sugg,
104+
}
105+
} else {
106+
MoreThanOneCharSugg::Quotes {
107+
start: full_lit_span
108+
.with_hi(full_lit_span.lo() + BytePos((prefix.len() + 1) as u32)),
109+
end: full_lit_span.with_lo(full_lit_span.hi() - BytePos(1)),
110+
is_byte: mode == Mode::Byte,
111+
prefix,
112+
}
103113
}
104114
});
105115
dcx.emit_err(UnescapeError::MoreThanOneChar {

Diff for: tests/ui/inference/str-as-char.stderr

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ error: character literal may only contain one codepoint
44
LL | let _: &str = '"""';
55
| ^^^^^
66
|
7-
help: if you meant to write a `str` literal, use double quotes
7+
help: if you meant to write a string literal, use double quotes
88
|
99
LL | let _: &str = "\"\"\"";
1010
| ~~~~~~~~
@@ -15,18 +15,18 @@ error: character literal may only contain one codepoint
1515
LL | let _: &str = '\"\"\"';
1616
| ^^^^^^^^
1717
|
18-
help: if you meant to write a `str` literal, use double quotes
18+
help: if you meant to write a string literal, use double quotes
1919
|
2020
LL | let _: &str = "\"\"\"";
21-
| ~~~~~~~~
21+
| ~ ~
2222

2323
error: character literal may only contain one codepoint
2424
--> $DIR/str-as-char.rs:10:19
2525
|
2626
LL | let _: &str = '"\"\"\\"\\"';
2727
| ^^^^^^^^^^^^^^^^^
2828
|
29-
help: if you meant to write a `str` literal, use double quotes
29+
help: if you meant to write a string literal, use double quotes
3030
|
3131
LL | let _: &str = "\"\"\\"\\"\\\"";
3232
| ~~~~~~~~~~~~~~~~~~~~
@@ -39,10 +39,10 @@ LL | let _: &str = 'a';
3939
| |
4040
| expected due to this
4141
|
42-
help: if you meant to write a `str` literal, use double quotes
42+
help: if you meant to write a string literal, use double quotes
4343
|
4444
LL | let _: &str = "a";
45-
| ~~~
45+
| ~ ~
4646

4747
error: aborting due to 4 previous errors
4848

Diff for: tests/ui/issues/issue-23589.stderr

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ error[E0308]: mismatched types
1515
LL | let v: Vec(&str) = vec!['1', '2'];
1616
| ^^^ expected `&str`, found `char`
1717
|
18-
help: if you meant to write a `str` literal, use double quotes
18+
help: if you meant to write a string literal, use double quotes
1919
|
2020
LL | let v: Vec(&str) = vec!["1", '2'];
21-
| ~~~
21+
| ~ ~
2222

2323
error: aborting due to 2 previous errors
2424

Diff for: tests/ui/lexer/lex-bad-char-literals-2.stderr

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ error: character literal may only contain one codepoint
44
LL | 'nope'
55
| ^^^^^^
66
|
7-
help: if you meant to write a `str` literal, use double quotes
7+
help: if you meant to write a string literal, use double quotes
88
|
99
LL | "nope"
10-
| ~~~~~~
10+
| ~ ~
1111

1212
error: aborting due to 1 previous error
1313

Diff for: tests/ui/lexer/lex-bad-char-literals-3.stderr

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ error: character literal may only contain one codepoint
44
LL | static c: char = '●●';
55
| ^^^^
66
|
7-
help: if you meant to write a `str` literal, use double quotes
7+
help: if you meant to write a string literal, use double quotes
88
|
99
LL | static c: char = "●●";
10-
| ~~~~
10+
| ~ ~
1111

1212
error: character literal may only contain one codepoint
1313
--> $DIR/lex-bad-char-literals-3.rs:5:20
1414
|
1515
LL | let ch: &str = '●●';
1616
| ^^^^
1717
|
18-
help: if you meant to write a `str` literal, use double quotes
18+
help: if you meant to write a string literal, use double quotes
1919
|
2020
LL | let ch: &str = "●●";
21-
| ~~~~
21+
| ~ ~
2222

2323
error: aborting due to 2 previous errors
2424

Diff for: tests/ui/lexer/lex-bad-char-literals-5.stderr

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@ error: character literal may only contain one codepoint
44
LL | static c: char = '\x10\x10';
55
| ^^^^^^^^^^
66
|
7-
help: if you meant to write a `str` literal, use double quotes
7+
help: if you meant to write a string literal, use double quotes
88
|
99
LL | static c: char = "\x10\x10";
10-
| ~~~~~~~~~~
10+
| ~ ~
1111

1212
error: character literal may only contain one codepoint
1313
--> $DIR/lex-bad-char-literals-5.rs:5:20
1414
|
1515
LL | let ch: &str = '\x10\x10';
1616
| ^^^^^^^^^^
1717
|
18-
help: if you meant to write a `str` literal, use double quotes
18+
help: if you meant to write a string literal, use double quotes
1919
|
2020
LL | let ch: &str = "\x10\x10";
21-
| ~~~~~~~~~~
21+
| ~ ~
2222

2323
error: aborting due to 2 previous errors
2424

0 commit comments

Comments
 (0)