1
1
use clippy_utils:: diagnostics:: { span_lint_and_sugg, span_lint_and_then} ;
2
- use clippy_utils:: is_diag_trait_item ;
3
- use clippy_utils:: macros:: { is_format_macro, FormatArgsExpn } ;
4
- use clippy_utils:: source:: snippet_opt;
2
+ use clippy_utils:: macros :: FormatParamKind :: { Implicit , Named , Numbered , Starred } ;
3
+ use clippy_utils:: macros:: { is_format_macro, FormatArgsExpn , FormatParam , FormatParamUsage } ;
4
+ use clippy_utils:: source:: { expand_past_previous_comma , snippet_opt} ;
5
5
use clippy_utils:: ty:: implements_trait;
6
+ use clippy_utils:: { is_diag_trait_item, meets_msrv, msrvs} ;
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 ;
13
- use rustc_session:: { declare_lint_pass, declare_tool_lint} ;
14
+ use rustc_semver:: RustcVersion ;
15
+ use rustc_session:: { declare_tool_lint, impl_lint_pass} ;
14
16
use rustc_span:: { sym, ExpnData , ExpnKind , Span , Symbol } ;
15
17
16
18
declare_clippy_lint ! {
@@ -64,7 +66,67 @@ declare_clippy_lint! {
64
66
"`to_string` applied to a type that implements `Display` in format args"
65
67
}
66
68
67
- declare_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS ] ) ;
69
+ declare_clippy_lint ! {
70
+ /// ### What it does
71
+ /// Detect when a variable is not inlined in a format string,
72
+ /// and suggests to inline it.
73
+ ///
74
+ /// ### Why is this bad?
75
+ /// Non-inlined code is slightly more difficult to read and understand,
76
+ /// as it requires arguments to be matched against the format string.
77
+ /// The inlined syntax, where allowed, is simpler.
78
+ ///
79
+ /// ### Example
80
+ /// ```rust
81
+ /// # let var = 42;
82
+ /// # let width = 1;
83
+ /// # let prec = 2;
84
+ /// format!("{}", var);
85
+ /// format!("{v:?}", v = var);
86
+ /// format!("{0} {0}", var);
87
+ /// format!("{0:1$}", var, width);
88
+ /// format!("{:.*}", prec, var);
89
+ /// ```
90
+ /// Use instead:
91
+ /// ```rust
92
+ /// # let var = 42;
93
+ /// # let width = 1;
94
+ /// # let prec = 2;
95
+ /// format!("{var}");
96
+ /// format!("{var:?}");
97
+ /// format!("{var} {var}");
98
+ /// format!("{var:width$}");
99
+ /// format!("{var:.prec$}");
100
+ /// ```
101
+ ///
102
+ /// ### Known Problems
103
+ ///
104
+ /// There may be a false positive if the format string is expanded from certain proc macros:
105
+ ///
106
+ /// ```ignore
107
+ /// println!(indoc!("{}"), var);
108
+ /// ```
109
+ ///
110
+ /// If a format string contains a numbered argument that cannot be inlined
111
+ /// nothing will be suggested, e.g. `println!("{0}={1}", var, 1+2)`.
112
+ #[ clippy:: version = "1.65.0" ]
113
+ pub UNINLINED_FORMAT_ARGS ,
114
+ pedantic,
115
+ "using non-inlined variables in `format!` calls"
116
+ }
117
+
118
+ impl_lint_pass ! ( FormatArgs => [ FORMAT_IN_FORMAT_ARGS , UNINLINED_FORMAT_ARGS , TO_STRING_IN_FORMAT_ARGS ] ) ;
119
+
120
+ pub struct FormatArgs {
121
+ msrv : Option < RustcVersion > ,
122
+ }
123
+
124
+ impl FormatArgs {
125
+ #[ must_use]
126
+ pub fn new ( msrv : Option < RustcVersion > ) -> Self {
127
+ Self { msrv }
128
+ }
129
+ }
68
130
69
131
impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
70
132
fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
@@ -86,9 +148,73 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86
148
check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87
149
check_to_string_in_format_args( cx, name, arg. param. value) ;
88
150
}
151
+ if meets_msrv( self . msrv, msrvs:: FORMAT_ARGS_CAPTURE ) {
152
+ check_uninlined_args( cx, & format_args, outermost_expn_data. call_site) ;
153
+ }
89
154
}
90
155
}
91
156
}
157
+
158
+ extract_msrv_attr ! ( LateContext ) ;
159
+ }
160
+
161
+ fn check_uninlined_args ( cx : & LateContext < ' _ > , args : & FormatArgsExpn < ' _ > , call_site : Span ) {
162
+ if args. format_string . span . from_expansion ( ) {
163
+ return ;
164
+ }
165
+
166
+ let mut fixes = Vec :: new ( ) ;
167
+ // If any of the arguments are referenced by an index number,
168
+ // and that argument is not a simple variable and cannot be inlined,
169
+ // we cannot remove any other arguments in the format string,
170
+ // because the index numbers might be wrong after inlining.
171
+ // Example of an un-inlinable format: print!("{}{1}", foo, 2)
172
+ if !args. params ( ) . all ( |p| check_one_arg ( cx, & p, & mut fixes) ) || fixes. is_empty ( ) {
173
+ return ;
174
+ }
175
+
176
+ // FIXME: Properly ignore a rare case where the format string is wrapped in a macro.
177
+ // Example: `format!(indoc!("{}"), foo);`
178
+ // If inlined, they will cause a compilation error:
179
+ // > to avoid ambiguity, `format_args!` cannot capture variables
180
+ // > when the format string is expanded from a macro
181
+ // @Alexendoo explanation:
182
+ // > indoc! is a proc macro that is producing a string literal with its span
183
+ // > set to its input it's not marked as from expansion, and since it's compatible
184
+ // > tokenization wise clippy_utils::is_from_proc_macro wouldn't catch it either
185
+ // This might be a relatively expensive test, so do it only we are ready to replace.
186
+ // See more examples in tests/ui/uninlined_format_args.rs
187
+
188
+ span_lint_and_then (
189
+ cx,
190
+ UNINLINED_FORMAT_ARGS ,
191
+ call_site,
192
+ "variables can be used directly in the `format!` string" ,
193
+ |diag| {
194
+ diag. multipart_suggestion ( "change this to" , fixes, Applicability :: MachineApplicable ) ;
195
+ } ,
196
+ ) ;
197
+ }
198
+
199
+ fn check_one_arg ( cx : & LateContext < ' _ > , param : & FormatParam < ' _ > , fixes : & mut Vec < ( Span , String ) > ) -> bool {
200
+ if matches ! ( param. kind, Implicit | Starred | Named ( _) | Numbered )
201
+ && let ExprKind :: Path ( QPath :: Resolved ( None , path) ) = param. value . kind
202
+ && let Path { span, segments, .. } = path
203
+ && let [ segment] = segments
204
+ {
205
+ let replacement = match param. usage {
206
+ FormatParamUsage :: Argument => segment. ident . name . to_string ( ) ,
207
+ FormatParamUsage :: Width => format ! ( "{}$" , segment. ident. name) ,
208
+ FormatParamUsage :: Precision => format ! ( ".{}$" , segment. ident. name) ,
209
+ } ;
210
+ fixes. push ( ( param. span , replacement) ) ;
211
+ let arg_span = expand_past_previous_comma ( cx, * span) ;
212
+ fixes. push ( ( arg_span, String :: new ( ) ) ) ;
213
+ true // successful inlining, continue checking
214
+ } else {
215
+ // if we can't inline a numbered argument, we can't continue
216
+ param. kind != Numbered
217
+ }
92
218
}
93
219
94
220
fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
@@ -170,7 +296,7 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex
170
296
}
171
297
}
172
298
173
- // Returns true if `hir_id` is referred to by multiple format params
299
+ /// Returns true if `hir_id` is referred to by multiple format params
174
300
fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
175
301
args. params ( )
176
302
. filter ( |param| param. value . hir_id == hir_id)
0 commit comments