Skip to content

Commit b11b685

Browse files
committed
Support user format-like macros
Add support for `#[clippy::format_args]` attribute that can be attached to any macro to indicate that it functions the same as the built-in format macros like `format!`, `println!` and `write!`
1 parent a01975b commit b11b685

11 files changed

+292
-7
lines changed

clippy_utils/src/attrs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub const BUILTIN_ATTRIBUTES: &[(&str, DeprecationStatus)] = &[
2828
("dump", DeprecationStatus::None),
2929
("msrv", DeprecationStatus::None),
3030
("has_significant_drop", DeprecationStatus::None),
31+
("format_args", DeprecationStatus::None),
3132
];
3233

3334
pub struct LimitStack {

clippy_utils/src/macros.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
#![allow(clippy::similar_names)] // `expr` and `expn`
22

3+
use crate::get_unique_attr;
34
use crate::visitors::{Descend, for_each_expr_without_closures};
45

56
use arrayvec::ArrayVec;
67
use rustc_ast::{FormatArgs, FormatArgument, FormatPlaceholder};
78
use rustc_data_structures::fx::FxHashMap;
89
use rustc_data_structures::sync::{Lrc, OnceLock};
910
use rustc_hir::{self as hir, Expr, ExprKind, HirId, Node, QPath};
10-
use rustc_lint::LateContext;
11+
use rustc_lint::{LateContext, LintContext};
1112
use rustc_span::def_id::DefId;
1213
use rustc_span::hygiene::{self, MacroKind, SyntaxContext};
1314
use rustc_span::{BytePos, ExpnData, ExpnId, ExpnKind, Span, SpanData, Symbol, sym};
@@ -36,7 +37,9 @@ pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
3637
if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
3738
FORMAT_MACRO_DIAG_ITEMS.contains(&name)
3839
} else {
39-
false
40+
// Allow users to tag any macro as being format!-like
41+
// TODO: consider deleting FORMAT_MACRO_DIAG_ITEMS and using just this method
42+
get_unique_attr(cx.sess(), cx.tcx.get_attrs_unchecked(macro_def_id), "format_args").is_some()
4043
}
4144
}
4245

tests/ui/format_args_unfixable.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,32 @@ fn test2() {
119119
format!("something failed at {}", Location::caller())
120120
);
121121
}
122+
123+
#[clippy::format_args]
124+
macro_rules! usr_println {
125+
($target:expr, $($args:tt)*) => {{
126+
if $target {
127+
println!($($args)*)
128+
}
129+
}};
130+
}
131+
132+
fn user_format() {
133+
let error = Error::new(ErrorKind::Other, "bad thing");
134+
let x = 'x';
135+
136+
usr_println!(true, "error: {}", format!("boom at {}", Location::caller()));
137+
//~^ ERROR: `format!` in `usr_println!` args
138+
usr_println!(true, "{}: {}", error, format!("boom at {}", Location::caller()));
139+
//~^ ERROR: `format!` in `usr_println!` args
140+
usr_println!(true, "{:?}: {}", error, format!("boom at {}", Location::caller()));
141+
//~^ ERROR: `format!` in `usr_println!` args
142+
usr_println!(true, "{{}}: {}", format!("boom at {}", Location::caller()));
143+
//~^ ERROR: `format!` in `usr_println!` args
144+
usr_println!(true, r#"error: "{}""#, format!("boom at {}", Location::caller()));
145+
//~^ ERROR: `format!` in `usr_println!` args
146+
usr_println!(true, "error: {}", format!(r#"boom at "{}""#, Location::caller()));
147+
//~^ ERROR: `format!` in `usr_println!` args
148+
usr_println!(true, "error: {}", format!("boom at {} {0}", Location::caller()));
149+
//~^ ERROR: `format!` in `usr_println!` args
150+
}

tests/ui/format_args_unfixable.stderr

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,5 +174,68 @@ LL | panic!("error: {}", format!("something failed at {}", Location::caller(
174174
= help: combine the `format!(..)` arguments with the outer `panic!(..)` call
175175
= help: or consider changing `format!` to `format_args!`
176176

177-
error: aborting due to 18 previous errors
177+
error: `format!` in `usr_println!` args
178+
--> tests/ui/format_args_unfixable.rs:136:5
179+
|
180+
LL | usr_println!(true, "error: {}", format!("boom at {}", Location::caller()));
181+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
182+
|
183+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
184+
= help: or consider changing `format!` to `format_args!`
185+
186+
error: `format!` in `usr_println!` args
187+
--> tests/ui/format_args_unfixable.rs:138:5
188+
|
189+
LL | usr_println!(true, "{}: {}", error, format!("boom at {}", Location::caller()));
190+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
191+
|
192+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
193+
= help: or consider changing `format!` to `format_args!`
194+
195+
error: `format!` in `usr_println!` args
196+
--> tests/ui/format_args_unfixable.rs:140:5
197+
|
198+
LL | usr_println!(true, "{:?}: {}", error, format!("boom at {}", Location::caller()));
199+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
200+
|
201+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
202+
= help: or consider changing `format!` to `format_args!`
203+
204+
error: `format!` in `usr_println!` args
205+
--> tests/ui/format_args_unfixable.rs:142:5
206+
|
207+
LL | usr_println!(true, "{{}}: {}", format!("boom at {}", Location::caller()));
208+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
209+
|
210+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
211+
= help: or consider changing `format!` to `format_args!`
212+
213+
error: `format!` in `usr_println!` args
214+
--> tests/ui/format_args_unfixable.rs:144:5
215+
|
216+
LL | usr_println!(true, r#"error: "{}""#, format!("boom at {}", Location::caller()));
217+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
218+
|
219+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
220+
= help: or consider changing `format!` to `format_args!`
221+
222+
error: `format!` in `usr_println!` args
223+
--> tests/ui/format_args_unfixable.rs:146:5
224+
|
225+
LL | usr_println!(true, "error: {}", format!(r#"boom at "{}""#, Location::caller()));
226+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
227+
|
228+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
229+
= help: or consider changing `format!` to `format_args!`
230+
231+
error: `format!` in `usr_println!` args
232+
--> tests/ui/format_args_unfixable.rs:148:5
233+
|
234+
LL | usr_println!(true, "error: {}", format!("boom at {} {0}", Location::caller()));
235+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
236+
|
237+
= help: combine the `format!(..)` arguments with the outer `usr_println!(..)` call
238+
= help: or consider changing `format!` to `format_args!`
239+
240+
error: aborting due to 25 previous errors
178241

tests/ui/uninlined_format_args.fixed

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ macro_rules! my_concat {
212212
}
213213
}
214214

215+
#[clippy::format_args]
215216
macro_rules! my_good_macro {
216217
($fmt:literal $(, $e:expr)* $(,)?) => {
217218
println!($fmt $(, $e)*)
@@ -255,8 +256,8 @@ fn tester2() {
255256
my_println2_args!(true, "{}", local_i32);
256257
my_println2!(true, "{}", local_i32);
257258
my_concat!("{}", local_i32);
258-
my_good_macro!("{}", local_i32);
259-
my_good_macro!("{}", local_i32,);
259+
my_good_macro!("{local_i32}");
260+
my_good_macro!("{local_i32}",);
260261

261262
// FIXME: Broken false positives, currently unhandled
262263
my_bad_macro!("{}", local_i32);

tests/ui/uninlined_format_args.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ macro_rules! my_concat {
217217
}
218218
}
219219

220+
#[clippy::format_args]
220221
macro_rules! my_good_macro {
221222
($fmt:literal $(, $e:expr)* $(,)?) => {
222223
println!($fmt $(, $e)*)

tests/ui/uninlined_format_args.stderr

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,5 +845,29 @@ LL - println!("expand='{}'", local_i32);
845845
LL + println!("expand='{local_i32}'");
846846
|
847847

848-
error: aborting due to 71 previous errors
848+
error: variables can be used directly in the `format!` string
849+
--> tests/ui/uninlined_format_args.rs:264:5
850+
|
851+
LL | my_good_macro!("{}", local_i32);
852+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
853+
|
854+
help: change this to
855+
|
856+
LL - my_good_macro!("{}", local_i32);
857+
LL + my_good_macro!("{local_i32}");
858+
|
859+
860+
error: variables can be used directly in the `format!` string
861+
--> tests/ui/uninlined_format_args.rs:265:5
862+
|
863+
LL | my_good_macro!("{}", local_i32,);
864+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
865+
|
866+
help: change this to
867+
|
868+
LL - my_good_macro!("{}", local_i32,);
869+
LL + my_good_macro!("{local_i32}",);
870+
|
871+
872+
error: aborting due to 73 previous errors
849873

tests/ui/unused_format_specs.1.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,38 @@ fn should_not_lint() {
3333
let args = format_args!("");
3434
println!("{args}");
3535
}
36+
37+
#[clippy::format_args]
38+
macro_rules! usr_println {
39+
($target:expr, $($args:tt)*) => {{
40+
if $target {
41+
println!($($args)*)
42+
}
43+
}};
44+
}
45+
46+
fn should_lint_user() {
47+
// prints `.`, not ` .`
48+
usr_println!(true, "{:5}.", format!(""));
49+
//~^ ERROR: format specifiers have no effect on `format_args!()`
50+
//prints `abcde`, not `abc`
51+
usr_println!(true, "{:.3}", format!("abcde"));
52+
//~^ ERROR: format specifiers have no effect on `format_args!()`
53+
54+
usr_println!(true, "{}.", format_args_from_macro!());
55+
//~^ ERROR: format specifiers have no effect on `format_args!()`
56+
57+
let args = format_args!("");
58+
usr_println!(true, "{args}");
59+
//~^ ERROR: format specifiers have no effect on `format_args!()`
60+
}
61+
62+
fn should_not_lint_user() {
63+
usr_println!(true, "{}", format_args!(""));
64+
// Technically the same as `{}`, but the `format_args` docs specifically mention that you can use
65+
// debug formatting so allow it
66+
usr_println!(true, "{:?}", format_args!(""));
67+
68+
let args = format_args!("");
69+
usr_println!(true, "{args}");
70+
}

tests/ui/unused_format_specs.2.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,38 @@ fn should_not_lint() {
3333
let args = format_args!("");
3434
println!("{args}");
3535
}
36+
37+
#[clippy::format_args]
38+
macro_rules! usr_println {
39+
($target:expr, $($args:tt)*) => {{
40+
if $target {
41+
println!($($args)*)
42+
}
43+
}};
44+
}
45+
46+
fn should_lint_user() {
47+
// prints `.`, not ` .`
48+
usr_println!(true, "{}.", format_args!(""));
49+
//~^ ERROR: format specifiers have no effect on `format_args!()`
50+
//prints `abcde`, not `abc`
51+
usr_println!(true, "{}", format_args!("abcde"));
52+
//~^ ERROR: format specifiers have no effect on `format_args!()`
53+
54+
usr_println!(true, "{}.", format_args_from_macro!());
55+
//~^ ERROR: format specifiers have no effect on `format_args!()`
56+
57+
let args = format_args!("");
58+
usr_println!(true, "{args}");
59+
//~^ ERROR: format specifiers have no effect on `format_args!()`
60+
}
61+
62+
fn should_not_lint_user() {
63+
usr_println!(true, "{}", format_args!(""));
64+
// Technically the same as `{}`, but the `format_args` docs specifically mention that you can use
65+
// debug formatting so allow it
66+
usr_println!(true, "{:?}", format_args!(""));
67+
68+
let args = format_args!("");
69+
usr_println!(true, "{args}");
70+
}

tests/ui/unused_format_specs.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,38 @@ fn should_not_lint() {
3333
let args = format_args!("");
3434
println!("{args}");
3535
}
36+
37+
#[clippy::format_args]
38+
macro_rules! usr_println {
39+
($target:expr, $($args:tt)*) => {{
40+
if $target {
41+
println!($($args)*)
42+
}
43+
}};
44+
}
45+
46+
fn should_lint_user() {
47+
// prints `.`, not ` .`
48+
usr_println!(true, "{:5}.", format_args!(""));
49+
//~^ ERROR: format specifiers have no effect on `format_args!()`
50+
//prints `abcde`, not `abc`
51+
usr_println!(true, "{:.3}", format_args!("abcde"));
52+
//~^ ERROR: format specifiers have no effect on `format_args!()`
53+
54+
usr_println!(true, "{:5}.", format_args_from_macro!());
55+
//~^ ERROR: format specifiers have no effect on `format_args!()`
56+
57+
let args = format_args!("");
58+
usr_println!(true, "{args:5}");
59+
//~^ ERROR: format specifiers have no effect on `format_args!()`
60+
}
61+
62+
fn should_not_lint_user() {
63+
usr_println!(true, "{}", format_args!(""));
64+
// Technically the same as `{}`, but the `format_args` docs specifically mention that you can use
65+
// debug formatting so allow it
66+
usr_println!(true, "{:?}", format_args!(""));
67+
68+
let args = format_args!("");
69+
usr_println!(true, "{args}");
70+
}

tests/ui/unused_format_specs.stderr

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,63 @@ LL - println!("{args:5}");
5858
LL + println!("{args}");
5959
|
6060

61-
error: aborting due to 4 previous errors
61+
error: format specifiers have no effect on `format_args!()`
62+
--> tests/ui/unused_format_specs.rs:48:25
63+
|
64+
LL | usr_println!(true, "{:5}.", format_args!(""));
65+
| ^^^^
66+
|
67+
help: for the width to apply consider using `format!()`
68+
|
69+
LL | usr_println!(true, "{:5}.", format!(""));
70+
| ~~~~~~
71+
help: if the current behavior is intentional, remove the format specifiers
72+
|
73+
LL - usr_println!(true, "{:5}.", format_args!(""));
74+
LL + usr_println!(true, "{}.", format_args!(""));
75+
|
76+
77+
error: format specifiers have no effect on `format_args!()`
78+
--> tests/ui/unused_format_specs.rs:51:25
79+
|
80+
LL | usr_println!(true, "{:.3}", format_args!("abcde"));
81+
| ^^^^^
82+
|
83+
help: for the precision to apply consider using `format!()`
84+
|
85+
LL | usr_println!(true, "{:.3}", format!("abcde"));
86+
| ~~~~~~
87+
help: if the current behavior is intentional, remove the format specifiers
88+
|
89+
LL - usr_println!(true, "{:.3}", format_args!("abcde"));
90+
LL + usr_println!(true, "{}", format_args!("abcde"));
91+
|
92+
93+
error: format specifiers have no effect on `format_args!()`
94+
--> tests/ui/unused_format_specs.rs:54:25
95+
|
96+
LL | usr_println!(true, "{:5}.", format_args_from_macro!());
97+
| ^^^^
98+
|
99+
= help: for the width to apply consider using `format!()`
100+
help: if the current behavior is intentional, remove the format specifiers
101+
|
102+
LL - usr_println!(true, "{:5}.", format_args_from_macro!());
103+
LL + usr_println!(true, "{}.", format_args_from_macro!());
104+
|
105+
106+
error: format specifiers have no effect on `format_args!()`
107+
--> tests/ui/unused_format_specs.rs:58:25
108+
|
109+
LL | usr_println!(true, "{args:5}");
110+
| ^^^^^^^^
111+
|
112+
= help: for the width to apply consider using `format!()`
113+
help: if the current behavior is intentional, remove the format specifiers
114+
|
115+
LL - usr_println!(true, "{args:5}");
116+
LL + usr_println!(true, "{args}");
117+
|
118+
119+
error: aborting due to 8 previous errors
62120

0 commit comments

Comments
 (0)