109 lines
3.5 KiB
Rust
109 lines
3.5 KiB
Rust
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
|
|
}
|