1 use arrayvec
::ArrayVec
;
2 use clippy_utils
::diagnostics
::{span_lint_and_sugg, span_lint_and_then}
;
3 use clippy_utils
::is_diag_trait_item
;
4 use clippy_utils
::macros
::{
5 find_format_arg_expr
, find_format_args
, format_arg_removal_span
, format_placeholder_format_span
, is_assert_macro
,
6 is_format_macro
, is_panic
, root_macro_call
, root_macro_call_first_node
, FormatParamUsage
,
8 use clippy_utils
::msrvs
::{self, Msrv}
;
9 use clippy_utils
::source
::snippet_opt
;
10 use clippy_utils
::ty
::{implements_trait, is_type_lang_item}
;
11 use if_chain
::if_chain
;
12 use itertools
::Itertools
;
14 FormatArgPosition
, FormatArgPositionKind
, FormatArgsPiece
, FormatArgumentKind
, FormatCount
, FormatOptions
,
15 FormatPlaceholder
, FormatTrait
,
19 SuggestionStyle
::{CompletelyHidden, ShowCode}
,
21 use rustc_hir
::{Expr, ExprKind, LangItem}
;
22 use rustc_lint
::{LateContext, LateLintPass, LintContext}
;
23 use rustc_middle
::ty
::adjustment
::{Adjust, Adjustment}
;
24 use rustc_middle
::ty
::Ty
;
25 use rustc_session
::{declare_tool_lint, impl_lint_pass}
;
26 use rustc_span
::def_id
::DefId
;
27 use rustc_span
::edition
::Edition
::Edition2021
;
28 use rustc_span
::{sym, Span, Symbol}
;
30 declare_clippy_lint
! {
32 /// Detects `format!` within the arguments of another macro that does
33 /// formatting such as `format!` itself, `write!` or `println!`. Suggests
34 /// inlining the `format!` call.
36 /// ### Why is this bad?
37 /// The recommended code is both shorter and avoids a temporary allocation.
41 /// # use std::panic::Location;
42 /// println!("error: {}", format!("something failed at {}", Location::caller()));
46 /// # use std::panic::Location;
47 /// println!("error: something failed at {}", Location::caller());
49 #[clippy::version = "1.58.0"]
50 pub FORMAT_IN_FORMAT_ARGS
,
52 "`format!` used in a macro that does formatting"
55 declare_clippy_lint
! {
57 /// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string)
58 /// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)
59 /// in a macro that does formatting.
61 /// ### Why is this bad?
62 /// Since the type implements `Display`, the use of `to_string` is
67 /// # use std::panic::Location;
68 /// println!("error: something failed at {}", Location::caller().to_string());
72 /// # use std::panic::Location;
73 /// println!("error: something failed at {}", Location::caller());
75 #[clippy::version = "1.58.0"]
76 pub TO_STRING_IN_FORMAT_ARGS
,
78 "`to_string` applied to a type that implements `Display` in format args"
81 declare_clippy_lint
! {
83 /// Detect when a variable is not inlined in a format string,
84 /// and suggests to inline it.
86 /// ### Why is this bad?
87 /// Non-inlined code is slightly more difficult to read and understand,
88 /// as it requires arguments to be matched against the format string.
89 /// The inlined syntax, where allowed, is simpler.
96 /// format!("{}", var);
97 /// format!("{v:?}", v = var);
98 /// format!("{0} {0}", var);
99 /// format!("{0:1$}", var, width);
100 /// format!("{:.*}", prec, var);
107 /// format!("{var}");
108 /// format!("{var:?}");
109 /// format!("{var} {var}");
110 /// format!("{var:width$}");
111 /// format!("{var:.prec$}");
114 /// If allow-mixed-uninlined-format-args is set to false in clippy.toml,
115 /// the following code will also trigger the lint:
118 /// format!("{} {}", var, 1+2);
123 /// format!("{var} {}", 1+2);
126 /// ### Known Problems
128 /// If a format string contains a numbered argument that cannot be inlined
129 /// nothing will be suggested, e.g. `println!("{0}={1}", var, 1+2)`.
130 #[clippy::version = "1.66.0"]
131 pub UNINLINED_FORMAT_ARGS
,
133 "using non-inlined variables in `format!` calls"
136 declare_clippy_lint
! {
138 /// Detects [formatting parameters] that have no effect on the output of
139 /// `format!()`, `println!()` or similar macros.
141 /// ### Why is this bad?
142 /// Shorter format specifiers are easier to read, it may also indicate that
143 /// an expected formatting operation such as adding padding isn't happening.
147 /// println!("{:.}", 1.0);
149 /// println!("not padded: {:5}", format_args!("..."));
153 /// println!("{}", 1.0);
155 /// println!("not padded: {}", format_args!("..."));
157 /// println!("padded: {:5}", format!("..."));
160 /// [formatting parameters]: https://doc.rust-lang.org/std/fmt/index.html#formatting-parameters
161 #[clippy::version = "1.66.0"]
162 pub UNUSED_FORMAT_SPECS
,
164 "use of a format specifier that has no effect"
167 impl_lint_pass
!(FormatArgs
=> [
168 FORMAT_IN_FORMAT_ARGS
,
169 TO_STRING_IN_FORMAT_ARGS
,
170 UNINLINED_FORMAT_ARGS
,
174 pub struct FormatArgs
{
181 pub fn new(msrv
: Msrv
, allow_mixed_uninlined_format_args
: bool
) -> Self {
184 ignore_mixed
: allow_mixed_uninlined_format_args
,
189 impl<'tcx
> LateLintPass
<'tcx
> for FormatArgs
{
190 fn check_expr(&mut self, cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'tcx
>) {
191 let Some(macro_call
) = root_macro_call_first_node(cx
, expr
) else { return }
;
192 if !is_format_macro(cx
, macro_call
.def_id
) {
195 let name
= cx
.tcx
.item_name(macro_call
.def_id
);
197 find_format_args(cx
, expr
, macro_call
.expn
, |format_args
| {
198 for piece
in &format_args
.template
{
199 if let FormatArgsPiece
::Placeholder(placeholder
) = piece
200 && let Ok(index
) = placeholder
.argument
.index
201 && let Some(arg
) = format_args
.arguments
.all_args().get(index
)
203 let arg_expr
= find_format_arg_expr(expr
, arg
);
205 check_unused_format_specifier(cx
, placeholder
, arg_expr
);
207 if placeholder
.format_trait
!= FormatTrait
::Display
208 || placeholder
.format_options
!= FormatOptions
::default()
209 || is_aliased(format_args
, index
)
214 if let Ok(arg_hir_expr
) = arg_expr
{
215 check_format_in_format_args(cx
, macro_call
.span
, name
, arg_hir_expr
);
216 check_to_string_in_format_args(cx
, name
, arg_hir_expr
);
221 if self.msrv
.meets(msrvs
::FORMAT_ARGS_CAPTURE
) {
222 check_uninlined_args(cx
, format_args
, macro_call
.span
, macro_call
.def_id
, self.ignore_mixed
);
227 extract_msrv_attr
!(LateContext
);
230 fn check_unused_format_specifier(
231 cx
: &LateContext
<'_
>,
232 placeholder
: &FormatPlaceholder
,
233 arg_expr
: Result
<&Expr
<'_
>, &rustc_ast
::Expr
>,
235 let ty_or_ast_expr
= arg_expr
.map(|expr
| cx
.typeck_results().expr_ty(expr
).peel_refs());
237 let is_format_args
= match ty_or_ast_expr
{
238 Ok(ty
) => is_type_lang_item(cx
, ty
, LangItem
::FormatArguments
),
239 Err(expr
) => matches
!(expr
.peel_parens_and_refs().kind
, rustc_ast
::ExprKind
::FormatArgs(_
)),
242 let options
= &placeholder
.format_options
;
244 let arg_span
= match arg_expr
{
245 Ok(expr
) => expr
.span
,
246 Err(expr
) => expr
.span
,
249 if let Some(placeholder_span
) = placeholder
.span
251 && *options
!= FormatOptions
::default()
257 "format specifiers have no effect on `format_args!()`",
259 let mut suggest_format
= |spec
| {
260 let message
= format
!("for the {spec} to apply consider using `format!()`");
262 if let Some(mac_call
) = root_macro_call(arg_span
)
263 && cx
.tcx
.is_diagnostic_item(sym
::format_args_macro
, mac_call
.def_id
)
265 diag
.span_suggestion(
266 cx
.sess().source_map().span_until_char(mac_call
.span
, '
!'
),
269 Applicability
::MaybeIncorrect
,
276 if options
.width
.is_some() {
277 suggest_format("width");
280 if options
.precision
.is_some() {
281 suggest_format("precision");
284 if let Some(format_span
) = format_placeholder_format_span(placeholder
) {
285 diag
.span_suggestion_verbose(
287 "if the current behavior is intentional, remove the format specifiers",
289 Applicability
::MaybeIncorrect
,
297 fn check_uninlined_args(
298 cx
: &LateContext
<'_
>,
299 args
: &rustc_ast
::FormatArgs
,
304 if args
.span
.from_expansion() {
307 if call_site
.edition() < Edition2021
&& (is_panic(cx
, def_id
) || is_assert_macro(cx
, def_id
)) {
308 // panic!, assert!, and debug_assert! before 2021 edition considers a single string argument as
313 let mut fixes
= Vec
::new();
314 // If any of the arguments are referenced by an index number,
315 // and that argument is not a simple variable and cannot be inlined,
316 // we cannot remove any other arguments in the format string,
317 // because the index numbers might be wrong after inlining.
318 // Example of an un-inlinable format: print!("{}{1}", foo, 2)
319 for (pos
, usage
) in format_arg_positions(args
) {
320 if !check_one_arg(args
, pos
, usage
, &mut fixes
, ignore_mixed
) {
325 if fixes
.is_empty() {
329 // multiline span display suggestion is sometimes broken: https://github.com/rust-lang/rust/pull/102729#discussion_r988704308
330 // in those cases, make the code suggestion hidden
331 let multiline_fix
= fixes
.iter().any(|(span
, _
)| cx
.sess().source_map().is_multiline(*span
));
333 // Suggest removing each argument only once, for example in `format!("{0} {0}", arg)`.
334 fixes
.sort_unstable_by_key(|(span
, _
)| *span
);
335 fixes
.dedup_by_key(|(span
, _
)| *span
);
339 UNINLINED_FORMAT_ARGS
,
341 "variables can be used directly in the `format!` string",
343 diag
.multipart_suggestion_with_style(
346 Applicability
::MachineApplicable
,
347 if multiline_fix { CompletelyHidden }
else { ShowCode }
,
354 args
: &rustc_ast
::FormatArgs
,
355 pos
: &FormatArgPosition
,
356 usage
: FormatParamUsage
,
357 fixes
: &mut Vec
<(Span
, String
)>,
360 let index
= pos
.index
.unwrap();
361 let arg
= &args
.arguments
.all_args()[index
];
363 if !matches
!(arg
.kind
, FormatArgumentKind
::Captured(_
))
364 && let rustc_ast
::ExprKind
::Path(None
, path
) = &arg
.expr
.kind
365 && let [segment
] = path
.segments
.as_slice()
366 && segment
.args
.is_none()
367 && let Some(arg_span
) = format_arg_removal_span(args
, index
)
368 && let Some(pos_span
) = pos
.span
370 let replacement
= match usage
{
371 FormatParamUsage
::Argument
=> segment
.ident
.name
.to_string(),
372 FormatParamUsage
::Width
=> format
!("{}$", segment
.ident
.name
),
373 FormatParamUsage
::Precision
=> format
!(".{}$", segment
.ident
.name
),
375 fixes
.push((pos_span
, replacement
));
376 fixes
.push((arg_span
, String
::new()));
377 true // successful inlining, continue checking
379 // Do not continue inlining (return false) in case
380 // * if we can't inline a numbered argument, e.g. `print!("{0} ...", foo.bar, ...)`
381 // * if allow_mixed_uninlined_format_args is false and this arg hasn't been inlined already
382 pos
.kind
!= FormatArgPositionKind
::Number
383 && (!ignore_mixed
|| matches
!(arg
.kind
, FormatArgumentKind
::Captured(_
)))
387 fn check_format_in_format_args(cx
: &LateContext
<'_
>, call_site
: Span
, name
: Symbol
, arg
: &Expr
<'_
>) {
388 let expn_data
= arg
.span
.ctxt().outer_expn_data();
389 if expn_data
.call_site
.from_expansion() {
392 let Some(mac_id
) = expn_data
.macro_def_id
else { return }
;
393 if !cx
.tcx
.is_diagnostic_item(sym
::format_macro
, mac_id
) {
398 FORMAT_IN_FORMAT_ARGS
,
400 &format
!("`format!` in `{name}!` args"),
403 "combine the `format!(..)` arguments with the outer `{name}!(..)` call"
405 diag
.help("or consider changing `format!` to `format_args!`");
410 fn check_to_string_in_format_args(cx
: &LateContext
<'_
>, name
: Symbol
, value
: &Expr
<'_
>) {
412 if !value
.span
.from_expansion();
413 if let ExprKind
::MethodCall(_
, receiver
, [], to_string_span
) = value
.kind
;
414 if let Some(method_def_id
) = cx
.typeck_results().type_dependent_def_id(value
.hir_id
);
415 if is_diag_trait_item(cx
, method_def_id
, sym
::ToString
);
416 let receiver_ty
= cx
.typeck_results().expr_ty(receiver
);
417 if let Some(display_trait_id
) = cx
.tcx
.get_diagnostic_item(sym
::Display
);
418 let (n_needed_derefs
, target
) =
419 count_needed_derefs(receiver_ty
, cx
.typeck_results().expr_adjustments(receiver
).iter());
420 if implements_trait(cx
, target
, display_trait_id
, &[]);
421 if let Some(sized_trait_id
) = cx
.tcx
.lang_items().sized_trait();
422 if let Some(receiver_snippet
) = snippet_opt(cx
, receiver
.span
);
424 let needs_ref
= !implements_trait(cx
, receiver_ty
, sized_trait_id
, &[]);
425 if n_needed_derefs
== 0 && !needs_ref
{
428 TO_STRING_IN_FORMAT_ARGS
,
429 to_string_span
.with_lo(receiver
.span
.hi()),
431 "`to_string` applied to a type that implements `Display` in `{name}!` args"
435 Applicability
::MachineApplicable
,
440 TO_STRING_IN_FORMAT_ARGS
,
443 "`to_string` applied to a type that implements `Display` in `{name}!` args"
447 "{}{:*>n_needed_derefs$}{receiver_snippet}",
448 if needs_ref { "&" }
else { "" }
,
451 Applicability
::MachineApplicable
,
458 fn format_arg_positions(
459 format_args
: &rustc_ast
::FormatArgs
,
460 ) -> impl Iterator
<Item
= (&FormatArgPosition
, FormatParamUsage
)> {
461 format_args
.template
.iter().flat_map(|piece
| match piece
{
462 FormatArgsPiece
::Placeholder(placeholder
) => {
463 let mut positions
= ArrayVec
::<_
, 3>::new();
465 positions
.push((&placeholder
.argument
, FormatParamUsage
::Argument
));
466 if let Some(FormatCount
::Argument(position
)) = &placeholder
.format_options
.width
{
467 positions
.push((position
, FormatParamUsage
::Width
));
469 if let Some(FormatCount
::Argument(position
)) = &placeholder
.format_options
.precision
{
470 positions
.push((position
, FormatParamUsage
::Precision
));
475 FormatArgsPiece
::Literal(_
) => ArrayVec
::new(),
479 /// Returns true if the format argument at `index` is referred to by multiple format params
480 fn is_aliased(format_args
: &rustc_ast
::FormatArgs
, index
: usize) -> bool
{
481 format_arg_positions(format_args
)
482 .filter(|(position
, _
)| position
.index
== Ok(index
))
487 fn count_needed_derefs
<'tcx
, I
>(mut ty
: Ty
<'tcx
>, mut iter
: I
) -> (usize, Ty
<'tcx
>)
489 I
: Iterator
<Item
= &'tcx Adjustment
<'tcx
>>,
492 let mut n_needed
= 0;
494 if let Some(Adjustment
{
495 kind
: Adjust
::Deref(overloaded_deref
),
500 if overloaded_deref
.is_some() {
505 return (n_needed
, ty
);