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 INLINE_FORMAT_ARGS ,
90
+ nursery,
91
+ "using non-inlined variables in `format!` calls"
92
+ }
93
+
94
+ declare_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS , INLINE_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,21 +103,87 @@ 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 {
80
- if !arg. format. is_default( ) {
81
- continue ;
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) ;
82
122
}
83
- if is_aliased( & format_args, arg. param. value. hir_id) {
123
+ if !arg . format . is_default ( ) || is_aliased( & format_args, arg. param. value. hir_id) {
84
124
continue ;
85
125
}
86
126
check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87
127
check_to_string_in_format_args( cx, name, arg. param. value) ;
88
128
}
129
+ if do_inline && let Some ( inline_spans) = inline_spans {
130
+ span_lint_and_then(
131
+ cx,
132
+ INLINE_FORMAT_ARGS ,
133
+ outermost_expn_data. call_site,
134
+ "variables can be used directly in the `format!` string" ,
135
+ |diag| {
136
+ diag. multipart_suggestion( "change this to" , inline_spans, Applicability :: MachineApplicable ) ;
137
+ } ,
138
+ ) ;
139
+ }
89
140
}
90
141
}
91
142
}
92
143
}
93
144
145
+ #[ derive( Debug , Clone , Copy ) ]
146
+ enum ParamType {
147
+ Argument ,
148
+ Width ,
149
+ Precision ,
150
+ }
151
+
152
+ fn check_inline (
153
+ cx : & LateContext < ' _ > ,
154
+ param : & FormatParam < ' _ > ,
155
+ ptype : ParamType ,
156
+ inline_spans : & mut Option < Vec < ( Span , String ) > > ,
157
+ ) -> bool {
158
+ if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
159
+ && let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
160
+ && let Path { span, segments, .. } = path
161
+ && let [ segment] = segments
162
+ {
163
+ let c = inline_spans. get_or_insert_with ( Vec :: new) ;
164
+ // TODO: Note the inconsistency here, that we may want to address separately:
165
+ // implicit, numbered, and starred `param.span` spans the whole relevant string:
166
+ // the empty space between `{}`, or the entire value `1$`, `.2$`, or `.*`
167
+ // but the named argument spans just the name itself, without the surrounding `.` and `$`.
168
+ let replacement = if param. kind == Numbered || param. kind == Starred {
169
+ match ptype {
170
+ ParamType :: Argument => segment. ident . name . to_string ( ) ,
171
+ ParamType :: Width => format ! ( "{}$" , segment. ident. name) ,
172
+ ParamType :: Precision => format ! ( ".{}$" , segment. ident. name) ,
173
+ }
174
+ } else {
175
+ segment. ident . name . to_string ( )
176
+ } ;
177
+ c. push ( ( param. span , replacement) ) ;
178
+ let arg_span = expand_past_previous_comma ( cx, * span) ;
179
+ c. push ( ( arg_span, String :: new ( ) ) ) ;
180
+ true // successful inlining, continue checking
181
+ } else {
182
+ // if we can't inline a numbered argument, we can't continue
183
+ param. kind != Numbered
184
+ }
185
+ }
186
+
94
187
fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
95
188
if expn_data. call_site . from_expansion ( ) {
96
189
outermost_expn_data ( expn_data. call_site . ctxt ( ) . outer_expn_data ( ) )
@@ -175,7 +268,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
175
268
}
176
269
}
177
270
178
- // Returns true if `hir_id` is referred to by multiple format params
271
+ /// Returns true if `hir_id` is referred to by multiple format params
179
272
fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
180
273
args. params ( )
181
274
. filter ( |param| param. value . hir_id == hir_id)
0 commit comments