Skip to content

Commit 97587e8

Browse files
committed
Don't escape unicode escape braces in print_literal
1 parent 5436dba commit 97587e8

File tree

5 files changed

+220
-36
lines changed

5 files changed

+220
-36
lines changed

clippy_lints/src/write.rs

Lines changed: 62 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,46 @@ 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+
fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String {
597+
#[derive(Clone, Copy)]
598+
enum State {
599+
Normal,
600+
Backslash,
601+
UnicodeEscape,
602+
}
603+
604+
let mut escaped = String::with_capacity(literal.len());
605+
let mut state = State::Normal;
606+
607+
for ch in literal.chars() {
608+
state = match (ch, state) {
609+
// Escape braces outside of unicode escapes by doubling them up
610+
('{' | '}', State::Normal) => {
611+
escaped.push(ch);
612+
State::Normal
613+
},
614+
// If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise:
615+
//
616+
// \u{aaaa} \\ \x01
617+
// ^ ^ ^
618+
('\\', State::Normal) if preserve_unicode_escapes => State::Backslash,
619+
// \u{aaaa}
620+
// ^
621+
('u', State::Backslash) => State::UnicodeEscape,
622+
// \xAA \\
623+
// ^ ^
624+
(_, State::Backslash) => State::Normal,
625+
// \u{aaaa}
626+
// ^
627+
('}', State::UnicodeEscape) => State::Normal,
628+
_ => state,
629+
};
630+
631+
escaped.push(ch);
632+
}
633+
634+
escaped
635+
}

tests/compile-test.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ const RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS: &[&str] = &[
381381
"needless_borrow_pat.rs",
382382
"needless_for_each_unfixable.rs",
383383
"nonminimal_bool.rs",
384-
"print_literal.rs",
385384
"redundant_static_lifetimes_multiple.rs",
386385
"ref_binding_to_reference.rs",
387386
"repl_uninit.rs",

tests/ui/print_literal.fixed

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//@run-rustfix
2+
3+
#![warn(clippy::print_literal)]
4+
#![allow(clippy::uninlined_format_args)]
5+
6+
fn main() {
7+
// these should be fine
8+
print!("Hello");
9+
println!("Hello");
10+
let world = "world";
11+
println!("Hello {}", world);
12+
println!("Hello {world}", world = world);
13+
println!("3 in hex is {:X}", 3);
14+
println!("2 + 1 = {:.4}", 3);
15+
println!("2 + 1 = {:5.4}", 3);
16+
println!("Debug test {:?}", "hello, world");
17+
println!("{0:8} {1:>8}", "hello", "world");
18+
println!("{1:8} {0:>8}", "hello", "world");
19+
println!("{foo:8} {bar:>8}", foo = "hello", bar = "world");
20+
println!("{bar:8} {foo:>8}", foo = "hello", bar = "world");
21+
println!("{number:>width$}", number = 1, width = 6);
22+
println!("{number:>0width$}", number = 1, width = 6);
23+
println!("{} of {:b} people know binary, the other half doesn't", 1, 2);
24+
println!("10 / 4 is {}", 2.5);
25+
println!("2 + 1 = {}", 3);
26+
println!("From expansion {}", stringify!(not a string literal));
27+
28+
// these should throw warnings
29+
print!("Hello world");
30+
println!("Hello {} world", world);
31+
println!("Hello world");
32+
println!("a literal {:.4}", 5);
33+
34+
// positional args don't change the fact
35+
// that we're using a literal -- this should
36+
// throw a warning
37+
println!("hello world");
38+
println!("world hello");
39+
40+
// named args shouldn't change anything either
41+
println!("hello world");
42+
println!("world hello");
43+
44+
// The string literal from `file!()` has a callsite span that isn't marked as coming from an
45+
// expansion
46+
println!("file: {}", file!());
47+
48+
// Braces in unicode escapes should not be escaped
49+
println!("{{}} \x00 \u{ab123} \\\u{ab123} {{:?}}");
50+
// This does not lint because it would have to suggest unescaping the character
51+
println!(r"{}", "\u{ab123}");
52+
// These are not unicode escapes
53+
println!("\\u{{ab123}} \\u{{{{");
54+
println!(r"\u{{ab123}} \u{{{{");
55+
println!("\\{{ab123}} \\u{{{{");
56+
println!("\\u{{ab123}}");
57+
58+
println!("mixed: {{hello}} {world}");
59+
}

tests/ui/print_literal.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//@run-rustfix
2+
13
#![warn(clippy::print_literal)]
24
#![allow(clippy::uninlined_format_args)]
35

@@ -42,4 +44,16 @@ fn main() {
4244
// The string literal from `file!()` has a callsite span that isn't marked as coming from an
4345
// expansion
4446
println!("file: {}", file!());
47+
48+
// Braces in unicode escapes should not be escaped
49+
println!("{}", "{} \x00 \u{ab123} \\\u{ab123} {:?}");
50+
// This does not lint because it would have to suggest unescaping the character
51+
println!(r"{}", "\u{ab123}");
52+
// These are not unicode escapes
53+
println!("{}", r"\u{ab123} \u{{");
54+
println!(r"{}", r"\u{ab123} \u{{");
55+
println!("{}", r"\{ab123} \u{{");
56+
println!("{}", "\\u{ab123}");
57+
58+
println!("mixed: {} {world}", "{hello}");
4559
}

tests/ui/print_literal.stderr

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: literal with an empty format string
2-
--> $DIR/print_literal.rs:27:24
2+
--> $DIR/print_literal.rs:29:24
33
|
44
LL | print!("Hello {}", "world");
55
| ^^^^^^^
@@ -12,7 +12,7 @@ LL + print!("Hello world");
1212
|
1313

1414
error: literal with an empty format string
15-
--> $DIR/print_literal.rs:28:36
15+
--> $DIR/print_literal.rs:30:36
1616
|
1717
LL | println!("Hello {} {}", world, "world");
1818
| ^^^^^^^
@@ -24,7 +24,7 @@ LL + println!("Hello {} world", world);
2424
|
2525

2626
error: literal with an empty format string
27-
--> $DIR/print_literal.rs:29:26
27+
--> $DIR/print_literal.rs:31:26
2828
|
2929
LL | println!("Hello {}", "world");
3030
| ^^^^^^^
@@ -36,7 +36,7 @@ LL + println!("Hello world");
3636
|
3737

3838
error: literal with an empty format string
39-
--> $DIR/print_literal.rs:30:26
39+
--> $DIR/print_literal.rs:32:26
4040
|
4141
LL | println!("{} {:.4}", "a literal", 5);
4242
| ^^^^^^^^^^^
@@ -48,7 +48,7 @@ LL + println!("a literal {:.4}", 5);
4848
|
4949

5050
error: literal with an empty format string
51-
--> $DIR/print_literal.rs:35:25
51+
--> $DIR/print_literal.rs:37:25
5252
|
5353
LL | println!("{0} {1}", "hello", "world");
5454
| ^^^^^^^
@@ -60,7 +60,7 @@ LL + println!("hello {1}", "world");
6060
|
6161

6262
error: literal with an empty format string
63-
--> $DIR/print_literal.rs:35:34
63+
--> $DIR/print_literal.rs:37:34
6464
|
6565
LL | println!("{0} {1}", "hello", "world");
6666
| ^^^^^^^
@@ -72,7 +72,7 @@ LL + println!("{0} world", "hello");
7272
|
7373

7474
error: literal with an empty format string
75-
--> $DIR/print_literal.rs:36:34
75+
--> $DIR/print_literal.rs:38:34
7676
|
7777
LL | println!("{1} {0}", "hello", "world");
7878
| ^^^^^^^
@@ -84,7 +84,7 @@ LL + println!("world {0}", "hello");
8484
|
8585

8686
error: literal with an empty format string
87-
--> $DIR/print_literal.rs:36:25
87+
--> $DIR/print_literal.rs:38:25
8888
|
8989
LL | println!("{1} {0}", "hello", "world");
9090
| ^^^^^^^
@@ -96,7 +96,7 @@ LL + println!("{1} hello", "world");
9696
|
9797

9898
error: literal with an empty format string
99-
--> $DIR/print_literal.rs:39:35
99+
--> $DIR/print_literal.rs:41:35
100100
|
101101
LL | println!("{foo} {bar}", foo = "hello", bar = "world");
102102
| ^^^^^^^
@@ -108,7 +108,7 @@ LL + println!("hello {bar}", bar = "world");
108108
|
109109

110110
error: literal with an empty format string
111-
--> $DIR/print_literal.rs:39:50
111+
--> $DIR/print_literal.rs:41:50
112112
|
113113
LL | println!("{foo} {bar}", foo = "hello", bar = "world");
114114
| ^^^^^^^
@@ -120,7 +120,7 @@ LL + println!("{foo} world", foo = "hello");
120120
|
121121

122122
error: literal with an empty format string
123-
--> $DIR/print_literal.rs:40:50
123+
--> $DIR/print_literal.rs:42:50
124124
|
125125
LL | println!("{bar} {foo}", foo = "hello", bar = "world");
126126
| ^^^^^^^
@@ -132,7 +132,7 @@ LL + println!("world {foo}", foo = "hello");
132132
|
133133

134134
error: literal with an empty format string
135-
--> $DIR/print_literal.rs:40:35
135+
--> $DIR/print_literal.rs:42:35
136136
|
137137
LL | println!("{bar} {foo}", foo = "hello", bar = "world");
138138
| ^^^^^^^
@@ -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:49: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:53: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:54: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:55: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:56: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:58: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)