Skip to content

Commit e0ccd7a

Browse files
committed
Implement var inlining in format!()
Implement rust-lang#8368 - a new lint to inline format arguments such as `print!("{}", var)` into `print!("{var}")`. code | suggestion | comment ---|---|--- `print!("{}", var)` | `print!("{var}")` | simple variables `print!("{0}", var)` | `print!("{var}")` | positional variables `print!("{v}", v=var)` | `print!("{var}")` | named variables `print!("{0} {0}", var)` | `print!("{var} {var}")` | aliased variables `print!("{0:1$}", var, width)` | `print!("{var:width$}")` | width support `print!("{0:.1$}", var, prec)` | `print!("{var:.prec$}")` | precision support `print!("{:.*}", prec, var)` | `print!("{var:.prec$}")` | asterisk support code | suggestion | comment ---|---|--- `print!("{0}={1}", var, 1+2)` | `print!("{var}={0}", 1+2)` | Format string uses an indexed argument that cannot be inlined. Supporting this case requires re-indexing of the format string. changelog: [`inline-format-args`]: A new lint to inline format arguments, i.e. `print!("{}", var)` into `print!("{var}")`
1 parent 2ddbc86 commit e0ccd7a

File tree

13 files changed

+1246
-21
lines changed

13 files changed

+1246
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3783,6 +3783,7 @@ Released 2018-09-13
37833783
[`inline_asm_x86_att_syntax`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_asm_x86_att_syntax
37843784
[`inline_asm_x86_intel_syntax`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_asm_x86_intel_syntax
37853785
[`inline_fn_without_body`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_fn_without_body
3786+
[`inline_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_format_args
37863787
[`inspect_for_each`]: https://rust-lang.github.io/rust-clippy/master/index.html#inspect_for_each
37873788
[`int_plus_one`]: https://rust-lang.github.io/rust-clippy/master/index.html#int_plus_one
37883789
[`integer_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#integer_arithmetic

clippy_lints/src/format_args.rs

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
22
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};
56
use clippy_utils::ty::implements_trait;
67
use if_chain::if_chain;
78
use itertools::Itertools;
89
use rustc_errors::Applicability;
9-
use rustc_hir::{Expr, ExprKind, HirId};
10+
use rustc_hir::{Expr, ExprKind, HirId, Path, QPath};
1011
use rustc_lint::{LateContext, LateLintPass};
1112
use rustc_middle::ty::adjustment::{Adjust, Adjustment};
1213
use 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 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]);
6895

6996
impl<'tcx> LateLintPass<'tcx> for FormatArgs {
7097
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
@@ -76,21 +103,87 @@ 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 {
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);
82122
}
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) {
84124
continue;
85125
}
86126
check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg.param.value);
87127
check_to_string_in_format_args(cx, name, arg.param.value);
88128
}
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+
}
89140
}
90141
}
91142
}
92143
}
93144

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+
94187
fn outermost_expn_data(expn_data: ExpnData) -> ExpnData {
95188
if expn_data.call_site.from_expansion() {
96189
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
175268
}
176269
}
177270

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
179272
fn is_aliased(args: &FormatArgsExpn<'_>, hir_id: HirId) -> bool {
180273
args.params()
181274
.filter(|param| param.value.hir_id == hir_id)

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ store.register_lints(&[
158158
floating_point_arithmetic::SUBOPTIMAL_FLOPS,
159159
format::USELESS_FORMAT,
160160
format_args::FORMAT_IN_FORMAT_ARGS,
161+
format_args::INLINE_FORMAT_ARGS,
161162
format_args::TO_STRING_IN_FORMAT_ARGS,
162163
format_impl::PRINT_IN_FORMAT_IMPL,
163164
format_impl::RECURSIVE_FORMAT_IMPL,

clippy_lints/src/lib.register_nursery.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ store.register_group(true, "clippy::nursery", Some("clippy_nursery"), vec![
1010
LintId::of(fallible_impl_from::FALLIBLE_IMPL_FROM),
1111
LintId::of(floating_point_arithmetic::IMPRECISE_FLOPS),
1212
LintId::of(floating_point_arithmetic::SUBOPTIMAL_FLOPS),
13+
LintId::of(format_args::INLINE_FORMAT_ARGS),
1314
LintId::of(future_not_send::FUTURE_NOT_SEND),
1415
LintId::of(index_refutable_slice::INDEX_REFUTABLE_SLICE),
1516
LintId::of(let_if_seq::USELESS_LET_IF_SEQ),

clippy_lints/src/write.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
22
use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn, MacroCall};
3-
use clippy_utils::source::snippet_opt;
3+
use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
44
use rustc_ast::LitKind;
55
use rustc_errors::Applicability;
66
use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
77
use rustc_lint::{LateContext, LateLintPass, LintContext};
88
use rustc_session::{declare_tool_lint, impl_lint_pass};
9-
use rustc_span::{sym, BytePos, Span};
9+
use rustc_span::{sym, BytePos};
1010

1111
declare_clippy_lint! {
1212
/// ### What it does
@@ -542,10 +542,3 @@ fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
542542

543543
if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
544544
}
545-
546-
// Expand from `writeln!(o, "")` to `writeln!(o, "")`
547-
// ^^ ^^^^
548-
fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
549-
let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
550-
extended.with_lo(extended.lo() - BytePos(1))
551-
}

clippy_utils/src/macros.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,10 @@ fn span_from_inner(base: SpanData, inner: rpf::InnerSpan) -> Span {
549549
pub enum FormatParamKind {
550550
/// An implicit parameter , such as `{}` or `{:?}`.
551551
Implicit,
552-
/// A parameter with an explicit number, or an asterisk precision. e.g. `{1}`, `{0:?}`,
553-
/// `{:.0$}` or `{:.*}`.
552+
/// A parameter with an explicit number, e.g. `{1}`, `{0:?}`, or `{:.0$}`
554553
Numbered,
554+
/// A parameter with an asterisk precision. e.g. `{:.*}`.
555+
Starred,
555556
/// A named parameter with a named `value_arg`, such as the `x` in `format!("{x}", x = 1)`.
556557
Named(Symbol),
557558
/// An implicit named parameter, such as the `y` in `format!("{y}")`.
@@ -631,9 +632,12 @@ impl<'tcx> Count<'tcx> {
631632
span,
632633
values,
633634
)?),
634-
rpf::Count::CountIsParam(_) | rpf::Count::CountIsStar(_) => {
635+
rpf::Count::CountIsParam(_) => {
635636
Self::Param(FormatParam::new(FormatParamKind::Numbered, position?, inner?, values)?)
636637
},
638+
rpf::Count::CountIsStar(_) => {
639+
Self::Param(FormatParam::new(FormatParamKind::Starred, position?, inner?, values)?)
640+
},
637641
rpf::Count::CountImplied => Self::Implied,
638642
})
639643
}
@@ -723,7 +727,7 @@ pub struct FormatArg<'tcx> {
723727
pub struct FormatArgsExpn<'tcx> {
724728
/// The format string literal.
725729
pub format_string: FormatString,
726-
// The format arguments, such as `{:?}`.
730+
/// The format arguments, such as `{:?}`.
727731
pub args: Vec<FormatArg<'tcx>>,
728732
/// Has an added newline due to `println!()`/`writeln!()`/etc. The last format string part will
729733
/// include this added newline.

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ macro_rules! msrv_aliases {
1313
// names may refer to stabilized feature flags or library items
1414
msrv_aliases! {
1515
1,62,0 { BOOL_THEN_SOME }
16+
1,58,0 { INLINE_FORMAT_ARGS }
1617
1,53,0 { OR_PATTERNS, MANUAL_BITS, BTREE_MAP_RETAIN, BTREE_SET_RETAIN, ARRAY_INTO_ITERATOR }
1718
1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST }
1819
1,51,0 { BORROW_AS_PTR, UNSIGNED_ABS }

clippy_utils/src/source.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ pub fn trim_span(sm: &SourceMap, span: Span) -> Span {
392392
.span()
393393
}
394394

395+
/// Expand a span to include a preceding comma
396+
/// ```rust,ignore
397+
/// writeln!(o, "") -> writeln!(o, "")
398+
/// ^^ ^^^^
399+
/// ```
400+
pub fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
401+
let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
402+
extended.with_lo(extended.lo() - BytePos(1))
403+
}
404+
395405
#[cfg(test)]
396406
mod test {
397407
use super::{reindent_multiline, without_block_comments};

src/docs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ docs! {
207207
"inline_asm_x86_att_syntax",
208208
"inline_asm_x86_intel_syntax",
209209
"inline_fn_without_body",
210+
"inline_format_args",
210211
"inspect_for_each",
211212
"int_plus_one",
212213
"integer_arithmetic",

src/docs/inline_format_args.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
### What it does
2+
Detect when a variable is not inlined in a format string,
3+
and suggests to inline it.
4+
5+
### Why is this bad?
6+
Non-inlined code is slightly more difficult to read and understand,
7+
as it requires arguments to be matched against the format string.
8+
The inlined syntax, where allowed, is simpler.
9+
10+
### Example
11+
```
12+
format!("{}", foo);
13+
```
14+
Use instead:
15+
```
16+
format!("{foo}");
17+
```

0 commit comments

Comments
 (0)