blob: 58e66c9f9b951f829c985270ab8dd1f3e0f76451 [file] [log] [blame]
use clippy_utils::macros::AST_FORMAT_ARGS;
use clippy_utils::source::snippet_opt;
use itertools::Itertools;
use rustc_ast::{Crate, Expr, ExprKind, FormatArgs};
use rustc_data_structures::fx::FxHashMap;
use rustc_lexer::{tokenize, TokenKind};
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::{hygiene, Span};
use std::iter::once;
use std::mem;
use std::rc::Rc;
/// Collects [`rustc_ast::FormatArgs`] so that future late passes can call
/// [`clippy_utils::macros::find_format_args`]
#[derive(Default)]
pub struct FormatArgsCollector {
format_args: FxHashMap<Span, Rc<FormatArgs>>,
}
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), Rc::new((**args).clone()));
}
}
fn check_crate_post(&mut self, _: &EarlyContext<'_>, _: &Crate) {
AST_FORMAT_ARGS.with(|ast_format_args| {
let result = ast_format_args.set(mem::take(&mut self.format_args));
debug_assert!(result.is_ok());
});
}
}
/// 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")`
// ^^ ^^ ^^^^^^
let between_spans = once(args.span)
.chain(argument_span)
.tuple_windows()
.map(|(start, end)| start.between(end));
for between_span in between_spans {
let mut seen_comma = false;
let Some(snippet) = snippet_opt(cx, between_span) else {
return true;
};
for token in tokenize(&snippet) {
match token.kind {
TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace => {},
TokenKind::Comma if !seen_comma => seen_comma = true,
// named arguments, `start_val, name = end_val`
// ^^^^^^^^^ between_span
TokenKind::Ident | TokenKind::Eq if seen_comma => {},
// An unexpected token usually indicates that we crossed a macro boundary
//
// `println!(some_proc_macro!("input {}"), a)`
// ^^^ between_span
// `println!("{}", val!(x))`
// ^^^^^^^ between_span
_ => return true,
}
}
if !seen_comma {
return true;
}
}
false
}