From 4f049f5a695b003168125a3251f5c0295e64f261 Mon Sep 17 00:00:00 2001 From: Alex Macleod Date: Thu, 18 Aug 2022 17:25:02 +0000 Subject: [PATCH] Refactor `FormatArgsExpn` --- clippy_lints/src/format.rs | 24 +- clippy_lints/src/format_args.rs | 31 +- clippy_lints/src/format_impl.rs | 11 +- .../src/methods/uninit_assumed_init.rs | 4 +- .../src/transmute/transmuting_null.rs | 4 +- clippy_utils/Cargo.toml | 1 + clippy_utils/src/lib.rs | 15 +- clippy_utils/src/macros.rs | 674 +++++++++++++----- clippy_utils/src/paths.rs | 1 - tests/ui/format_args.fixed | 13 +- tests/ui/format_args.rs | 13 +- tests/ui/format_args.stderr | 42 +- 12 files changed, 580 insertions(+), 253 deletions(-) diff --git a/clippy_lints/src/format.rs b/clippy_lints/src/format.rs index 925a8cb8deed..0c5851cdbed2 100644 --- a/clippy_lints/src/format.rs +++ b/clippy_lints/src/format.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn}; -use clippy_utils::source::{snippet_opt, snippet_with_applicability}; +use clippy_utils::source::snippet_with_applicability; use clippy_utils::sugg::Sugg; use if_chain::if_chain; use rustc_errors::Applicability; @@ -56,29 +56,27 @@ impl<'tcx> LateLintPass<'tcx> for UselessFormat { }; let mut applicability = Applicability::MachineApplicable; - if format_args.value_args.is_empty() { - match *format_args.format_string_parts { + if format_args.args.is_empty() { + match *format_args.format_string.parts { [] => span_useless_format_empty(cx, call_site, "String::new()".to_owned(), applicability), [_] => { - if let Some(s_src) = snippet_opt(cx, format_args.format_string_span) { - // Simulate macro expansion, converting {{ and }} to { and }. - let s_expand = s_src.replace("{{", "{").replace("}}", "}"); - let sugg = format!("{}.to_string()", s_expand); - span_useless_format(cx, call_site, sugg, applicability); - } + // Simulate macro expansion, converting {{ and }} to { and }. + let s_expand = format_args.format_string.snippet.replace("{{", "{").replace("}}", "}"); + let sugg = format!("{}.to_string()", s_expand); + span_useless_format(cx, call_site, sugg, applicability); }, [..] => {}, } - } else if let [value] = *format_args.value_args { + } else if let [arg] = &*format_args.args { + let value = arg.param.value; if_chain! { - if format_args.format_string_parts == [kw::Empty]; + if format_args.format_string.parts == [kw::Empty]; if match cx.typeck_results().expr_ty(value).peel_refs().kind() { ty::Adt(adt, _) => cx.tcx.is_diagnostic_item(sym::String, adt.did()), ty::Str => true, _ => false, }; - if let Some(args) = format_args.args(); - if args.iter().all(|arg| arg.format_trait == sym::Display && !arg.has_string_formatting()); + if !arg.format.has_string_formatting(); then { let is_new_string = match value.kind { ExprKind::Binary(..) => true, diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 1e6feaac26c3..5347ff880ce0 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -1,11 +1,12 @@ use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::is_diag_trait_item; -use clippy_utils::macros::{is_format_macro, FormatArgsArg, FormatArgsExpn}; +use clippy_utils::macros::{is_format_macro, FormatArgsExpn}; use clippy_utils::source::snippet_opt; use clippy_utils::ty::implements_trait; use if_chain::if_chain; +use itertools::Itertools; use rustc_errors::Applicability; -use rustc_hir::{Expr, ExprKind}; +use rustc_hir::{Expr, ExprKind, HirId}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::adjustment::{Adjust, Adjustment}; use rustc_middle::ty::Ty; @@ -74,20 +75,16 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs { if let Some(macro_def_id) = outermost_expn_data.macro_def_id; if is_format_macro(cx, macro_def_id); if let ExpnKind::Macro(_, name) = outermost_expn_data.kind; - if let Some(args) = format_args.args(); then { - for (i, arg) in args.iter().enumerate() { - if arg.format_trait != sym::Display { + for arg in &format_args.args { + if arg.format.has_string_formatting() { continue; } - if arg.has_string_formatting() { + if is_aliased(&format_args, arg.param.value.hir_id) { continue; } - if is_aliased(&args, i) { - continue; - } - check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg.value); - check_to_string_in_format_args(cx, name, arg.value); + check_format_in_format_args(cx, outermost_expn_data.call_site, name, arg.param.value); + check_to_string_in_format_args(cx, name, arg.param.value); } } } @@ -167,12 +164,12 @@ fn check_to_string_in_format_args(cx: &LateContext<'_>, name: Symbol, value: &Ex } } -// Returns true if `args[i]` "refers to" or "is referred to by" another argument. -fn is_aliased(args: &[FormatArgsArg<'_>], i: usize) -> bool { - let value = args[i].value; - args.iter() - .enumerate() - .any(|(j, arg)| i != j && std::ptr::eq(value, arg.value)) +// Returns true if `hir_id` is referred to by multiple format params +fn is_aliased(args: &FormatArgsExpn<'_>, hir_id: HirId) -> bool { + args.params() + .filter(|param| param.value.hir_id == hir_id) + .at_most_one() + .is_err() } fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>) diff --git a/clippy_lints/src/format_impl.rs b/clippy_lints/src/format_impl.rs index 04b5be6c80ec..d8bc0bf08f2b 100644 --- a/clippy_lints/src/format_impl.rs +++ b/clippy_lints/src/format_impl.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; -use clippy_utils::macros::{is_format_macro, root_macro_call_first_node, FormatArgsArg, FormatArgsExpn}; +use clippy_utils::macros::{is_format_macro, root_macro_call_first_node, FormatArg, FormatArgsExpn}; use clippy_utils::{get_parent_as_impl, is_diag_trait_item, path_to_local, peel_ref_operators}; use if_chain::if_chain; use rustc_errors::Applicability; @@ -168,10 +168,9 @@ fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, if let macro_def_id = outer_macro.def_id; if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, outer_macro.expn); if is_format_macro(cx, macro_def_id); - if let Some(args) = format_args.args(); then { - for arg in args { - if arg.format_trait != impl_trait.name { + for arg in format_args.args { + if arg.format.r#trait != impl_trait.name { continue; } check_format_arg_self(cx, expr, &arg, impl_trait); @@ -180,11 +179,11 @@ fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, } } -fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &FormatArgsArg<'_>, impl_trait: FormatTrait) { +fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &FormatArg<'_>, impl_trait: FormatTrait) { // Handle multiple dereferencing of references e.g. &&self // Handle dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl) // Since the argument to fmt is itself a reference: &self - let reference = peel_ref_operators(cx, arg.value); + let reference = peel_ref_operators(cx, arg.param.value); let map = cx.tcx.hir(); // Is the reference self? if path_to_local(reference).map(|x| map.name(x)) == Some(kw::SelfLower) { diff --git a/clippy_lints/src/methods/uninit_assumed_init.rs b/clippy_lints/src/methods/uninit_assumed_init.rs index 77d21f1d3730..a1c6294737cf 100644 --- a/clippy_lints/src/methods/uninit_assumed_init.rs +++ b/clippy_lints/src/methods/uninit_assumed_init.rs @@ -1,5 +1,5 @@ use clippy_utils::diagnostics::span_lint; -use clippy_utils::{is_expr_diagnostic_item, ty::is_uninit_value_valid_for_ty}; +use clippy_utils::{is_path_diagnostic_item, ty::is_uninit_value_valid_for_ty}; use if_chain::if_chain; use rustc_hir as hir; use rustc_lint::LateContext; @@ -12,7 +12,7 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr if_chain! { if let hir::ExprKind::Call(callee, args) = recv.kind; if args.is_empty(); - if is_expr_diagnostic_item(cx, callee, sym::maybe_uninit_uninit); + if is_path_diagnostic_item(cx, callee, sym::maybe_uninit_uninit); if !is_uninit_value_valid_for_ty(cx, cx.typeck_results().expr_ty_adjusted(expr)); then { span_lint( diff --git a/clippy_lints/src/transmute/transmuting_null.rs b/clippy_lints/src/transmute/transmuting_null.rs index c4981124f396..d8e349af7af8 100644 --- a/clippy_lints/src/transmute/transmuting_null.rs +++ b/clippy_lints/src/transmute/transmuting_null.rs @@ -1,6 +1,6 @@ use clippy_utils::consts::{constant_context, Constant}; use clippy_utils::diagnostics::span_lint; -use clippy_utils::is_expr_diagnostic_item; +use clippy_utils::is_path_diagnostic_item; use if_chain::if_chain; use rustc_ast::LitKind; use rustc_hir::{Expr, ExprKind}; @@ -45,7 +45,7 @@ pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, arg: &'t // `std::mem::transmute(std::ptr::null::())` if_chain! { if let ExprKind::Call(func1, []) = arg.kind; - if is_expr_diagnostic_item(cx, func1, sym::ptr_null); + if is_path_diagnostic_item(cx, func1, sym::ptr_null); then { span_lint(cx, TRANSMUTING_NULL, expr.span, LINT_MSG); return true; diff --git a/clippy_utils/Cargo.toml b/clippy_utils/Cargo.toml index a688050f63a6..c36bca06507d 100644 --- a/clippy_utils/Cargo.toml +++ b/clippy_utils/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] arrayvec = { version = "0.7", default-features = false } if_chain = "1.0" +itertools = "0.10.1" rustc-semver = "1.1" [features] diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs index dc772e5efeef..9308f085214f 100644 --- a/clippy_utils/src/lib.rs +++ b/clippy_utils/src/lib.rs @@ -27,6 +27,7 @@ extern crate rustc_infer; extern crate rustc_lexer; extern crate rustc_lint; extern crate rustc_middle; +extern crate rustc_parse_format; extern crate rustc_session; extern crate rustc_span; extern crate rustc_target; @@ -371,15 +372,19 @@ pub fn match_qpath(path: &QPath<'_>, segments: &[&str]) -> bool { /// If the expression is a path, resolves it to a `DefId` and checks if it matches the given path. /// -/// Please use `is_expr_diagnostic_item` if the target is a diagnostic item. +/// Please use `is_path_diagnostic_item` if the target is a diagnostic item. pub fn is_expr_path_def_path(cx: &LateContext<'_>, expr: &Expr<'_>, segments: &[&str]) -> bool { path_def_id(cx, expr).map_or(false, |id| match_def_path(cx, id, segments)) } -/// If the expression is a path, resolves it to a `DefId` and checks if it matches the given -/// diagnostic item. -pub fn is_expr_diagnostic_item(cx: &LateContext<'_>, expr: &Expr<'_>, diag_item: Symbol) -> bool { - path_def_id(cx, expr).map_or(false, |id| cx.tcx.is_diagnostic_item(diag_item, id)) +/// If `maybe_path` is a path node which resolves to an item, resolves it to a `DefId` and checks if +/// it matches the given diagnostic item. +pub fn is_path_diagnostic_item<'tcx>( + cx: &LateContext<'_>, + maybe_path: &impl MaybePath<'tcx>, + diag_item: Symbol, +) -> bool { + path_def_id(cx, maybe_path).map_or(false, |id| cx.tcx.is_diagnostic_item(diag_item, id)) } /// THIS METHOD IS DEPRECATED and will eventually be removed since it does not match against the diff --git a/clippy_utils/src/macros.rs b/clippy_utils/src/macros.rs index a268e339bb13..37d8f1e458df 100644 --- a/clippy_utils/src/macros.rs +++ b/clippy_utils/src/macros.rs @@ -1,16 +1,21 @@ #![allow(clippy::similar_names)] // `expr` and `expn` +use crate::is_path_diagnostic_item; +use crate::source::snippet_opt; use crate::visitors::expr_visitor_no_bodies; use arrayvec::ArrayVec; -use if_chain::if_chain; +use itertools::{izip, Either, Itertools}; use rustc_ast::ast::LitKind; use rustc_hir::intravisit::Visitor; use rustc_hir::{self as hir, Expr, ExprKind, HirId, Node, QPath}; +use rustc_lexer::unescape::unescape_literal; +use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind}; use rustc_lint::LateContext; +use rustc_parse_format::{self as rpf, Alignment}; use rustc_span::def_id::DefId; use rustc_span::hygiene::{self, MacroKind, SyntaxContext}; -use rustc_span::{sym, ExpnData, ExpnId, ExpnKind, Span, Symbol}; +use rustc_span::{sym, BytePos, ExpnData, ExpnId, ExpnKind, Pos, Span, SpanData, Symbol}; use std::ops::ControlFlow; const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[ @@ -332,121 +337,495 @@ fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> } } -/// A parsed `format_args!` expansion +/// The format string doesn't exist in the HIR, so we reassemble it from source code #[derive(Debug)] -pub struct FormatArgsExpn<'tcx> { - /// Span of the first argument, the format string - pub format_string_span: Span, - /// The format string split by formatted args like `{..}` - pub format_string_parts: Vec, - /// Values passed after the format string - pub value_args: Vec<&'tcx Expr<'tcx>>, - /// Each element is a `value_args` index and a formatting trait (e.g. `sym::Debug`) - pub formatters: Vec<(usize, Symbol)>, - /// List of `fmt::v1::Argument { .. }` expressions. If this is empty, - /// then `formatters` represents the format args (`{..}`). - /// If this is non-empty, it represents the format args, and the `position` - /// parameters within the struct expressions are indexes of `formatters`. - pub specs: Vec<&'tcx Expr<'tcx>>, +pub struct FormatString { + /// Span of the whole format string literal, including `[r#]"`. + pub span: Span, + /// Snippet of the whole format string literal, including `[r#]"`. + pub snippet: String, + /// If the string is raw `r"..."`/`r#""#`, how many `#`s does it have on each side. + pub style: Option, + /// The unescaped value of the format string, e.g. `"val – {}"` for the literal + /// `"val \u{2013} {}"`. + pub unescaped: String, + /// The format string split by format args like `{..}`. + pub parts: Vec, } -impl<'tcx> FormatArgsExpn<'tcx> { - /// Parses an expanded `format_args!` or `format_args_nl!` invocation - pub fn parse(cx: &LateContext<'_>, expr: &'tcx Expr<'tcx>) -> Option { - macro_backtrace(expr.span).find(|macro_call| { - matches!( - cx.tcx.item_name(macro_call.def_id), - sym::const_format_args | sym::format_args | sym::format_args_nl - ) - })?; - let mut format_string_span: Option = None; - let mut format_string_parts: Vec = Vec::new(); - let mut value_args: Vec<&Expr<'_>> = Vec::new(); - let mut formatters: Vec<(usize, Symbol)> = Vec::new(); - let mut specs: Vec<&Expr<'_>> = Vec::new(); - expr_visitor_no_bodies(|e| { - // if we're still inside of the macro definition... - if e.span.ctxt() == expr.span.ctxt() { - // ArgumentV1::new_() - if_chain! { - if let ExprKind::Call(callee, [val]) = e.kind; - if let ExprKind::Path(QPath::TypeRelative(ty, seg)) = callee.kind; - if let hir::TyKind::Path(QPath::Resolved(_, path)) = ty.kind; - if path.segments.last().unwrap().ident.name == sym::ArgumentV1; - if seg.ident.name.as_str().starts_with("new_"); - then { - let val_idx = if_chain! { - if val.span.ctxt() == expr.span.ctxt(); - if let ExprKind::Field(_, field) = val.kind; - if let Ok(idx) = field.name.as_str().parse(); - then { - // tuple index - idx - } else { - // assume the value expression is passed directly - formatters.len() - } - }; - let fmt_trait = match seg.ident.name.as_str() { - "new_display" => "Display", - "new_debug" => "Debug", - "new_lower_exp" => "LowerExp", - "new_upper_exp" => "UpperExp", - "new_octal" => "Octal", - "new_pointer" => "Pointer", - "new_binary" => "Binary", - "new_lower_hex" => "LowerHex", - "new_upper_hex" => "UpperHex", - _ => unreachable!(), - }; - formatters.push((val_idx, Symbol::intern(fmt_trait))); - } - } - if let ExprKind::Struct(QPath::Resolved(_, path), ..) = e.kind { - if path.segments.last().unwrap().ident.name == sym::Argument { - specs.push(e); - } +impl FormatString { + fn new(cx: &LateContext<'_>, pieces: &Expr<'_>) -> Option { + // format_args!(r"a {} b \", 1); + // + // expands to + // + // ::core::fmt::Arguments::new_v1(&["a ", " b \\"], + // &[::core::fmt::ArgumentV1::new_display(&1)]); + // + // where `pieces` is the expression `&["a ", " b \\"]`. It has the span of `r"a {} b \"` + let span = pieces.span; + let snippet = snippet_opt(cx, span)?; + + let (inner, style) = match tokenize(&snippet).next()?.kind { + TokenKind::Literal { kind, .. } => { + let style = match kind { + LiteralKind::Str { .. } => None, + LiteralKind::RawStr { n_hashes: Some(n), .. } => Some(n.into()), + _ => return None, + }; + + let start = style.map_or(1, |n| 2 + n); + let end = snippet.len() - style.map_or(1, |n| 1 + n); + + (&snippet[start..end], style) + }, + _ => return None, + }; + + let mode = if style.is_some() { + unescape::Mode::RawStr + } else { + unescape::Mode::Str + }; + + let mut unescaped = String::with_capacity(inner.len()); + unescape_literal(inner, mode, &mut |_, ch| { + unescaped.push(ch.unwrap()); + }); + + let mut parts = Vec::new(); + expr_visitor_no_bodies(|expr| { + if let ExprKind::Lit(lit) = &expr.kind { + if let LitKind::Str(symbol, _) = lit.node { + parts.push(symbol); } - // walk through the macro expansion - return true; } - // assume that the first expr with a differing context represents - // (and has the span of) the format string - if format_string_span.is_none() { - format_string_span = Some(e.span); - let span = e.span; - // walk the expr and collect string literals which are format string parts - expr_visitor_no_bodies(|e| { - if e.span.ctxt() != span.ctxt() { - // defensive check, probably doesn't happen - return false; - } - if let ExprKind::Lit(lit) = &e.kind { - if let LitKind::Str(symbol, _s) = lit.node { - format_string_parts.push(symbol); - } - } - true - }) - .visit_expr(e); + + true + }) + .visit_expr(pieces); + + Some(Self { + span, + snippet, + style, + unescaped, + parts, + }) + } +} + +struct FormatArgsValues<'tcx> { + /// See `FormatArgsExpn::value_args` + value_args: Vec<&'tcx Expr<'tcx>>, + /// Maps an `rt::v1::Argument::position` or an `rt::v1::Count::Param` to its index in + /// `value_args` + pos_to_value_index: Vec, + /// Used to check if a value is declared inline & to resolve `InnerSpan`s. + format_string_span: SpanData, +} + +impl<'tcx> FormatArgsValues<'tcx> { + fn new(args: &'tcx Expr<'tcx>, format_string_span: SpanData) -> Self { + let mut pos_to_value_index = Vec::new(); + let mut value_args = Vec::new(); + expr_visitor_no_bodies(|expr| { + if expr.span.ctxt() == args.span.ctxt() { + // ArgumentV1::new_() + // ArgumentV1::from_usize() + if let ExprKind::Call(callee, [val]) = expr.kind + && let ExprKind::Path(QPath::TypeRelative(ty, _)) = callee.kind + && let hir::TyKind::Path(QPath::Resolved(_, path)) = ty.kind + && path.segments.last().unwrap().ident.name == sym::ArgumentV1 + { + let val_idx = if val.span.ctxt() == expr.span.ctxt() + && let ExprKind::Field(_, field) = val.kind + && let Ok(idx) = field.name.as_str().parse() + { + // tuple index + idx + } else { + // assume the value expression is passed directly + pos_to_value_index.len() + }; + + pos_to_value_index.push(val_idx); + } + + true } else { - // assume that any further exprs with a differing context are value args - value_args.push(e); + // assume that any expr with a differing span is a value + value_args.push(expr); + + false } - // don't walk anything not from the macro expansion (e.a. inputs) - false }) - .visit_expr(expr); - Some(FormatArgsExpn { - format_string_span: format_string_span?, - format_string_parts, + .visit_expr(args); + + Self { value_args, - formatters, - specs, + pos_to_value_index, + format_string_span, + } + } +} + +/// The positions of a format argument's value, precision and width +/// +/// A position is an index into the second argument of `Arguments::new_v1[_formatted]` +#[derive(Debug, Default, Copy, Clone)] +struct ParamPosition { + /// The position stored in `rt::v1::Argument::position`. + value: usize, + /// The position stored in `rt::v1::FormatSpec::width` if it is a `Count::Param`. + width: Option, + /// The position stored in `rt::v1::FormatSpec::precision` if it is a `Count::Param`. + precision: Option, +} + +/// Parses the `fmt` arg of `Arguments::new_v1_formatted(pieces, args, fmt, _)` +fn parse_rt_fmt<'tcx>(fmt_arg: &'tcx Expr<'tcx>) -> Option + 'tcx> { + fn parse_count(expr: &Expr<'_>) -> Option { + // ::core::fmt::rt::v1::Count::Param(1usize), + if let ExprKind::Call(ctor, [val]) = expr.kind + && let ExprKind::Path(QPath::Resolved(_, path)) = ctor.kind + && path.segments.last()?.ident.name == sym::Param + && let ExprKind::Lit(lit) = &val.kind + && let LitKind::Int(pos, _) = lit.node + { + Some(pos as usize) + } else { + None + } + } + + if let ExprKind::AddrOf(.., array) = fmt_arg.kind + && let ExprKind::Array(specs) = array.kind + { + Some(specs.iter().map(|spec| { + let mut position = ParamPosition::default(); + + // ::core::fmt::rt::v1::Argument { + // position: 0usize, + // format: ::core::fmt::rt::v1::FormatSpec { + // .. + // precision: ::core::fmt::rt::v1::Count::Implied, + // width: ::core::fmt::rt::v1::Count::Implied, + // }, + // } + + // TODO: this can be made much nicer next sync with `Visitor::visit_expr_field` + if let ExprKind::Struct(_, fields, _) = spec.kind { + for field in fields { + match (field.ident.name, &field.expr.kind) { + (sym::position, ExprKind::Lit(lit)) => { + if let LitKind::Int(pos, _) = lit.node { + position.value = pos as usize; + } + }, + (sym::format, &ExprKind::Struct(_, spec_fields, _)) => { + for spec_field in spec_fields { + match spec_field.ident.name { + sym::precision => { + position.precision = parse_count(spec_field.expr); + }, + sym::width => { + position.width = parse_count(spec_field.expr); + }, + _ => {}, + } + } + }, + _ => {}, + } + } + } + + position + })) + } else { + None + } +} + +/// `Span::from_inner`, but for `rustc_parse_format`'s `InnerSpan` +fn span_from_inner(base: SpanData, inner: rpf::InnerSpan) -> Span { + Span::new( + base.lo + BytePos::from_usize(inner.start), + base.lo + BytePos::from_usize(inner.end), + base.ctxt, + base.parent, + ) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FormatParamKind { + /// An implicit parameter , such as `{}` or `{:?}`. + Implicit, + /// A parameter with an explicit number, or an asterisk precision. e.g. `{1}`, `{0:?}`, + /// `{:.0$}` or `{:.*}`. + Numbered, + /// A named parameter with a named `value_arg`, such as the `x` in `format!("{x}", x = 1)`. + Named(Symbol), + /// An implicit named paramter, such as the `y` in `format!("{y}")`. + NamedInline(Symbol), +} + +/// A `FormatParam` is any place in a `FormatArgument` that refers to a supplied value, e.g. +/// +/// ``` +/// let precision = 2; +/// format!("{:.precision$}", 0.1234); +/// ``` +/// +/// has two `FormatParam`s, a [`FormatParamKind::Implicit`] `.kind` with a `.value` of `0.1234` +/// and a [`FormatParamKind::NamedInline("precision")`] `.kind` with a `.value` of `2` +#[derive(Debug, Copy, Clone)] +pub struct FormatParam<'tcx> { + /// The expression this parameter refers to. + pub value: &'tcx Expr<'tcx>, + /// How this paramter refers to its `value`. + pub kind: FormatParamKind, + /// Span of the parameter, may be zero width. Includes the whitespace of implicit parameters. + /// + /// ```text + /// format!("{}, { }, {0}, {name}", ...); + /// ^ ~~ ~ ~~~~ + /// ``` + pub span: Span, +} + +impl<'tcx> FormatParam<'tcx> { + fn new( + mut kind: FormatParamKind, + position: usize, + inner: rpf::InnerSpan, + values: &FormatArgsValues<'tcx>, + ) -> Option { + let value_index = *values.pos_to_value_index.get(position)?; + let value = *values.value_args.get(value_index)?; + let span = span_from_inner(values.format_string_span, inner); + + // if a param is declared inline, e.g. `format!("{x}")`, the generated expr's span points + // into the format string + if let FormatParamKind::Named(name) = kind && values.format_string_span.contains(value.span.data()) { + kind = FormatParamKind::NamedInline(name); + } + + Some(Self { value, kind, span }) + } +} + +/// Used by [width](https://doc.rust-lang.org/std/fmt/#width) and +/// [precision](https://doc.rust-lang.org/std/fmt/#precision) specifiers. +#[derive(Debug, Copy, Clone)] +pub enum Count<'tcx> { + /// Specified with a literal number, stores the value. + Is(usize, Span), + /// Specified using `$` and `*` syntaxes. The `*` format is still considered to be + /// `FormatParamKind::Numbered`. + Param(FormatParam<'tcx>), + /// Not specified. + Implied, +} + +impl<'tcx> Count<'tcx> { + fn new( + count: rpf::Count<'_>, + position: Option, + inner: Option, + values: &FormatArgsValues<'tcx>, + ) -> Option { + Some(match count { + rpf::Count::CountIs(val) => Self::Is(val, span_from_inner(values.format_string_span, inner?)), + rpf::Count::CountIsName(name, span) => Self::Param(FormatParam::new( + FormatParamKind::Named(Symbol::intern(name)), + position?, + span, + values, + )?), + rpf::Count::CountIsParam(_) => { + Self::Param(FormatParam::new(FormatParamKind::Numbered, position?, inner?, values)?) + }, + rpf::Count::CountImplied => Self::Implied, + }) + } + + pub fn is_implied(self) -> bool { + matches!(self, Count::Implied) + } + + pub fn param(self) -> Option> { + match self { + Count::Param(param) => Some(param), + _ => None, + } + } +} + +/// Specification for the formatting of an argument in the format string. See +/// for the precise meanings. +#[derive(Debug)] +pub struct FormatSpec<'tcx> { + /// Optionally specified character to fill alignment with. + pub fill: Option, + /// Optionally specified alignment. + pub align: Alignment, + /// Packed version of various flags provided, see [`rustc_parse_format::Flag`]. + pub flags: u32, + /// Represents either the maximum width or the integer precision. + pub precision: Count<'tcx>, + /// The minimum width, will be padded according to `width`/`align` + pub width: Count<'tcx>, + /// The formatting trait used by the argument, e.g. `sym::Display` for `{}`, `sym::Debug` for + /// `{:?}`. + pub r#trait: Symbol, + pub trait_span: Option, +} + +impl<'tcx> FormatSpec<'tcx> { + fn new(spec: rpf::FormatSpec<'_>, positions: ParamPosition, values: &FormatArgsValues<'tcx>) -> Option { + Some(Self { + fill: spec.fill, + align: spec.align, + flags: spec.flags, + precision: Count::new(spec.precision, positions.precision, spec.precision_span, values)?, + width: Count::new(spec.width, positions.width, spec.width_span, values)?, + r#trait: match spec.ty { + "" => sym::Display, + "?" => sym::Debug, + "o" => sym!(Octal), + "x" => sym!(LowerHex), + "X" => sym!(UpperHex), + "p" => sym::Pointer, + "b" => sym!(Binary), + "e" => sym!(LowerExp), + "E" => sym!(UpperExp), + _ => return None, + }, + trait_span: spec + .ty_span + .map(|span| span_from_inner(values.format_string_span, span)), }) } - /// Finds a nested call to `format_args!` within a `format!`-like macro call + /// Returns true if this format spec would change the contents of a string when formatted + pub fn has_string_formatting(&self) -> bool { + self.r#trait != sym::Display || !self.width.is_implied() || !self.precision.is_implied() + } +} + +/// A format argument, such as `{}`, `{foo:?}`. +#[derive(Debug)] +pub struct FormatArg<'tcx> { + /// The parameter the argument refers to. + pub param: FormatParam<'tcx>, + /// How to format `param`. + pub format: FormatSpec<'tcx>, + /// span of the whole argument, `{..}`. + pub span: Span, +} + +/// A parsed `format_args!` expansion. +#[derive(Debug)] +pub struct FormatArgsExpn<'tcx> { + /// The format string literal. + pub format_string: FormatString, + // The format arguments, such as `{:?}`. + pub args: Vec>, + /// Has an added newline due to `println!()`/`writeln!()`/etc. The last format string part will + /// include this added newline. + pub newline: bool, + /// Values passed after the format string and implicit captures. `[1, z + 2, x]` for + /// `format!("{x} {} {y}", 1, z + 2)`. + value_args: Vec<&'tcx Expr<'tcx>>, +} + +impl<'tcx> FormatArgsExpn<'tcx> { + pub fn parse(cx: &LateContext<'_>, expr: &'tcx Expr<'tcx>) -> Option { + let macro_name = macro_backtrace(expr.span) + .map(|macro_call| cx.tcx.item_name(macro_call.def_id)) + .find(|&name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))?; + let newline = macro_name == sym::format_args_nl; + + // ::core::fmt::Arguments::new_v1(pieces, args) + // ::core::fmt::Arguments::new_v1_formatted(pieces, args, fmt, _unsafe_arg) + if let ExprKind::Call(callee, [pieces, args, rest @ ..]) = expr.kind + && let ExprKind::Path(QPath::TypeRelative(ty, seg)) = callee.kind + && is_path_diagnostic_item(cx, ty, sym::Arguments) + && matches!(seg.ident.as_str(), "new_v1" | "new_v1_formatted") + { + let format_string = FormatString::new(cx, pieces)?; + + let mut parser = rpf::Parser::new( + &format_string.unescaped, + format_string.style, + Some(format_string.snippet.clone()), + // `format_string.unescaped` does not contain the appended newline + false, + rpf::ParseMode::Format, + ); + + let parsed_args = parser + .by_ref() + .filter_map(|piece| match piece { + rpf::Piece::NextArgument(a) => Some(a), + rpf::Piece::String(_) => None, + }) + .collect_vec(); + if !parser.errors.is_empty() { + return None; + } + + let positions = if let Some(fmt_arg) = rest.first() { + // If the argument contains format specs, `new_v1_formatted(_, _, fmt, _)`, parse + // them. + + Either::Left(parse_rt_fmt(fmt_arg)?) + } else { + // If no format specs are given, the positions are in the given order and there are + // no `precision`/`width`s to consider. + + Either::Right((0..).map(|n| ParamPosition { + value: n, + width: None, + precision: None, + })) + }; + + let values = FormatArgsValues::new(args, format_string.span.data()); + + let args = izip!(positions, parsed_args, parser.arg_places) + .map(|(position, parsed_arg, arg_span)| { + Some(FormatArg { + param: FormatParam::new( + match parsed_arg.position { + rpf::Position::ArgumentImplicitlyIs(_) => FormatParamKind::Implicit, + rpf::Position::ArgumentIs(_) => FormatParamKind::Numbered, + // NamedInline is handled by `FormatParam::new()` + rpf::Position::ArgumentNamed(name) => FormatParamKind::Named(Symbol::intern(name)), + }, + position.value, + parsed_arg.position_span, + &values, + )?, + format: FormatSpec::new(parsed_arg.format, position, &values)?, + span: span_from_inner(values.format_string_span, arg_span), + }) + }) + .collect::>>()?; + + Some(Self { + format_string, + args, + value_args: values.value_args, + newline, + }) + } else { + None + } + } + pub fn find_nested(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, expn_id: ExpnId) -> Option { let mut format_args = None; expr_visitor_no_bodies(|e| { @@ -466,88 +845,23 @@ impl<'tcx> FormatArgsExpn<'tcx> { format_args } - /// Returns a vector of `FormatArgsArg`. - pub fn args(&self) -> Option>> { - if self.specs.is_empty() { - let args = std::iter::zip(&self.value_args, &self.formatters) - .map(|(value, &(_, format_trait))| FormatArgsArg { - value, - format_trait, - spec: None, - }) - .collect(); - return Some(args); - } - self.specs - .iter() - .map(|spec| { - if_chain! { - // struct `core::fmt::rt::v1::Argument` - if let ExprKind::Struct(_, fields, _) = spec.kind; - if let Some(position_field) = fields.iter().find(|f| f.ident.name == sym::position); - if let ExprKind::Lit(lit) = &position_field.expr.kind; - if let LitKind::Int(position, _) = lit.node; - if let Ok(i) = usize::try_from(position); - if let Some(&(j, format_trait)) = self.formatters.get(i); - then { - Some(FormatArgsArg { - value: self.value_args[j], - format_trait, - spec: Some(spec), - }) - } else { - None - } - } - }) - .collect() - } - /// Source callsite span of all inputs pub fn inputs_span(&self) -> Span { match *self.value_args { - [] => self.format_string_span, + [] => self.format_string.span, [.., last] => self - .format_string_span - .to(hygiene::walk_chain(last.span, self.format_string_span.ctxt())), + .format_string + .span + .to(hygiene::walk_chain(last.span, self.format_string.span.ctxt())), } } -} -/// Type representing a `FormatArgsExpn`'s format arguments -pub struct FormatArgsArg<'tcx> { - /// An element of `value_args` according to `position` - pub value: &'tcx Expr<'tcx>, - /// An element of `args` according to `position` - pub format_trait: Symbol, - /// An element of `specs` - pub spec: Option<&'tcx Expr<'tcx>>, -} - -impl<'tcx> FormatArgsArg<'tcx> { - /// Returns true if any formatting parameters are used that would have an effect on strings, - /// like `{:+2}` instead of just `{}`. - pub fn has_string_formatting(&self) -> bool { - self.spec.map_or(false, |spec| { - // `!` because these conditions check that `self` is unformatted. - !if_chain! { - // struct `core::fmt::rt::v1::Argument` - if let ExprKind::Struct(_, fields, _) = spec.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(_, subfields, _) = format_field.expr.kind; - if subfields.iter().all(|field| match field.ident.name { - sym::precision | sym::width => match field.expr.kind { - ExprKind::Path(QPath::Resolved(_, path)) => { - path.segments.last().unwrap().ident.name == sym::Implied - } - _ => false, - } - _ => true, - }); - then { true } else { false } - } - }) + /// Iterator of all format params, both values and those referenced by `width`/`precision`s. + pub fn params(&'tcx self) -> impl Iterator> { + self.args + .iter() + .flat_map(|arg| [Some(arg.param), arg.format.precision.param(), arg.format.width.param()]) + .flatten() } } diff --git a/clippy_utils/src/paths.rs b/clippy_utils/src/paths.rs index 8d697a301c44..199a3ab12ae0 100644 --- a/clippy_utils/src/paths.rs +++ b/clippy_utils/src/paths.rs @@ -71,7 +71,6 @@ pub const IPADDR_V6: [&str; 5] = ["std", "net", "ip", "IpAddr", "V6"]; pub const ITER_COUNT: [&str; 6] = ["core", "iter", "traits", "iterator", "Iterator", "count"]; pub const ITER_EMPTY: [&str; 5] = ["core", "iter", "sources", "empty", "Empty"]; pub const ITER_REPEAT: [&str; 5] = ["core", "iter", "sources", "repeat", "repeat"]; -#[expect(clippy::invalid_paths)] // internal lints do not know about all external crates pub const ITERTOOLS_NEXT_TUPLE: [&str; 3] = ["itertools", "Itertools", "next_tuple"]; #[cfg(feature = "internal")] pub const KW_MODULE: [&str; 3] = ["rustc_span", "symbol", "kw"]; diff --git a/tests/ui/format_args.fixed b/tests/ui/format_args.fixed index 69b5e1c722e0..4322891db762 100644 --- a/tests/ui/format_args.fixed +++ b/tests/ui/format_args.fixed @@ -1,8 +1,6 @@ // run-rustfix -#![allow(unreachable_code)] -#![allow(unused_macros)] -#![allow(unused_variables)] +#![allow(unused)] #![allow(clippy::assertions_on_constants)] #![allow(clippy::eq_op)] #![allow(clippy::print_literal)] @@ -115,3 +113,12 @@ fn main() { // https://github.com/rust-lang/rust-clippy/issues/7903 println!("{foo}{foo:?}", foo = "foo".to_string()); } + +fn issue8643(vendor_id: usize, product_id: usize, name: &str) { + println!( + "{:<9} {:<10} {}", + format!("0x{:x}", vendor_id), + format!("0x{:x}", product_id), + name + ); +} diff --git a/tests/ui/format_args.rs b/tests/ui/format_args.rs index 3a434c5bf002..61ad04612cdc 100644 --- a/tests/ui/format_args.rs +++ b/tests/ui/format_args.rs @@ -1,8 +1,6 @@ // run-rustfix -#![allow(unreachable_code)] -#![allow(unused_macros)] -#![allow(unused_variables)] +#![allow(unused)] #![allow(clippy::assertions_on_constants)] #![allow(clippy::eq_op)] #![allow(clippy::print_literal)] @@ -115,3 +113,12 @@ fn main() { // https://github.com/rust-lang/rust-clippy/issues/7903 println!("{foo}{foo:?}", foo = "foo".to_string()); } + +fn issue8643(vendor_id: usize, product_id: usize, name: &str) { + println!( + "{:<9} {:<10} {}", + format!("0x{:x}", vendor_id), + format!("0x{:x}", product_id), + name + ); +} diff --git a/tests/ui/format_args.stderr b/tests/ui/format_args.stderr index c0cbca507958..0aca1c1a0dfb 100644 --- a/tests/ui/format_args.stderr +++ b/tests/ui/format_args.stderr @@ -1,5 +1,5 @@ error: `to_string` applied to a type that implements `Display` in `format!` args - --> $DIR/format_args.rs:76:72 + --> $DIR/format_args.rs:74:72 | LL | let _ = format!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this @@ -7,121 +7,121 @@ LL | let _ = format!("error: something failed at {}", Location::caller().to_ = note: `-D clippy::to-string-in-format-args` implied by `-D warnings` error: `to_string` applied to a type that implements `Display` in `write!` args - --> $DIR/format_args.rs:80:27 + --> $DIR/format_args.rs:78:27 | LL | Location::caller().to_string() | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `writeln!` args - --> $DIR/format_args.rs:85:27 + --> $DIR/format_args.rs:83:27 | LL | Location::caller().to_string() | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `print!` args - --> $DIR/format_args.rs:87:63 + --> $DIR/format_args.rs:85:63 | LL | print!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:88:65 + --> $DIR/format_args.rs:86:65 | LL | println!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `eprint!` args - --> $DIR/format_args.rs:89:64 + --> $DIR/format_args.rs:87:64 | LL | eprint!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `eprintln!` args - --> $DIR/format_args.rs:90:66 + --> $DIR/format_args.rs:88:66 | LL | eprintln!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `format_args!` args - --> $DIR/format_args.rs:91:77 + --> $DIR/format_args.rs:89:77 | LL | let _ = format_args!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `assert!` args - --> $DIR/format_args.rs:92:70 + --> $DIR/format_args.rs:90:70 | LL | assert!(true, "error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `assert_eq!` args - --> $DIR/format_args.rs:93:73 + --> $DIR/format_args.rs:91:73 | LL | assert_eq!(0, 0, "error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `assert_ne!` args - --> $DIR/format_args.rs:94:73 + --> $DIR/format_args.rs:92:73 | LL | assert_ne!(0, 0, "error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `panic!` args - --> $DIR/format_args.rs:95:63 + --> $DIR/format_args.rs:93:63 | LL | panic!("error: something failed at {}", Location::caller().to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:96:20 + --> $DIR/format_args.rs:94:20 | LL | println!("{}", X(1).to_string()); | ^^^^^^^^^^^^^^^^ help: use this: `*X(1)` error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:97:20 + --> $DIR/format_args.rs:95:20 | LL | println!("{}", Y(&X(1)).to_string()); | ^^^^^^^^^^^^^^^^^^^^ help: use this: `***Y(&X(1))` error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:98:24 + --> $DIR/format_args.rs:96:24 | LL | println!("{}", Z(1).to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:99:20 + --> $DIR/format_args.rs:97:20 | LL | println!("{}", x.to_string()); | ^^^^^^^^^^^^^ help: use this: `**x` error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:100:20 + --> $DIR/format_args.rs:98:20 | LL | println!("{}", x_ref.to_string()); | ^^^^^^^^^^^^^^^^^ help: use this: `***x_ref` error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:102:39 + --> $DIR/format_args.rs:100:39 | LL | println!("{foo}{bar}", foo = "foo".to_string(), bar = "bar"); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:103:52 + --> $DIR/format_args.rs:101:52 | LL | println!("{foo}{bar}", foo = "foo", bar = "bar".to_string()); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:104:39 + --> $DIR/format_args.rs:102:39 | LL | println!("{foo}{bar}", bar = "bar".to_string(), foo = "foo"); | ^^^^^^^^^^^^ help: remove this error: `to_string` applied to a type that implements `Display` in `println!` args - --> $DIR/format_args.rs:105:52 + --> $DIR/format_args.rs:103:52 | LL | println!("{foo}{bar}", bar = "bar", foo = "foo".to_string()); | ^^^^^^^^^^^^ help: remove this