11use clippy_utils:: diagnostics:: { span_lint_and_sugg, span_lint_and_then} ;
22use 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} ;
56use clippy_utils:: ty:: implements_trait;
67use if_chain:: if_chain;
78use itertools:: Itertools ;
89use rustc_errors:: Applicability ;
9- use rustc_hir:: { Expr , ExprKind , HirId } ;
10+ use rustc_hir:: { Expr , ExprKind , HirId , Path , QPath } ;
1011use rustc_lint:: { LateContext , LateLintPass } ;
1112use rustc_middle:: ty:: adjustment:: { Adjust , Adjustment } ;
1213use rustc_middle:: ty:: Ty ;
@@ -64,7 +65,33 @@ declare_clippy_lint! {
6465 "`to_string` applied to a type that implements `Display` in format args"
6566}
6667
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 ] ) ;
6895
6996impl < ' tcx > LateLintPass < ' tcx > for FormatArgs {
7097 fn check_expr ( & mut self , cx : & LateContext < ' tcx > , expr : & ' tcx Expr < ' tcx > ) {
@@ -76,7 +103,23 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
76103 if is_format_macro( cx, macro_def_id) ;
77104 if let ExpnKind :: Macro ( _, name) = outermost_expn_data. kind;
78105 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 ;
79113 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+ }
80123 if !arg. format. is_default( ) {
81124 continue ;
82125 }
@@ -86,11 +129,64 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
86129 check_format_in_format_args( cx, outermost_expn_data. call_site, name, arg. param. value) ;
87130 check_to_string_in_format_args( cx, name, arg. param. value) ;
88131 }
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+ }
89143 }
90144 }
91145 }
92146}
93147
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+
94190fn outermost_expn_data ( expn_data : ExpnData ) -> ExpnData {
95191 if expn_data. call_site . from_expansion ( ) {
96192 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
170266 }
171267}
172268
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
174270fn is_aliased ( args : & FormatArgsExpn < ' _ > , hir_id : HirId ) -> bool {
175271 args. params ( )
176272 . filter ( |param| param. value . hir_id == hir_id)
0 commit comments