1
1
use clippy_utils:: diagnostics:: { span_lint_and_sugg, span_lint_and_then} ;
2
2
use clippy_utils:: is_diag_trait_item;
3
- use clippy_utils:: macros:: { is_format_macro, FormatArgsExpn } ;
4
- use clippy_utils:: source:: snippet_opt;
3
+ use clippy_utils:: macros:: FormatParamKind :: { Implicit , Named , Numbered , Starred } ;
4
+ use clippy_utils:: macros:: { is_format_macro, FormatArgsExpn , FormatParam } ;
5
+ use clippy_utils:: source:: { expand_past_previous_comma, snippet_opt} ;
5
6
use clippy_utils:: ty:: implements_trait;
6
7
use if_chain:: if_chain;
7
8
use itertools:: Itertools ;
8
9
use rustc_errors:: Applicability ;
9
- use rustc_hir:: { Expr , ExprKind , HirId } ;
10
+ use rustc_hir:: { Expr , ExprKind , HirId , Path , QPath } ;
10
11
use rustc_lint:: { LateContext , LateLintPass } ;
11
12
use rustc_middle:: ty:: adjustment:: { Adjust , Adjustment } ;
12
13
use rustc_middle:: ty:: Ty ;
@@ -64,7 +65,33 @@ declare_clippy_lint! {
64
65
"`to_string` applied to a type that implements `Display` in format args"
65
66
}
66
67
67
- declare_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS ] ) ;
68
+ declare_clippy_lint ! {
69
+ /// ### What it does
70
+ /// Detect when a variable is not inlined in a format string,
71
+ /// and suggests to inline it.
72
+ ///
73
+ /// ### Why is this bad?
74
+ /// Non-inlined code is slightly more difficult to read and understand,
75
+ /// as it requires arguments to be matched against the format string.
76
+ /// The inlined syntax, where allowed, is simpler.
77
+ ///
78
+ /// ### Example
79
+ /// ```rust
80
+ /// # let foo = 42;
81
+ /// format!("{}", foo);
82
+ /// ```
83
+ /// Use instead:
84
+ /// ```rust
85
+ /// # let foo = 42;
86
+ /// format!("{foo}");
87
+ /// ```
88
+ #[ clippy:: version = "1.64.0" ]
89
+ pub NEEDLESS_FORMAT_ARGS ,
90
+ nursery,
91
+ "using non-inlined variables in `format!` calls"
92
+ }
93
+
94
+ declare_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , NEEDLESS_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS ] ) ;
68
95
69
96
impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
70
97
fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
@@ -76,7 +103,23 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
76
103
if is_format_macro( cx, macro_def_id) ;
77
104
if let ExpnKind :: Macro ( _, name) = outermost_expn_data. kind;
78
105
then {
106
+ // if at least some of the arguments/format/precision are referenced by an index,
107
+ // e.g. format!("{} {1}", foo, bar) or format!("{:1$}", foo, 2)
108
+ // we cannot remove an argument from a list until we support renumbering.
109
+ // We are OK if we inline all numbered arguments.
110
+ let mut do_inline = true ;
111
+ // if we find one or more suggestions, this becomes a Vec of replacements
112
+ let mut inline_spans = None ;
79
113
for arg in & format_args. args {
114
+ if do_inline {
115
+ do_inline = check_inline( cx, & arg. param, ParamType :: Argument , & mut inline_spans) ;
116
+ }
117
+ if do_inline && let Some ( p) = arg. format. width. param( ) {
118
+ do_inline = check_inline( cx, & p, ParamType :: Width , & mut inline_spans) ;
119
+ }
120
+ if do_inline && let Some ( p) = arg. format. precision. param( ) {
121
+ do_inline = check_inline( cx, & p, ParamType :: Precision , & mut inline_spans) ;
122
+ }
80
123
if !arg. format. is_default( ) {
81
124
continue ;
82
125
}
@@ -86,11 +129,64 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86
129
check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87
130
check_to_string_in_format_args( cx, name, arg. param. value) ;
88
131
}
132
+ if do_inline && let Some ( inline_spans) = inline_spans {
133
+ span_lint_and_then(
134
+ cx,
135
+ NEEDLESS_FORMAT_ARGS ,
136
+ outermost_expn_data. call_site,
137
+ "variables can be used directly in the `format!` string" ,
138
+ |diag| {
139
+ diag. multipart_suggestion( "change this to" , inline_spans, Applicability :: MachineApplicable ) ;
140
+ } ,
141
+ ) ;
142
+ }
89
143
}
90
144
}
91
145
}
92
146
}
93
147
148
+ #[ derive( Debug , Clone , Copy ) ]
149
+ enum ParamType {
150
+ Argument ,
151
+ Width ,
152
+ Precision ,
153
+ }
154
+
155
+ fn check_inline (
156
+ cx : & LateContext < ' _ > ,
157
+ param : & FormatParam < ' _ > ,
158
+ ptype : ParamType ,
159
+ inline_spans : & mut Option < Vec < ( Span , String ) > > ,
160
+ ) -> bool {
161
+ if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
162
+ && let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
163
+ && let Path { span, segments, .. } = path
164
+ && let [ segment] = segments
165
+ {
166
+ let c = inline_spans. get_or_insert_with ( Vec :: new) ;
167
+ // TODO: Note the inconsistency here, that we may want to address separately:
168
+ // implicit, numbered, and starred `param.span` spans the whole relevant string:
169
+ // the empty space between `{}`, or the entire value `1$`, `.2$`, or `.*`
170
+ // but the named argument spans just the name itself, without the surrounding `.` and `$`.
171
+ let replacement = if param. kind == Numbered || param. kind == Starred {
172
+ match ptype {
173
+ ParamType :: Argument => segment. ident . name . to_string ( ) ,
174
+ ParamType :: Width => format ! ( "{}$" , segment. ident. name) ,
175
+ ParamType :: Precision => format ! ( ".{}$" , segment. ident. name) ,
176
+ }
177
+ } else {
178
+ segment. ident . name . to_string ( )
179
+ } ;
180
+ c. push ( ( param. span , replacement) ) ;
181
+ let arg_span = expand_past_previous_comma ( cx, * span) ;
182
+ c. push ( ( arg_span, String :: new ( ) ) ) ;
183
+ true // successful inlining, continue checking
184
+ } else {
185
+ // if we can't inline a numbered argument, we can't continue
186
+ param. kind != Numbered
187
+ }
188
+ }
189
+
94
190
fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
95
191
if expn_data. call_site . from_expansion ( ) {
96
192
outermost_expn_data ( expn_data. call_site . ctxt ( ) . outer_expn_data ( ) )
@@ -170,7 +266,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
170
266
}
171
267
}
172
268
173
- // Returns true if `hir_id` is referred to by multiple format params
269
+ /// Returns true if `hir_id` is referred to by multiple format params
174
270
fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
175
271
args. params ( )
176
272
. filter ( |param| param. value . hir_id == hir_id)
0 commit comments