Skip to content

Commit 3686b2b

Browse files
committed
Migrate write.rs to rustc_ast::FormatArgs
1 parent 75d7680 commit 3686b2b

File tree

3 files changed

+173
-65
lines changed

3 files changed

+173
-65
lines changed

clippy_lints/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
862862
let allow_dbg_in_tests = conf.allow_dbg_in_tests;
863863
store.register_late_pass(move |_| Box::new(dbg_macro::DbgMacro::new(allow_dbg_in_tests)));
864864
let allow_print_in_tests = conf.allow_print_in_tests;
865+
store.register_early_pass(move || Box::new(write::Write::new(allow_print_in_tests)));
865866
store.register_late_pass(move |_| Box::new(write::Write::new(allow_print_in_tests)));
866867
let cargo_ignore_publish = conf.cargo_ignore_publish;
867868
store.register_late_pass(move |_| {

clippy_lints/src/write.rs

+94-64
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
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::{
3+
find_format_args, format_arg_removal_span, populate_ast_format_args, root_macro_call_first_node, MacroCall,
4+
};
35
use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
46
use clippy_utils::{is_in_cfg_test, is_in_test_function};
5-
use rustc_ast::LitKind;
7+
use rustc_ast::token::LitKind;
8+
use rustc_ast::{FormatArgPosition, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder, FormatTrait};
69
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
8-
use rustc_lint::{LateContext, LateLintPass, LintContext};
10+
use rustc_hir::{Expr, Impl, Item, ItemKind};
11+
use rustc_lint::{EarlyLintPass, LateContext, LateLintPass, LintContext};
912
use rustc_session::{declare_tool_lint, impl_lint_pass};
1013
use rustc_span::{sym, BytePos};
1114

@@ -257,6 +260,12 @@ impl_lint_pass!(Write => [
257260
WRITE_LITERAL,
258261
]);
259262

263+
impl EarlyLintPass for Write {
264+
fn check_expr(&mut self, _: &rustc_lint::EarlyContext<'_>, expr: &rustc_ast::Expr) {
265+
populate_ast_format_args(expr);
266+
}
267+
}
268+
260269
impl<'tcx> LateLintPass<'tcx> for Write {
261270
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
262271
if is_debug_impl(cx, item) {
@@ -297,34 +306,40 @@ impl<'tcx> LateLintPass<'tcx> for Write {
297306
_ => return,
298307
}
299308

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-
}
309+
find_format_args(cx, expr, macro_call.expn, |format_args| {
310+
// ignore `writeln!(w)` and `write!(v, some_macro!())`
311+
if format_args.span.from_expansion() {
312+
return;
313+
}
306314

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-
}
315+
match diag_name {
316+
sym::print_macro | sym::eprint_macro | sym::write_macro => {
317+
check_newline(cx, format_args, &macro_call, name);
318+
},
319+
sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
320+
check_empty_string(cx, format_args, &macro_call, name);
321+
},
322+
_ => {},
323+
}
316324

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

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");
327+
if !self.in_debug_impl {
328+
for piece in &format_args.template {
329+
if let &FormatArgsPiece::Placeholder(FormatPlaceholder {
330+
span: Some(span),
331+
format_trait: FormatTrait::Debug,
332+
..
333+
}) = piece
334+
{
335+
span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting");
336+
}
323337
}
324338
}
325-
}
339+
});
326340
}
327341
}
342+
328343
fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
329344
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind
330345
&& let Some(trait_id) = trait_ref.trait_def_id()
@@ -335,27 +350,28 @@ fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
335350
}
336351
}
337352

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 };
353+
fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
354+
let Some(FormatArgsPiece::Literal(last)) = format_args.template.last() else { return };
343355

344356
let count_vertical_whitespace = || {
345-
format_string_parts
357+
format_args
358+
.template
346359
.iter()
347-
.flat_map(|part| part.as_str().chars())
360+
.filter_map(|piece| match piece {
361+
FormatArgsPiece::Literal(literal) => Some(literal),
362+
FormatArgsPiece::Placeholder(_) => None,
363+
})
364+
.flat_map(|literal| literal.as_str().chars())
348365
.filter(|ch| matches!(ch, '\r' | '\n'))
349366
.count()
350367
};
351368

352369
if last.as_str().ends_with('\n')
353370
// ignore format strings with other internal vertical whitespace
354371
&& count_vertical_whitespace() == 1
355-
356-
// ignore trailing arguments: `print!("Issue\n{}", 1265);`
357-
&& format_string_parts.len() > format_args.args.len()
358372
{
373+
let mut format_string_span = format_args.span;
374+
359375
let lint = if name == "write" {
360376
format_string_span = expand_past_previous_comma(cx, format_string_span);
361377

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

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

379395
diag.multipart_suggestion(
@@ -398,11 +414,12 @@ fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_c
398414
}
399415
}
400416

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"
417+
fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) {
418+
if let [FormatArgsPiece::Literal(literal)] = &format_args.template[..]
419+
&& literal.as_str() == "\n"
405420
{
421+
let mut span = format_args.span;
422+
406423
let lint = if name == "writeln" {
407424
span = expand_past_previous_comma(cx, span);
408425

@@ -428,33 +445,43 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, ma
428445
}
429446
}
430447

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;
448+
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
449+
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);
450+
451+
let mut counts = vec![0u32; format_args.arguments.all_args().len()];
452+
for piece in &format_args.template {
453+
if let FormatArgsPiece::Placeholder(placeholder) = piece {
454+
counts[arg_index(&placeholder.argument)] += 1;
455+
}
435456
}
436457

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-
'\'' => "'",
458+
for piece in &format_args.template {
459+
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
460+
argument,
461+
span: Some(placeholder_span),
462+
format_trait: FormatTrait::Display,
463+
format_options,
464+
}) = piece
465+
&& *format_options == FormatOptions::default()
466+
&& let index = arg_index(argument)
467+
&& counts[index] == 1
468+
&& let Some(arg) = format_args.arguments.by_index(index)
469+
&& let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind
470+
&& !arg.expr.span.from_expansion()
471+
&& let Some(value_string) = snippet_opt(cx, arg.expr.span)
472+
{
473+
let (replacement, replace_raw) = match lit.kind {
474+
LitKind::Str | LitKind::StrRaw(_) => extract_str_literal(&value_string),
475+
LitKind::Char => (
476+
match lit.symbol.as_str() {
477+
"\"" => "\\\"",
478+
"\\'" => "'",
452479
_ => &value_string[1..value_string.len() - 1],
453480
}
454481
.to_string(),
455482
false,
456483
),
457-
LitKind::Bool(b) => (b.to_string(), false),
484+
LitKind::Bool => (lit.symbol.to_string(), false),
458485
_ => continue,
459486
};
460487

@@ -464,7 +491,9 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &
464491
PRINT_LITERAL
465492
};
466493

467-
let format_string_is_raw = format_args.format_string.style.is_some();
494+
let Some(format_string_snippet) = snippet_opt(cx, format_args.span) else { continue };
495+
let format_string_is_raw = format_string_snippet.starts_with('r');
496+
468497
let replacement = match (format_string_is_raw, replace_raw) {
469498
(false, false) => Some(replacement),
470499
(false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")),
@@ -485,23 +514,24 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &
485514
span_lint_and_then(
486515
cx,
487516
lint,
488-
value.span,
517+
arg.expr.span,
489518
"literal with an empty format string",
490519
|diag| {
491520
if let Some(replacement) = replacement
492521
// `format!("{}", "a")`, `format!("{named}", named = "b")
493522
// ~~~~~ ~~~~~~~~~~~~~
494-
&& let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
523+
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
495524
{
496525
let replacement = replacement.replace('{', "{{").replace('}', "}}");
497526
diag.multipart_suggestion(
498527
"try this",
499-
vec![(arg.span, replacement), (value_span, String::new())],
528+
vec![(*placeholder_span, replacement), (removal_span, String::new())],
500529
Applicability::MachineApplicable,
501530
);
502531
}
503532
},
504533
);
534+
505535
}
506536
}
507537
}

clippy_utils/src/macros.rs

+78-1
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};
19-
use std::ops::ControlFlow;
22+
use std::ops::{ControlFlow, Deref};
23+
use std::sync::atomic::{AtomicBool, Ordering};
2024

2125
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[
2226
sym::assert_eq_macro,
@@ -339,6 +343,79 @@ 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 only needs to be called by
365+
/// one lint pass.
366+
pub fn populate_ast_format_args(expr: &rustc_ast::Expr) {
367+
if let rustc_ast::ExprKind::FormatArgs(args) = &expr.kind {
368+
AST_FORMAT_ARGS.with(|ast_format_args| {
369+
ast_format_args.borrow_mut().insert(expr.span, args.deref().clone());
370+
});
371+
}
372+
}
373+
374+
/// Calls `callback` with an AST [`FormatArgs`] node if one is found
375+
pub fn find_format_args(cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId, callback: impl FnOnce(&FormatArgs)) {
376+
let format_args_expr = for_each_expr(start, |expr| {
377+
let ctxt = expr.span.ctxt();
378+
if ctxt == start.span.ctxt() {
379+
ControlFlow::Continue(Descend::Yes)
380+
} else if ctxt.outer_expn().is_descendant_of(expn_id)
381+
&& macro_backtrace(expr.span)
382+
.map(|macro_call| cx.tcx.item_name(macro_call.def_id))
383+
.any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
384+
{
385+
ControlFlow::Break(expr)
386+
} else {
387+
ControlFlow::Continue(Descend::No)
388+
}
389+
});
390+
391+
if let Some(format_args_expr) = format_args_expr {
392+
AST_FORMAT_ARGS.with(|ast_format_args| {
393+
ast_format_args.borrow().get(&format_args_expr.span).map(callback);
394+
});
395+
}
396+
}
397+
398+
/// Returns the [`Span`] of the value at `index` extended to the previous comma, e.g. for the value
399+
/// `10`
400+
///
401+
/// ```ignore
402+
/// format("{}.{}", 10, 11)
403+
/// // ^^^^
404+
/// ```
405+
pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
406+
let ctxt = format_args.span.ctxt();
407+
408+
let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
409+
410+
let prev = if index == 0 {
411+
format_args.span
412+
} else {
413+
hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
414+
};
415+
416+
Some(current.with_lo(prev.hi()))
417+
}
418+
342419
/// The format string doesn't exist in the HIR, so we reassemble it from source code
343420
#[derive(Debug)]
344421
pub struct FormatString {

0 commit comments

Comments
 (0)