Skip to content

Commit

Permalink
Add format_in_format_args and to_string_in_format_args lints
Browse files Browse the repository at this point in the history
  • Loading branch information
smoelius committed Oct 7, 2021
1 parent 872f321 commit da82920
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 46 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,7 @@ Released 2018-09-13
[`for_loops_over_fallibles`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles
[`forget_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_copy
[`forget_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_ref
[`format_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_in_format_args
[`from_iter_instead_of_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_iter_instead_of_collect
[`from_over_into`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_over_into
[`from_str_radix_10`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_str_radix_10
Expand Down Expand Up @@ -3013,6 +3014,7 @@ Released 2018-09-13
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
[`todo`]: https://rust-lang.github.io/rust-clippy/master/index.html#todo
[`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments
[`too_many_lines`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines
Expand Down
48 changes: 2 additions & 46 deletions clippy_lints/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::format::{check_unformatted, is_display_arg};
use clippy_utils::higher::FormatExpn;
use clippy_utils::last_path_segment;
use clippy_utils::source::{snippet_opt, snippet_with_applicability};
use clippy_utils::sugg::Sugg;
use if_chain::if_chain;
use rustc_errors::Applicability;
use rustc_hir::{BorrowKind, Expr, ExprKind, QPath};
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::{declare_lint_pass, declare_tool_lint};
Expand Down Expand Up @@ -106,47 +106,3 @@ fn span_useless_format(cx: &LateContext<'_>, span: Span, mut sugg: String, mut a
applicability,
);
}

fn is_display_arg(expr: &Expr<'_>) -> bool {
if_chain! {
if let ExprKind::Call(_, [_, fmt]) = expr.kind;
if let ExprKind::Path(QPath::Resolved(_, path)) = fmt.kind;
if let [.., t, _] = path.segments;
if t.ident.name == sym::Display;
then { true } else { false }
}
}

/// Checks if the expression matches
/// ```rust,ignore
/// &[_ {
/// format: _ {
/// width: _::Implied,
/// precision: _::Implied,
/// ...
/// },
/// ...,
/// }]
/// ```
fn check_unformatted(expr: &Expr<'_>) -> bool {
if_chain! {
if let ExprKind::AddrOf(BorrowKind::Ref, _, expr) = expr.kind;
if let ExprKind::Array([expr]) = expr.kind;
// struct `core::fmt::rt::v1::Argument`
if let ExprKind::Struct(_, fields, _) = expr.kind;
if let Some(format_field) = fields.iter().find(|f| f.ident.name == sym::format);
// struct `core::fmt::rt::v1::FormatSpec`
if let ExprKind::Struct(_, fields, _) = format_field.expr.kind;
if let Some(precision_field) = fields.iter().find(|f| f.ident.name == sym::precision);
if let ExprKind::Path(ref precision_path) = precision_field.expr.kind;
if last_path_segment(precision_path).ident.name == sym::Implied;
if let Some(width_field) = fields.iter().find(|f| f.ident.name == sym::width);
if let ExprKind::Path(ref width_qpath) = width_field.expr.kind;
if last_path_segment(width_qpath).ident.name == sym::Implied;
then {
return true;
}
}

false
}
262 changes: 262 additions & 0 deletions clippy_lints/src/format_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::format::{check_unformatted, is_display_arg};
use clippy_utils::higher::{FormatArgsExpn, FormatExpn};
use clippy_utils::source::snippet_opt;
use clippy_utils::ty::implements_trait;
use clippy_utils::{get_trait_def_id, match_def_path, paths};
use if_chain::if_chain;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{BytePos, ExpnKind, Span, Symbol};

declare_clippy_lint! {
/// ### What it does
/// Warns on `format!` within the arguments of another macro that does
/// formatting such as `format!` itself, `write!` or `println!`. Suggests
/// inlining the `format!` call.
///
/// ### Why is this bad?
/// The recommended code is both shorter and avoids a temporary allocation.
///
/// ### Example
/// ```rust
/// # use std::panic::Location;
/// println!("error: {}", format!("something failed at {}", Location::caller()));
/// ```
/// Use instead:
/// ```rust
/// # use std::panic::Location;
/// println!("error: something failed at {}", Location::caller());
/// ```
pub FORMAT_IN_FORMAT_ARGS,
perf,
"`format!` used in a macro that does formatting"
}

declare_lint_pass!(FormatInFormatArgs => [FORMAT_IN_FORMAT_ARGS]);

impl<'tcx> LateLintPass<'tcx> for FormatInFormatArgs {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
check_expr(cx, expr, format_in_format_args);
}
}

declare_clippy_lint! {
/// ### What it does
/// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string)
/// applied to a type that implements [`Display`](https://doc.rust-lang.org/std/fmt/trait.Display.html)
/// in a macro that does formatting.
///
/// ### Why is this bad?
/// Since the type implements `Display`, the use of `to_string` is
/// unnecessary.
///
/// ### Example
/// ```rust
/// # use std::panic::Location;
/// println!("error: something failed at {}", Location::caller().to_string());
/// ```
/// Use instead:
/// ```rust
/// # use std::panic::Location;
/// println!("error: something failed at {}", Location::caller());
/// ```
pub TO_STRING_IN_FORMAT_ARGS,
perf,
"`to_string` applied to a type that implements `Display` in format args"
}

declare_lint_pass!(ToStringInFormatArgs => [TO_STRING_IN_FORMAT_ARGS]);

impl<'tcx> LateLintPass<'tcx> for ToStringInFormatArgs {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
check_expr(cx, expr, to_string_in_format_args);
}
}

fn check_expr<'tcx, F>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, check_value: F)
where
F: Fn(&LateContext<'_>, &FormatArgsExpn<'_>, Span, Symbol, usize, &Expr<'_>) -> bool,
{
if_chain! {
if let Some(format_args) = FormatArgsExpn::parse(expr);
let call_site = expr.span.ctxt().outer_expn_data().call_site;
if call_site.from_expansion();
let expn_data = call_site.ctxt().outer_expn_data();
if let ExpnKind::Macro(_, name) = expn_data.kind;
if format_args.fmt_expr.map_or(true, check_unformatted);
then {
assert_eq!(format_args.args.len(), format_args.value_args.len());
for (i, (arg, value)) in format_args.args.iter().zip(format_args.value_args.iter()).enumerate() {
if !is_display_arg(arg) {
continue;
}
if check_value(cx, &format_args, expn_data.call_site, name, i, value) {
break;
}
}
}
}
}

fn format_in_format_args(
cx: &LateContext<'_>,
format_args: &FormatArgsExpn<'_>,
call_site: Span,
name: Symbol,
i: usize,
value: &Expr<'_>,
) -> bool {
if_chain! {
if let Some(FormatExpn{ format_args: inner_format_args, .. }) = FormatExpn::parse(value);
if let Some(format_string) = snippet_opt(cx, format_args.format_string_span);
if let Some(inner_format_string) = snippet_opt(cx, inner_format_args.format_string_span);
if let Some((sugg, applicability)) = format_in_format_args_sugg(
cx,
name,
&format_string,
&format_args.value_args,
i,
&inner_format_string,
&inner_format_args.value_args
);
then {
span_lint_and_sugg(
cx,
FORMAT_IN_FORMAT_ARGS,
trim_semicolon(cx, call_site),
&format!("`format!` in `{}!` args", name),
"inline the `format!` call",
sugg,
applicability,
);
// Report only the first instance.
return true;
}
}
false
}

fn to_string_in_format_args(
cx: &LateContext<'_>,
_format_args: &FormatArgsExpn<'_>,
_call_site: Span,
name: Symbol,
_i: usize,
value: &Expr<'_>,
) -> bool {
if_chain! {
if let ExprKind::MethodCall(_, _, args, _) = value.kind;
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id);
if match_def_path(cx, method_def_id, &paths::TO_STRING_METHOD);
if let Some(receiver) = args.get(0);
let ty = cx.typeck_results().expr_ty(receiver);
if let Some(display_trait_id) = get_trait_def_id(cx, &paths::DISPLAY_TRAIT);
if implements_trait(cx, ty, display_trait_id, &[]);
if let Some(snippet) = snippet_opt(cx, value.span);
if let Some(dot) = snippet.rfind('.');
then {
let span = value.span.with_lo(
value.span.lo() + BytePos(u32::try_from(dot).unwrap())
);
span_lint_and_sugg(
cx,
TO_STRING_IN_FORMAT_ARGS,
span,
&format!("`to_string` applied to a type that implements `Display` in `{}!` args", name),
"remove this",
String::new(),
Applicability::MachineApplicable,
);
}
}
false
}

fn format_in_format_args_sugg(
cx: &LateContext<'_>,
name: Symbol,
format_string: &str,
values: &[&Expr<'_>],
i: usize,
inner_format_string: &str,
inner_values: &[&Expr<'_>],
) -> Option<(String, Applicability)> {
let (left, right) = split_format_string(format_string, i);
// If the inner format string is raw, the user is on their own.
let (new_format_string, applicability) = if inner_format_string.starts_with('r') {
(left + ".." + &right, Applicability::HasPlaceholders)
} else {
(
left + &trim_quotes(inner_format_string) + &right,
Applicability::MachineApplicable,
)
};
let values = values
.iter()
.map(|value| snippet_opt(cx, value.span))
.collect::<Option<Vec<_>>>()?;
let inner_values = inner_values
.iter()
.map(|value| snippet_opt(cx, value.span))
.collect::<Option<Vec<_>>>()?;
let new_values = itertools::join(
values
.iter()
.take(i)
.chain(inner_values.iter())
.chain(values.iter().skip(i + 1)),
", ",
);
Some((
format!(r#"{}!({}, {})"#, name, new_format_string, new_values),
applicability,
))
}

fn split_format_string(format_string: &str, i: usize) -> (String, String) {
let mut iter = format_string.chars();
for j in 0..=i {
assert!(advance(&mut iter) == '}' || j < i);
}

let right = iter.collect::<String>();

let size = format_string.len();
let right_size = right.len();
let left_size = size - right_size;
assert!(left_size >= 2);
let left = std::str::from_utf8(&format_string.as_bytes()[..left_size - 2])
.unwrap()
.to_owned();
(left, right)
}

fn advance(iter: &mut std::str::Chars<'_>) -> char {
loop {
let first_char = iter.next().unwrap();
if first_char != '{' {
continue;
}
let second_char = iter.next().unwrap();
if second_char == '{' {
continue;
}
return second_char;
}
}

fn trim_quotes(string_literal: &str) -> String {
let len = string_literal.chars().count();
assert!(len >= 2);
string_literal.chars().skip(1).take(len - 2).collect()
}

fn trim_semicolon(cx: &LateContext<'_>, span: Span) -> Span {
snippet_opt(cx, span).map_or(span, |snippet| {
let snippet = snippet.trim_end_matches(';');
span.with_hi(span.lo() + BytePos(u32::try_from(snippet.len()).unwrap()))
})
}
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.register_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
LintId::of(float_equality_without_abs::FLOAT_EQUALITY_WITHOUT_ABS),
LintId::of(float_literal::EXCESSIVE_PRECISION),
LintId::of(format::USELESS_FORMAT),
LintId::of(format_args::FORMAT_IN_FORMAT_ARGS),
LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS),
LintId::of(formatting::POSSIBLE_MISSING_COMMA),
LintId::of(formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING),
LintId::of(formatting::SUSPICIOUS_ELSE_FORMATTING),
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ store.register_lints(&[
floating_point_arithmetic::IMPRECISE_FLOPS,
floating_point_arithmetic::SUBOPTIMAL_FLOPS,
format::USELESS_FORMAT,
format_args::FORMAT_IN_FORMAT_ARGS,
format_args::TO_STRING_IN_FORMAT_ARGS,
formatting::POSSIBLE_MISSING_COMMA,
formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING,
formatting::SUSPICIOUS_ELSE_FORMATTING,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.register_perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
store.register_group(true, "clippy::perf", Some("clippy_perf"), vec![
LintId::of(entry::MAP_ENTRY),
LintId::of(escape::BOXED_LOCAL),
LintId::of(format_args::FORMAT_IN_FORMAT_ARGS),
LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS),
LintId::of(large_const_arrays::LARGE_CONST_ARRAYS),
LintId::of(large_enum_variant::LARGE_ENUM_VARIANT),
LintId::of(loops::MANUAL_MEMCPY),
Expand Down
3 changes: 3 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ mod float_equality_without_abs;
mod float_literal;
mod floating_point_arithmetic;
mod format;
mod format_args;
mod formatting;
mod from_over_into;
mod from_str_radix_10;
Expand Down Expand Up @@ -769,6 +770,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(move || Box::new(if_then_panic::IfThenPanic));
let enable_raw_pointer_heuristic_for_send = conf.enable_raw_pointer_heuristic_for_send;
store.register_late_pass(move || Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new(enable_raw_pointer_heuristic_for_send)));
store.register_late_pass(move || Box::new(format_args::FormatInFormatArgs));
store.register_late_pass(move || Box::new(format_args::ToStringInFormatArgs));
}

#[rustfmt::skip]
Expand Down
Loading

0 comments on commit da82920

Please sign in to comment.