| use clippy_utils::macros::FormatArgsStorage; | |
| use clippy_utils::source::SpanRangeExt; | |
| use itertools::Itertools; | |
| use rustc_ast::{Crate, Expr, ExprKind, FormatArgs}; | |
| use rustc_data_structures::fx::FxHashMap; | |
| use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize}; | |
| use rustc_lint::{EarlyContext, EarlyLintPass}; | |
| use rustc_session::impl_lint_pass; | |
| use rustc_span::{Span, hygiene}; | |
| use std::iter::once; | |
| use std::mem; | |
| /// Populates [`FormatArgsStorage`] with AST [`FormatArgs`] nodes | |
| pub struct FormatArgsCollector { | |
| format_args: FxHashMap<Span, FormatArgs>, | |
| storage: FormatArgsStorage, | |
| } | |
| impl FormatArgsCollector { | |
| pub fn new(storage: FormatArgsStorage) -> Self { | |
| Self { | |
| format_args: FxHashMap::default(), | |
| storage, | |
| } | |
| } | |
| } | |
| impl_lint_pass!(FormatArgsCollector => []); | |
| impl EarlyLintPass for FormatArgsCollector { | |
| fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { | |
| if let ExprKind::FormatArgs(args) = &expr.kind { | |
| if has_span_from_proc_macro(cx, args) { | |
| return; | |
| } | |
| self.format_args.insert(expr.span.with_parent(None), (**args).clone()); | |
| } | |
| } | |
| fn check_crate_post(&mut self, _: &EarlyContext<'_>, _: &Crate) { | |
| self.storage.set(mem::take(&mut self.format_args)); | |
| } | |
| } | |
| /// Detects if the format string or an argument has its span set by a proc macro to something inside | |
| /// a macro callsite, e.g. | |
| /// | |
| /// ```ignore | |
| /// println!(some_proc_macro!("input {}"), a); | |
| /// ``` | |
| /// | |
| /// Where `some_proc_macro` expands to | |
| /// | |
| /// ```ignore | |
| /// println!("output {}", a); | |
| /// ``` | |
| /// | |
| /// But with the span of `"output {}"` set to the macro input | |
| /// | |
| /// ```ignore | |
| /// println!(some_proc_macro!("input {}"), a); | |
| /// // ^^^^^^^^^^ | |
| /// ``` | |
| fn has_span_from_proc_macro(cx: &EarlyContext<'_>, args: &FormatArgs) -> bool { | |
| let ctxt = args.span.ctxt(); | |
| // `format!("{} {} {c}", "one", "two", c = "three")` | |
| // ^^^^^ ^^^^^ ^^^^^^^ | |
| let argument_span = args | |
| .arguments | |
| .explicit_args() | |
| .iter() | |
| .map(|argument| hygiene::walk_chain(argument.expr.span, ctxt)); | |
| // `format!("{} {} {c}", "one", "two", c = "three")` | |
| // ^^ ^^ ^^^^^^ | |
| !once(args.span) | |
| .chain(argument_span) | |
| .tuple_windows() | |
| .map(|(start, end)| start.between(end)) | |
| .all(|sp| { | |
| sp.check_source_text(cx, |src| { | |
| // text should be either `, name` or `, name =` | |
| let mut iter = tokenize(src, FrontmatterAllowed::No).filter(|t| { | |
| !matches!( | |
| t.kind, | |
| TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace | |
| ) | |
| }); | |
| iter.next().is_some_and(|t| matches!(t.kind, TokenKind::Comma)) | |
| && iter.all(|t| matches!(t.kind, TokenKind::Ident | TokenKind::Eq)) | |
| }) | |
| }) | |
| } |