Skip to content

Commit 0900712

Browse files
authored
Rollup merge of #115611 - lukas-code:format!("{r#ident}"), r=compiler-errors
add diagnostic for raw identifiers in format string Format strings don't support raw identifiers (e.g. `format!("{r#type}")`), but they do support keywords in the format string directly (e.g. `format!("{type}")`). This PR improves the error output when attempting to use a raw identifier in a format string and adds a machine-applicable suggestion to remove the `r#`. fixes #115466
2 parents 6011fd4 + d990eee commit 0900712

File tree

6 files changed

+158
-35
lines changed

6 files changed

+158
-35
lines changed

compiler/rustc_builtin_macros/messages.ftl

+4-2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ builtin_macros_format_positional_after_named = positional arguments cannot follo
137137
.label = positional arguments must be before named arguments
138138
.named_args = named argument
139139
140+
builtin_macros_format_remove_raw_ident = remove the `r#`
141+
140142
builtin_macros_format_requires_string = requires at least a format string argument
141143
142144
builtin_macros_format_string_invalid = invalid format string: {$desc}
@@ -165,6 +167,8 @@ builtin_macros_format_unused_arg = {$named ->
165167
builtin_macros_format_unused_args = multiple unused formatting arguments
166168
.label = multiple missing formatting specifiers
167169
170+
builtin_macros_format_use_positional = consider using a positional formatting argument instead
171+
168172
builtin_macros_global_asm_clobber_abi = `clobber_abi` cannot be used with `global_asm!`
169173
170174
builtin_macros_invalid_crate_attribute = invalid crate attribute
@@ -205,8 +209,6 @@ builtin_macros_requires_cfg_pattern =
205209
206210
builtin_macros_should_panic = functions using `#[should_panic]` must return `()`
207211
208-
builtin_macros_sugg = consider using a positional formatting argument instead
209-
210212
builtin_macros_test_arg_non_lifetime = functions used as tests can not have any non-lifetime generic parameters
211213
212214
builtin_macros_test_args = functions used as tests can not have any arguments

compiler/rustc_builtin_macros/src/errors.rs

+23-12
Original file line numberDiff line numberDiff line change
@@ -539,18 +539,29 @@ pub(crate) struct InvalidFormatStringLabel {
539539
}
540540

541541
#[derive(Subdiagnostic)]
542-
#[multipart_suggestion(
543-
builtin_macros_sugg,
544-
style = "verbose",
545-
applicability = "machine-applicable"
546-
)]
547-
pub(crate) struct InvalidFormatStringSuggestion {
548-
#[suggestion_part(code = "{len}")]
549-
pub(crate) captured: Span,
550-
pub(crate) len: String,
551-
#[suggestion_part(code = ", {arg}")]
552-
pub(crate) span: Span,
553-
pub(crate) arg: String,
542+
pub(crate) enum InvalidFormatStringSuggestion {
543+
#[multipart_suggestion(
544+
builtin_macros_format_use_positional,
545+
style = "verbose",
546+
applicability = "machine-applicable"
547+
)]
548+
UsePositional {
549+
#[suggestion_part(code = "{len}")]
550+
captured: Span,
551+
len: String,
552+
#[suggestion_part(code = ", {arg}")]
553+
span: Span,
554+
arg: String,
555+
},
556+
#[suggestion(
557+
builtin_macros_format_remove_raw_ident,
558+
code = "",
559+
applicability = "machine-applicable"
560+
)]
561+
RemoveRawIdent {
562+
#[primary_span]
563+
span: Span,
564+
},
554565
}
555566

556567
#[derive(Diagnostic)]

compiler/rustc_builtin_macros/src/format.rs

+23-14
Original file line numberDiff line numberDiff line change
@@ -260,20 +260,29 @@ fn make_format_args(
260260
if let Some((label, span)) = err.secondary_label && is_source_literal {
261261
e.label_ = Some(errors::InvalidFormatStringLabel { span: fmt_span.from_inner(InnerSpan::new(span.start, span.end)), label } );
262262
}
263-
if err.should_be_replaced_with_positional_argument {
264-
let captured_arg_span =
265-
fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end));
266-
if let Ok(arg) = ecx.source_map().span_to_snippet(captured_arg_span) {
267-
let span = match args.unnamed_args().last() {
268-
Some(arg) => arg.expr.span,
269-
None => fmt_span,
270-
};
271-
e.sugg_ = Some(errors::InvalidFormatStringSuggestion {
272-
captured: captured_arg_span,
273-
len: args.unnamed_args().len().to_string(),
274-
span: span.shrink_to_hi(),
275-
arg,
276-
});
263+
match err.suggestion {
264+
parse::Suggestion::None => {}
265+
parse::Suggestion::UsePositional => {
266+
let captured_arg_span =
267+
fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end));
268+
if let Ok(arg) = ecx.source_map().span_to_snippet(captured_arg_span) {
269+
let span = match args.unnamed_args().last() {
270+
Some(arg) => arg.expr.span,
271+
None => fmt_span,
272+
};
273+
e.sugg_ = Some(errors::InvalidFormatStringSuggestion::UsePositional {
274+
captured: captured_arg_span,
275+
len: args.unnamed_args().len().to_string(),
276+
span: span.shrink_to_hi(),
277+
arg,
278+
});
279+
}
280+
}
281+
parse::Suggestion::RemoveRawIdent(span) => {
282+
if is_source_literal {
283+
let span = fmt_span.from_inner(InnerSpan::new(span.start, span.end));
284+
e.sugg_ = Some(errors::InvalidFormatStringSuggestion::RemoveRawIdent { span })
285+
}
277286
}
278287
}
279288
ecx.emit_err(e);

compiler/rustc_parse_format/src/lib.rs

+47-7
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,17 @@ pub struct ParseError {
210210
pub label: string::String,
211211
pub span: InnerSpan,
212212
pub secondary_label: Option<(string::String, InnerSpan)>,
213-
pub should_be_replaced_with_positional_argument: bool,
213+
pub suggestion: Suggestion,
214+
}
215+
216+
pub enum Suggestion {
217+
None,
218+
/// Replace inline argument with positional argument:
219+
/// `format!("{foo.bar}")` -> `format!("{}", foo.bar)`
220+
UsePositional,
221+
/// Remove `r#` from identifier:
222+
/// `format!("{r#foo}")` -> `format!("{foo}")`
223+
RemoveRawIdent(InnerSpan),
214224
}
215225

216226
/// The parser structure for interpreting the input format string. This is
@@ -365,7 +375,7 @@ impl<'a> Parser<'a> {
365375
label: label.into(),
366376
span,
367377
secondary_label: None,
368-
should_be_replaced_with_positional_argument: false,
378+
suggestion: Suggestion::None,
369379
});
370380
}
371381

@@ -389,7 +399,7 @@ impl<'a> Parser<'a> {
389399
label: label.into(),
390400
span,
391401
secondary_label: None,
392-
should_be_replaced_with_positional_argument: false,
402+
suggestion: Suggestion::None,
393403
});
394404
}
395405

@@ -493,7 +503,7 @@ impl<'a> Parser<'a> {
493503
label,
494504
span: pos.to(pos),
495505
secondary_label,
496-
should_be_replaced_with_positional_argument: false,
506+
suggestion: Suggestion::None,
497507
});
498508

499509
None
@@ -573,7 +583,37 @@ impl<'a> Parser<'a> {
573583
Some(ArgumentIs(i))
574584
} else {
575585
match self.cur.peek() {
576-
Some(&(_, c)) if rustc_lexer::is_id_start(c) => Some(ArgumentNamed(self.word())),
586+
Some(&(lo, c)) if rustc_lexer::is_id_start(c) => {
587+
let word = self.word();
588+
589+
// Recover from `r#ident` in format strings.
590+
// FIXME: use a let chain
591+
if word == "r" {
592+
if let Some((pos, '#')) = self.cur.peek() {
593+
if self.input[pos + 1..]
594+
.chars()
595+
.next()
596+
.is_some_and(rustc_lexer::is_id_start)
597+
{
598+
self.cur.next();
599+
let word = self.word();
600+
let prefix_span = self.span(lo, lo + 2);
601+
let full_span = self.span(lo, lo + 2 + word.len());
602+
self.errors.insert(0, ParseError {
603+
description: "raw identifiers are not supported".to_owned(),
604+
note: Some("identifiers in format strings can be keywords and don't need to be prefixed with `r#`".to_string()),
605+
label: "raw identifier used here".to_owned(),
606+
span: full_span,
607+
secondary_label: None,
608+
suggestion: Suggestion::RemoveRawIdent(prefix_span),
609+
});
610+
return Some(ArgumentNamed(word));
611+
}
612+
}
613+
}
614+
615+
Some(ArgumentNamed(word))
616+
}
577617

578618
// This is an `ArgumentNext`.
579619
// Record the fact and do the resolution after parsing the
@@ -841,7 +881,7 @@ impl<'a> Parser<'a> {
841881
label: "expected `?` to occur after `:`".to_owned(),
842882
span: pos.to(pos),
843883
secondary_label: None,
844-
should_be_replaced_with_positional_argument: false,
884+
suggestion: Suggestion::None,
845885
},
846886
);
847887
}
@@ -867,7 +907,7 @@ impl<'a> Parser<'a> {
867907
label: "not supported".to_string(),
868908
span: InnerSpan::new(arg.position_span.start, field.position_span.end),
869909
secondary_label: None,
870-
should_be_replaced_with_positional_argument: true,
910+
suggestion: Suggestion::UsePositional,
871911
},
872912
);
873913
}

tests/ui/fmt/raw-idents.rs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Regression test for https://github.com/rust-lang/rust/issues/115466
2+
3+
// The "identifier" in format strings is parsed as an IDENTIFIER_OR_KEYWORD, not an IDENTIFIER.
4+
// Test that there is an actionable diagnostic if a RAW_IDENTIFIER is used instead.
5+
6+
fn main() {
7+
let r#type = "foobar";
8+
println!("It is {r#type}"); //~ ERROR: invalid format string: raw identifiers are not supported
9+
println!(r##"It still is {r#type}"##); //~ ERROR: invalid format string: raw identifiers are not supported
10+
println!(concat!("{r#", "type}")); //~ ERROR: invalid format string: raw identifiers are not supported
11+
println!("{\x72\x23type:?}"); //~ ERROR: invalid format string: raw identifiers are not supported
12+
13+
// OK
14+
println!("{type}");
15+
println!("{let}", let = r#type);
16+
println!("{let}", r#let = r#type);
17+
}

tests/ui/fmt/raw-idents.stderr

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
error: invalid format string: raw identifiers are not supported
2+
--> $DIR/raw-idents.rs:8:22
3+
|
4+
LL | println!("It is {r#type}");
5+
| --^^^^
6+
| |
7+
| raw identifier used here in format string
8+
| help: remove the `r#`
9+
|
10+
= note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
11+
12+
error: invalid format string: raw identifiers are not supported
13+
--> $DIR/raw-idents.rs:9:31
14+
|
15+
LL | println!(r##"It still is {r#type}"##);
16+
| --^^^^
17+
| |
18+
| raw identifier used here in format string
19+
| help: remove the `r#`
20+
|
21+
= note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
22+
23+
error: invalid format string: raw identifiers are not supported
24+
--> $DIR/raw-idents.rs:10:14
25+
|
26+
LL | println!(concat!("{r#", "type}"));
27+
| ^^^^^^^^^^^^^^^^^^^^^^^ raw identifier used here in format string
28+
|
29+
= note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
30+
= note: this error originates in the macro `concat` (in Nightly builds, run with -Z macro-backtrace for more info)
31+
32+
error: invalid format string: raw identifiers are not supported
33+
--> $DIR/raw-idents.rs:11:16
34+
|
35+
LL | println!("{\x72\x23type:?}");
36+
| --------^^^^
37+
| |
38+
| raw identifier used here in format string
39+
| help: remove the `r#`
40+
|
41+
= note: identifiers in format strings can be keywords and don't need to be prefixed with `r#`
42+
43+
error: aborting due to 4 previous errors
44+

0 commit comments

Comments
 (0)