Skip to content

Commit 8e3b1a6

Browse files
committed
Don't escape unicode escape braces in print_literal
1 parent b5bfd11 commit 8e3b1a6

File tree

4 files changed

+160
-23
lines changed

4 files changed

+160
-23
lines changed

clippy_lints/src/write.rs

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -471,9 +471,9 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
471471
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
472472
&& !arg.expr.span.from_expansion()
473473
&& let Some(value_string) = snippet_opt(cx, arg.expr.span)
474-
{
474+
{
475475
let (replacement, replace_raw) = match lit.kind {
476-
LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) {
476+
LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) {
477477
Some(extracted) => extracted,
478478
None => return,
479479
},
@@ -519,27 +519,24 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
519519
},
520520
};
521521

522-
span_lint_and_then(
523-
cx,
524-
lint,
525-
arg.expr.span,
526-
"literal with an empty format string",
527-
|diag| {
528-
if let Some(replacement) = replacement
529-
// `format!("{}", "a")`, `format!("{named}", named = "b")
530-
// ~~~~~ ~~~~~~~~~~~~~
531-
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
532-
{
533-
let replacement = replacement.replace('{', "{{").replace('}', "}}");
534-
diag.multipart_suggestion(
535-
"try",
536-
vec![(*placeholder_span, replacement), (removal_span, String::new())],
537-
Applicability::MachineApplicable,
538-
);
539-
}
540-
},
541-
);
522+
span_lint_and_then(cx, lint, arg.expr.span, "literal with an empty format string", |diag| {
523+
if let Some(replacement) = replacement
524+
// `format!("{}", "a")`, `format!("{named}", named = "b")
525+
// ~~~~~ ~~~~~~~~~~~~~
526+
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
527+
{
528+
let replacement = escape_braces(&replacement, !format_string_is_raw && !replace_raw);
542529

530+
diag.multipart_suggestion(
531+
"try",
532+
vec![
533+
(*placeholder_span, replacement),
534+
(removal_span, String::new()),
535+
],
536+
Applicability::MachineApplicable,
537+
);
538+
}
539+
});
543540
}
544541
}
545542
}
@@ -593,3 +590,47 @@ fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
593590

594591
if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
595592
}
593+
594+
/// Replaces `{` with `{{` and `}` with `}}`. If `preserve_unicode_escapes` is `true` the braces in
595+
/// `\u{xxxx}` are left unmodified
596+
#[expect(clippy::match_same_arms)]
597+
fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String {
598+
#[derive(Clone, Copy)]
599+
enum State {
600+
Normal,
601+
Backslash,
602+
UnicodeEscape,
603+
}
604+
605+
let mut escaped = String::with_capacity(literal.len());
606+
let mut state = State::Normal;
607+
608+
for ch in literal.chars() {
609+
state = match (ch, state) {
610+
// Escape braces outside of unicode escapes by doubling them up
611+
('{' | '}', State::Normal) => {
612+
escaped.push(ch);
613+
State::Normal
614+
},
615+
// If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise:
616+
//
617+
// \u{aaaa} \\ \x01
618+
// ^ ^ ^
619+
('\\', State::Normal) if preserve_unicode_escapes => State::Backslash,
620+
// \u{aaaa}
621+
// ^
622+
('u', State::Backslash) => State::UnicodeEscape,
623+
// \xAA \\
624+
// ^ ^
625+
(_, State::Backslash) => State::Normal,
626+
// \u{aaaa}
627+
// ^
628+
('}', State::UnicodeEscape) => State::Normal,
629+
_ => state,
630+
};
631+
632+
escaped.push(ch);
633+
}
634+
635+
escaped
636+
}

tests/ui/print_literal.fixed

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,16 @@ fn main() {
4242
// The string literal from `file!()` has a callsite span that isn't marked as coming from an
4343
// expansion
4444
println!("file: {}", file!());
45+
46+
// Braces in unicode escapes should not be escaped
47+
println!("{{}} \x00 \u{ab123} \\\u{ab123} {{:?}}");
48+
// This does not lint because it would have to suggest unescaping the character
49+
println!(r"{}", "\u{ab123}");
50+
// These are not unicode escapes
51+
println!("\\u{{ab123}} \\u{{{{");
52+
println!(r"\u{{ab123}} \u{{{{");
53+
println!("\\{{ab123}} \\u{{{{");
54+
println!("\\u{{ab123}}");
55+
56+
println!("mixed: {{hello}} {world}");
4557
}

tests/ui/print_literal.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,16 @@ fn main() {
4242
// The string literal from `file!()` has a callsite span that isn't marked as coming from an
4343
// expansion
4444
println!("file: {}", file!());
45+
46+
// Braces in unicode escapes should not be escaped
47+
println!("{}", "{} \x00 \u{ab123} \\\u{ab123} {:?}");
48+
// This does not lint because it would have to suggest unescaping the character
49+
println!(r"{}", "\u{ab123}");
50+
// These are not unicode escapes
51+
println!("{}", r"\u{ab123} \u{{");
52+
println!(r"{}", r"\u{ab123} \u{{");
53+
println!("{}", r"\{ab123} \u{{");
54+
println!("{}", "\\u{ab123}");
55+
56+
println!("mixed: {} {world}", "{hello}");
4557
}

tests/ui/print_literal.stderr

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,77 @@ LL - println!("{bar} {foo}", foo = "hello", bar = "world");
143143
LL + println!("{bar} hello", bar = "world");
144144
|
145145

146-
error: aborting due to 12 previous errors
146+
error: literal with an empty format string
147+
--> $DIR/print_literal.rs:47:20
148+
|
149+
LL | println!("{}", "{} /x00 /u{ab123} ///u{ab123} {:?}");
150+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
151+
|
152+
help: try
153+
|
154+
LL - println!("{}", "{} /x00 /u{ab123} ///u{ab123} {:?}");
155+
LL + println!("{{}} /x00 /u{ab123} ///u{ab123} {{:?}}");
156+
|
157+
158+
error: literal with an empty format string
159+
--> $DIR/print_literal.rs:51:20
160+
|
161+
LL | println!("{}", r"/u{ab123} /u{{");
162+
| ^^^^^^^^^^^^^^^^^
163+
|
164+
help: try
165+
|
166+
LL - println!("{}", r"/u{ab123} /u{{");
167+
LL + println!("//u{{ab123}} //u{{{{");
168+
|
169+
170+
error: literal with an empty format string
171+
--> $DIR/print_literal.rs:52:21
172+
|
173+
LL | println!(r"{}", r"/u{ab123} /u{{");
174+
| ^^^^^^^^^^^^^^^^^
175+
|
176+
help: try
177+
|
178+
LL - println!(r"{}", r"/u{ab123} /u{{");
179+
LL + println!(r"/u{{ab123}} /u{{{{");
180+
|
181+
182+
error: literal with an empty format string
183+
--> $DIR/print_literal.rs:53:20
184+
|
185+
LL | println!("{}", r"/{ab123} /u{{");
186+
| ^^^^^^^^^^^^^^^^
187+
|
188+
help: try
189+
|
190+
LL - println!("{}", r"/{ab123} /u{{");
191+
LL + println!("//{{ab123}} //u{{{{");
192+
|
193+
194+
error: literal with an empty format string
195+
--> $DIR/print_literal.rs:54:20
196+
|
197+
LL | println!("{}", "//u{ab123}");
198+
| ^^^^^^^^^^^^
199+
|
200+
help: try
201+
|
202+
LL - println!("{}", "//u{ab123}");
203+
LL + println!("//u{{ab123}}");
204+
|
205+
206+
error: literal with an empty format string
207+
--> $DIR/print_literal.rs:56:35
208+
|
209+
LL | println!("mixed: {} {world}", "{hello}");
210+
| ^^^^^^^^^
211+
|
212+
help: try
213+
|
214+
LL - println!("mixed: {} {world}", "{hello}");
215+
LL + println!("mixed: {{hello}} {world}");
216+
|
217+
218+
error: aborting due to 18 previous errors
147219

0 commit comments

Comments
 (0)