Skip to content

Commit 717577f

Browse files
committed
Auto merge of #10275 - Alexendoo:format-args-ast, r=flip1995
Migrate `write.rs` to `rustc_ast::FormatArgs` changelog: none Part 1 of #10233 The additions to `clippy_utils` are the main novelty of this PR, there's no removals yet since other parts still rely on `FormatArgsExpn` The changes to `write.rs` itself are relatively straightforward this time around, as there's no lints in it that rely on type checking format params r? `@flip1995`
2 parents 9035958 + d1407e5 commit 717577f

File tree

5 files changed

+185
-63
lines changed

5 files changed

+185
-63
lines changed

clippy_lints/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
533533
.collect(),
534534
))
535535
});
536+
store.register_early_pass(|| Box::new(utils::format_args_collector::FormatArgsCollector));
536537
store.register_late_pass(|_| Box::new(utils::dump_hir::DumpHir));
537538
store.register_late_pass(|_| Box::new(utils::author::Author));
538539
let await_holding_invalid_types = conf.await_holding_invalid_types.clone();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use clippy_utils::macros::collect_ast_format_args;
2+
use rustc_ast::{Expr, ExprKind};
3+
use rustc_lint::{EarlyContext, EarlyLintPass};
4+
use rustc_session::{declare_lint_pass, declare_tool_lint};
5+
6+
declare_clippy_lint! {
7+
/// ### What it does
8+
/// Collects [`rustc_ast::FormatArgs`] so that future late passes can call
9+
/// [`clippy_utils::macros::find_format_args`]
10+
pub FORMAT_ARGS_COLLECTOR,
11+
internal_warn,
12+
"collects `format_args` AST nodes for use in later lints"
13+
}
14+
15+
declare_lint_pass!(FormatArgsCollector => [FORMAT_ARGS_COLLECTOR]);
16+
17+
impl EarlyLintPass for FormatArgsCollector {
18+
fn check_expr(&mut self, _: &EarlyContext<'_>, expr: &Expr) {
19+
if let ExprKind::FormatArgs(args) = &expr.kind {
20+
collect_ast_format_args(expr.span, args);
21+
}
22+
}
23+
}

clippy_lints/src/utils/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod author;
22
pub mod conf;
33
pub mod dump_hir;
4+
pub mod format_args_collector;
45
#[cfg(feature = "internal")]
56
pub mod internal_lints;

clippy_lints/src/write.rs

+85-63
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2-
use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn, MacroCall};
2+
use clippy_utils::macros::{find_format_args, format_arg_removal_span, root_macro_call_first_node, MacroCall};
33
use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
44
use clippy_utils::{is_in_cfg_test, is_in_test_function};
5-
use rustc_ast::LitKind;
5+
use rustc_ast::token::LitKind;
6+
use rustc_ast::{FormatArgPosition, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder, FormatTrait};
67
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
8+
use rustc_hir::{Expr, Impl, Item, ItemKind};
89
use rustc_lint::{LateContext, LateLintPass, LintContext};
910
use rustc_session::{declare_tool_lint, impl_lint_pass};
1011
use rustc_span::{sym, BytePos};
@@ -297,34 +298,40 @@ impl<'tcx> LateLintPass<'tcx> for Write {
297298
_ => return,
298299
}
299300

300-
let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn) else { return };
301-
302-
// ignore `writeln!(w)` and `write!(v, some_macro!())`
303-
if format_args.format_string.span.from_expansion() {
304-
return;
305-
}
301+
find_format_args(cx, expr, macro_call.expn, |format_args| {
302+
// ignore `writeln!(w)` and `write!(v, some_macro!())`
303+
if format_args.span.from_expansion() {
304+
return;
305+
}
306306

307-
match diag_name {
308-
sym::print_macro | sym::eprint_macro | sym::write_macro => {
309-
check_newline(cx, &format_args, &macro_call, name);
310-
},
311-
sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
312-
check_empty_string(cx, &format_args, &macro_call, name);
313-
},
314-
_ => {},
315-
}
307+
match diag_name {
308+
sym::print_macro | sym::eprint_macro | sym::write_macro => {
309+
check_newline(cx, format_args, &macro_call, name);
310+
},
311+
sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
312+
check_empty_string(cx, format_args, &macro_call, name);
313+
},
314+
_ => {},
315+
}
316316

317-
check_literal(cx, &format_args, name);
317+
check_literal(cx, format_args, name);
318318

319-
if !self.in_debug_impl {
320-
for arg in &format_args.args {
321-
if arg.format.r#trait == sym::Debug {
322-
span_lint(cx, USE_DEBUG, arg.span, "use of `Debug`-based formatting");
319+
if !self.in_debug_impl {
320+
for piece in &format_args.template {
321+
if let &FormatArgsPiece::Placeholder(FormatPlaceholder {
322+
span: Some(span),
323+
format_trait: FormatTrait::Debug,
324+
..
325+
}) = piece
326+
{
327+
span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting");
328+
}
323329
}
324330
}
325-
}
331+
});
326332
}
327333
}
334+
328335
fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
329336
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind
330337
&& let Some(trait_id) = trait_ref.trait_def_id()
@@ -335,27 +342,28 @@ fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
335342
}
336343
}
337344

338-
fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
339-
let format_string_parts = &format_args.format_string.parts;
340-
let mut format_string_span = format_args.format_string.span;
341-
342-
let Some(last) = format_string_parts.last() else { return };
345+
fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
346+
let Some(FormatArgsPiece::Literal(last)) = format_args.template.last() else { return };
343347

344348
let count_vertical_whitespace = || {
345-
format_string_parts
349+
format_args
350+
.template
346351
.iter()
347-
.flat_map(|part| part.as_str().chars())
352+
.filter_map(|piece| match piece {
353+
FormatArgsPiece::Literal(literal) => Some(literal),
354+
FormatArgsPiece::Placeholder(_) => None,
355+
})
356+
.flat_map(|literal| literal.as_str().chars())
348357
.filter(|ch| matches!(ch, '\r' | '\n'))
349358
.count()
350359
};
351360

352361
if last.as_str().ends_with('\n')
353362
// ignore format strings with other internal vertical whitespace
354363
&& count_vertical_whitespace() == 1
355-
356-
// ignore trailing arguments: `print!("Issue\n{}", 1265);`
357-
&& format_string_parts.len() > format_args.args.len()
358364
{
365+
let mut format_string_span = format_args.span;
366+
359367
let lint = if name == "write" {
360368
format_string_span = expand_past_previous_comma(cx, format_string_span);
361369

@@ -373,7 +381,7 @@ fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_c
373381
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
374382
let Some(format_snippet) = snippet_opt(cx, format_string_span) else { return };
375383

376-
if format_string_parts.len() == 1 && last.as_str() == "\n" {
384+
if format_args.template.len() == 1 && last.as_str() == "\n" {
377385
// print!("\n"), write!(f, "\n")
378386

379387
diag.multipart_suggestion(
@@ -398,11 +406,12 @@ fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_c
398406
}
399407
}
400408

401-
fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
402-
if let [part] = &format_args.format_string.parts[..]
403-
&& let mut span = format_args.format_string.span
404-
&& part.as_str() == "\n"
409+
fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
410+
if let [FormatArgsPiece::Literal(literal)] = &format_args.template[..]
411+
&& literal.as_str() == "\n"
405412
{
413+
let mut span = format_args.span;
414+
406415
let lint = if name == "writeln" {
407416
span = expand_past_previous_comma(cx, span);
408417

@@ -428,33 +437,43 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, ma
428437
}
429438
}
430439

431-
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &str) {
432-
let mut counts = HirIdMap::<usize>::default();
433-
for param in format_args.params() {
434-
*counts.entry(param.value.hir_id).or_default() += 1;
440+
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
441+
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);
442+
443+
let mut counts = vec![0u32; format_args.arguments.all_args().len()];
444+
for piece in &format_args.template {
445+
if let FormatArgsPiece::Placeholder(placeholder) = piece {
446+
counts[arg_index(&placeholder.argument)] += 1;
447+
}
435448
}
436449

437-
for arg in &format_args.args {
438-
let value = arg.param.value;
439-
440-
if counts[&value.hir_id] == 1
441-
&& arg.format.is_default()
442-
&& let ExprKind::Lit(lit) = &value.kind
443-
&& !value.span.from_expansion()
444-
&& let Some(value_string) = snippet_opt(cx, value.span)
445-
{
446-
let (replacement, replace_raw) = match lit.node {
447-
LitKind::Str(..) => extract_str_literal(&value_string),
448-
LitKind::Char(ch) => (
449-
match ch {
450-
'"' => "\\\"",
451-
'\'' => "'",
450+
for piece in &format_args.template {
451+
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
452+
argument,
453+
span: Some(placeholder_span),
454+
format_trait: FormatTrait::Display,
455+
format_options,
456+
}) = piece
457+
&& *format_options == FormatOptions::default()
458+
&& let index = arg_index(argument)
459+
&& counts[index] == 1
460+
&& let Some(arg) = format_args.arguments.by_index(index)
461+
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
462+
&& !arg.expr.span.from_expansion()
463+
&& let Some(value_string) = snippet_opt(cx, arg.expr.span)
464+
{
465+
let (replacement, replace_raw) = match lit.kind {
466+
LitKind::Str | LitKind::StrRaw(_) => extract_str_literal(&value_string),
467+
LitKind::Char => (
468+
match lit.symbol.as_str() {
469+
"\"" => "\\\"",
470+
"\\'" => "'",
452471
_ => &value_string[1..value_string.len() - 1],
453472
}
454473
.to_string(),
455474
false,
456475
),
457-
LitKind::Bool(b) => (b.to_string(), false),
476+
LitKind::Bool => (lit.symbol.to_string(), false),
458477
_ => continue,
459478
};
460479

@@ -464,7 +483,9 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &
464483
PRINT_LITERAL
465484
};
466485

467-
let format_string_is_raw = format_args.format_string.style.is_some();
486+
let Some(format_string_snippet) = snippet_opt(cx, format_args.span) else { continue };
487+
let format_string_is_raw = format_string_snippet.starts_with('r');
488+
468489
let replacement = match (format_string_is_raw, replace_raw) {
469490
(false, false) => Some(replacement),
470491
(false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")),
@@ -485,23 +506,24 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &
485506
span_lint_and_then(
486507
cx,
487508
lint,
488-
value.span,
509+
arg.expr.span,
489510
"literal with an empty format string",
490511
|diag| {
491512
if let Some(replacement) = replacement
492513
// `format!("{}", "a")`, `format!("{named}", named = "b")
493514
// ~~~~~ ~~~~~~~~~~~~~
494-
&& let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
515+
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
495516
{
496517
let replacement = replacement.replace('{', "{{").replace('}', "}}");
497518
diag.multipart_suggestion(
498519
"try this",
499-
vec![(arg.span, replacement), (value_span, String::new())],
520+
vec![(*placeholder_span, replacement), (removal_span, String::new())],
500521
Applicability::MachineApplicable,
501522
);
502523
}
503524
},
504525
);
526+
505527
}
506528
}
507529
}

clippy_utils/src/macros.rs

+75
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use crate::visitors::{for_each_expr, Descend};
66
use arrayvec::ArrayVec;
77
use itertools::{izip, Either, Itertools};
88
use rustc_ast::ast::LitKind;
9+
use rustc_ast::FormatArgs;
10+
use rustc_data_structures::fx::FxHashMap;
911
use rustc_hir::intravisit::{walk_expr, Visitor};
1012
use rustc_hir::{self as hir, Expr, ExprField, ExprKind, HirId, LangItem, Node, QPath, TyKind};
1113
use rustc_lexer::unescape::unescape_literal;
@@ -15,8 +17,10 @@ use rustc_parse_format::{self as rpf, Alignment};
1517
use rustc_span::def_id::DefId;
1618
use rustc_span::hygiene::{self, MacroKind, SyntaxContext};
1719
use rustc_span::{sym, BytePos, ExpnData, ExpnId, ExpnKind, Pos, Span, SpanData, Symbol};
20+
use std::cell::RefCell;
1821
use std::iter::{once, zip};
1922
use std::ops::ControlFlow;
23+
use std::sync::atomic::{AtomicBool, Ordering};
2024

2125
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[
2226
sym::assert_eq_macro,
@@ -339,6 +343,77 @@ fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) ->
339343
}
340344
}
341345

346+
thread_local! {
347+
/// We preserve the [`FormatArgs`] structs from the early pass for use in the late pass to be
348+
/// able to access the many features of a [`LateContext`].
349+
///
350+
/// A thread local is used because [`FormatArgs`] is `!Send` and `!Sync`, we are making an
351+
/// assumption that the early pass the populates the map and the later late passes will all be
352+
/// running on the same thread.
353+
static AST_FORMAT_ARGS: RefCell<FxHashMap<Span, FormatArgs>> = {
354+
static CALLED: AtomicBool = AtomicBool::new(false);
355+
debug_assert!(
356+
!CALLED.swap(true, Ordering::SeqCst),
357+
"incorrect assumption: `AST_FORMAT_ARGS` should only be accessed by a single thread",
358+
);
359+
360+
RefCell::default()
361+
};
362+
}
363+
364+
/// Record [`rustc_ast::FormatArgs`] for use in late lint passes, this should only be called by
365+
/// `FormatArgsCollector`
366+
pub fn collect_ast_format_args(span: Span, format_args: &FormatArgs) {
367+
AST_FORMAT_ARGS.with(|ast_format_args| {
368+
ast_format_args.borrow_mut().insert(span, format_args.clone());
369+
});
370+
}
371+
372+
/// Calls `callback` with an AST [`FormatArgs`] node if one is found
373+
pub fn find_format_args(cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId, callback: impl FnOnce(&FormatArgs)) {
374+
let format_args_expr = for_each_expr(start, |expr| {
375+
let ctxt = expr.span.ctxt();
376+
if ctxt == start.span.ctxt() {
377+
ControlFlow::Continue(Descend::Yes)
378+
} else if ctxt.outer_expn().is_descendant_of(expn_id)
379+
&& macro_backtrace(expr.span)
380+
.map(|macro_call| cx.tcx.item_name(macro_call.def_id))
381+
.any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
382+
{
383+
ControlFlow::Break(expr)
384+
} else {
385+
ControlFlow::Continue(Descend::No)
386+
}
387+
});
388+
389+
if let Some(format_args_expr) = format_args_expr {
390+
AST_FORMAT_ARGS.with(|ast_format_args| {
391+
ast_format_args.borrow().get(&format_args_expr.span).map(callback);
392+
});
393+
}
394+
}
395+
396+
/// Returns the [`Span`] of the value at `index` extended to the previous comma, e.g. for the value
397+
/// `10`
398+
///
399+
/// ```ignore
400+
/// format("{}.{}", 10, 11)
401+
/// // ^^^^
402+
/// ```
403+
pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
404+
let ctxt = format_args.span.ctxt();
405+
406+
let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
407+
408+
let prev = if index == 0 {
409+
format_args.span
410+
} else {
411+
hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
412+
};
413+
414+
Some(current.with_lo(prev.hi()))
415+
}
416+
342417
/// The format string doesn't exist in the HIR, so we reassemble it from source code
343418
#[derive(Debug)]
344419
pub struct FormatString {

0 commit comments

Comments
 (0)