Skip to content

Commit bd8e17b

Browse files
committed
Restore write_literal suggestions, lint FormatSpec args
1 parent a3553f1 commit bd8e17b

File tree

11 files changed

+476
-71
lines changed

11 files changed

+476
-71
lines changed

clippy_lints/src/format.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl<'tcx> LateLintPass<'tcx> for UselessFormat {
7676
ty::Str => true,
7777
_ => false,
7878
};
79-
if let Some(args) = format_args.args();
79+
if let Some(args) = format_args.args(cx);
8080
if args.iter().all(|arg| arg.format_trait == sym::Display && !arg.has_string_formatting());
8181
then {
8282
let is_new_string = match value.kind {

clippy_lints/src/format_args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs {
7474
if let Some(macro_def_id) = outermost_expn_data.macro_def_id;
7575
if is_format_macro(cx, macro_def_id);
7676
if let ExpnKind::Macro(_, name) = outermost_expn_data.kind;
77-
if let Some(args) = format_args.args();
77+
if let Some(args) = format_args.args(cx);
7878
then {
7979
for (i, arg) in args.iter().enumerate() {
8080
if arg.format_trait != sym::Display {

clippy_lints/src/format_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>,
168168
if let macro_def_id = outer_macro.def_id;
169169
if let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, outer_macro.expn);
170170
if is_format_macro(cx, macro_def_id);
171-
if let Some(args) = format_args.args();
171+
if let Some(args) = format_args.args(cx);
172172
then {
173173
for arg in args {
174174
if arg.format_trait != impl_trait.name {

clippy_lints/src/write.rs

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn, MacroCall
33
use clippy_utils::source::snippet_opt;
44
use rustc_ast::LitKind;
55
use rustc_errors::Applicability;
6-
use rustc_hir::{Expr, ExprKind, Impl, Item, ItemKind};
6+
use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
77
use rustc_lint::{LateContext, LateLintPass, LintContext};
88
use rustc_session::{declare_tool_lint, impl_lint_pass};
99
use rustc_span::{sym, BytePos, Span};
@@ -128,10 +128,6 @@ declare_clippy_lint! {
128128
/// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
129129
/// (i.e., just put the literal in the format string)
130130
///
131-
/// ### Known problems
132-
/// Format strings with format specifiers in are not linted, e.g.
133-
/// `print!("{}, {:#?}", "foo", bar)`
134-
///
135131
/// ### Example
136132
/// ```rust
137133
/// println!("{}", "foo");
@@ -206,10 +202,6 @@ declare_clippy_lint! {
206202
/// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
207203
/// (i.e., just put the literal in the format string)
208204
///
209-
/// ### Known problems
210-
/// Format strings with format specifiers in are not linted, e.g.
211-
/// `print!("{}, {:#?}", "foo", bar)`
212-
///
213205
/// ### Example
214206
/// ```rust
215207
/// # use std::fmt::Write;
@@ -440,38 +432,76 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, ma
440432
}
441433
}
442434

443-
// Expand from `writeln!(o, "")` to `writeln!(o, "")`
444-
// ^^ ^^^^
445-
fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
446-
let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
447-
extended.with_lo(extended.lo() - BytePos(1))
448-
}
449-
450-
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &str) {
451-
if !format_args.specs.is_empty() {
435+
fn check_literal<'tcx>(cx: &LateContext<'tcx>, format_args: &FormatArgsExpn<'tcx>, name: &str) {
436+
if format_args.format_string_span.from_expansion() {
452437
return;
453438
}
439+
let raw = format_args.is_raw(cx);
440+
441+
let Some(args) = format_args.args(cx) else { return };
442+
let mut counts = HirIdMap::<usize>::default();
443+
for arg in &args {
444+
*counts.entry(arg.value.hir_id).or_default() += 1;
445+
}
454446

455-
for &(idx, formatter) in &format_args.formatters {
447+
for arg in args {
456448
if_chain! {
457-
if formatter == sym::Display;
458-
let expr = format_args.value_args[idx];
459-
if !expr.span.from_expansion();
460-
if !format_args.format_string_span.from_expansion();
461-
if let ExprKind::Lit(lit) = &expr.kind;
462-
if let LitKind::Str(..)
463-
| LitKind::Byte(_)
464-
| LitKind::Char(_)
465-
| LitKind::Bool(_) = lit.node;
449+
if counts[&arg.value.hir_id] == 1;
450+
if arg.format_trait == sym::Display;
451+
if !arg.has_primitive_formatting();
452+
if !arg.value.span.from_expansion();
453+
if let ExprKind::Lit(lit) = &arg.value.kind;
466454
then {
455+
let replacement = match lit.node {
456+
LitKind::Str(s, _) => s.to_string(),
457+
LitKind::Char(c) => c.to_string(),
458+
LitKind::Bool(b) => b.to_string(),
459+
_ => continue,
460+
};
461+
467462
let lint = if name.starts_with("write") {
468463
WRITE_LITERAL
469464
} else {
470465
PRINT_LITERAL
471466
};
472467

473-
span_lint(cx, lint, expr.span, "literal with an empty format string")
468+
span_lint_and_then(cx, lint, arg.value.span, "literal with an empty format string", |diag| {
469+
if raw && replacement.contains(&['"', '#']) {
470+
return;
471+
}
472+
473+
let backslash = if raw {
474+
r"\"
475+
} else {
476+
r"\\"
477+
};
478+
let replacement = replacement
479+
.replace('{', "{{")
480+
.replace('}', "}}")
481+
.replace('"', "\\\"")
482+
.replace('\\', backslash);
483+
484+
// `format!("{}", "a")`, `format!("{named}", named = "b")
485+
// ~~~~~ ~~~~~~~~~~~~~
486+
let value_span = expand_past_previous_comma(cx, arg.value.span);
487+
488+
diag.multipart_suggestion(
489+
"try this",
490+
vec![
491+
(arg.span, replacement),
492+
(value_span, String::new()),
493+
],
494+
Applicability::MachineApplicable,
495+
);
496+
});
474497
}
475498
}
476499
}
477500
}
501+
502+
// Expand from `writeln!(o, "")` to `writeln!(o, "")`
503+
// ^^ ^^^^
504+
fn expand_past_previous_comma(cx: &LateContext<'_>, span: Span) -> Span {
505+
let extended = cx.sess().source_map().span_extend_to_prev_char(span, ',', true);
506+
extended.with_lo(extended.lo() - BytePos(1))
507+
}

clippy_utils/src/macros.rs

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
#![allow(clippy::similar_names)] // `expr` and `expn`
22

3+
use crate::source::snippet_opt;
34
use crate::visitors::expr_visitor_no_bodies;
45

56
use arrayvec::ArrayVec;
67
use if_chain::if_chain;
78
use rustc_ast::ast::LitKind;
89
use rustc_hir::intravisit::Visitor;
9-
use rustc_hir::{self as hir, Expr, ExprKind, HirId, Node, QPath};
10+
use rustc_hir::{self as hir, Expr, ExprField, ExprKind, HirId, Node, QPath};
1011
use rustc_lint::LateContext;
1112
use rustc_span::def_id::DefId;
1213
use rustc_span::hygiene::{self, MacroKind, SyntaxContext};
13-
use rustc_span::{sym, ExpnData, ExpnId, ExpnKind, Span, Symbol};
14+
use rustc_span::source_map::Spanned;
15+
use rustc_span::{sym, BytePos, ExpnData, ExpnId, ExpnKind, Pos, Span, Symbol};
16+
use std::iter;
1417
use std::ops::ControlFlow;
1518

1619
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[
@@ -66,7 +69,7 @@ impl MacroCall {
6669

6770
/// Returns an iterator of expansions that created the given span
6871
pub fn expn_backtrace(mut span: Span) -> impl Iterator<Item = (ExpnId, ExpnData)> {
69-
std::iter::from_fn(move || {
72+
iter::from_fn(move || {
7073
let ctxt = span.ctxt();
7174
if ctxt == SyntaxContext::root() {
7275
return None;
@@ -90,7 +93,7 @@ pub fn expn_is_local(expn: ExpnId) -> bool {
9093
}
9194
let data = expn.expn_data();
9295
let backtrace = expn_backtrace(data.call_site);
93-
std::iter::once((expn, data))
96+
iter::once((expn, data))
9497
.chain(backtrace)
9598
.find_map(|(_, data)| data.macro_def_id)
9699
.map_or(true, DefId::is_local)
@@ -467,20 +470,24 @@ impl<'tcx> FormatArgsExpn<'tcx> {
467470
}
468471

469472
/// Returns a vector of `FormatArgsArg`.
470-
pub fn args(&self) -> Option<Vec<FormatArgsArg<'tcx>>> {
473+
pub fn args(&self, cx: &LateContext<'tcx>) -> Option<Vec<FormatArgsArg<'tcx>>> {
474+
let spans = self.format_argument_spans(cx)?;
475+
471476
if self.specs.is_empty() {
472-
let args = std::iter::zip(&self.value_args, &self.formatters)
473-
.map(|(value, &(_, format_trait))| FormatArgsArg {
477+
let args = iter::zip(&self.value_args, &self.formatters)
478+
.zip(spans)
479+
.map(|((value, &(_, format_trait)), span)| FormatArgsArg {
474480
value,
475481
format_trait,
482+
span,
476483
spec: None,
477484
})
478485
.collect();
479486
return Some(args);
480487
}
481-
self.specs
482-
.iter()
483-
.map(|spec| {
488+
489+
iter::zip(&self.specs, spans)
490+
.map(|(spec, span)| {
484491
if_chain! {
485492
// struct `core::fmt::rt::v1::Argument`
486493
if let ExprKind::Struct(_, fields, _) = spec.kind;
@@ -493,6 +500,7 @@ impl<'tcx> FormatArgsExpn<'tcx> {
493500
Some(FormatArgsArg {
494501
value: self.value_args[j],
495502
format_trait,
503+
span,
496504
spec: Some(spec),
497505
})
498506
} else {
@@ -503,6 +511,14 @@ impl<'tcx> FormatArgsExpn<'tcx> {
503511
.collect()
504512
}
505513

514+
pub fn is_raw(&self, cx: &LateContext<'tcx>) -> bool {
515+
if let Some(format_string) = snippet_opt(cx, self.format_string_span) {
516+
format_string.starts_with("r")
517+
} else {
518+
false
519+
}
520+
}
521+
506522
/// Source callsite span of all inputs
507523
pub fn inputs_span(&self) -> Span {
508524
match *self.value_args {
@@ -512,6 +528,36 @@ impl<'tcx> FormatArgsExpn<'tcx> {
512528
.to(hygiene::walk_chain(last.span, self.format_string_span.ctxt())),
513529
}
514530
}
531+
532+
/// Returns the spans covering the `{}`s in the format string
533+
fn format_argument_spans(&self, cx: &LateContext<'tcx>) -> Option<Vec<Span>> {
534+
let format_string = snippet_opt(cx, self.format_string_span)?.into_bytes();
535+
let lo = self.format_string_span.lo();
536+
let get = |i| format_string.get(i).copied();
537+
538+
let mut spans = Vec::new();
539+
let mut i = 0;
540+
let mut arg_start = 0;
541+
loop {
542+
match (get(i), get(i + 1)) {
543+
(Some(b'{'), Some(b'{')) | (Some(b'}'), Some(b'}')) => {
544+
i += 1;
545+
},
546+
(Some(b'{'), _) => arg_start = i,
547+
(Some(b'}'), _) => spans.push(
548+
self.format_string_span
549+
.with_lo(lo + BytePos::from_usize(arg_start))
550+
.with_hi(lo + BytePos::from_usize(i + 1)),
551+
),
552+
(None, _) => break,
553+
_ => {},
554+
}
555+
556+
i += 1;
557+
}
558+
559+
Some(spans)
560+
}
515561
}
516562

517563
/// Type representing a `FormatArgsExpn`'s format arguments
@@ -522,12 +568,36 @@ pub struct FormatArgsArg<'tcx> {
522568
pub format_trait: Symbol,
523569
/// An element of `specs`
524570
pub spec: Option<&'tcx Expr<'tcx>>,
571+
/// Span of the `{}`
572+
pub span: Span,
525573
}
526574

527575
impl<'tcx> FormatArgsArg<'tcx> {
528576
/// Returns true if any formatting parameters are used that would have an effect on strings,
529-
/// like `{:+2}` instead of just `{}`.
577+
/// like `{:<2}` instead of just `{}`.
578+
///
579+
/// `{:+}` would return false for `has_string_formatting`, but true for
580+
/// `has_primitive_formatting` as it effects numbers but not strings
530581
pub fn has_string_formatting(&self) -> bool {
582+
self.has_formatting(field_effects_string)
583+
}
584+
585+
/// Returns true if any formatting parameters are used that would have an effect on primitives,
586+
/// like `{:+2}` instead of just `{}`.
587+
pub fn has_primitive_formatting(&self) -> bool {
588+
self.has_formatting(|field| match field.ident.name {
589+
sym::flags => matches!(
590+
field.expr.kind,
591+
ExprKind::Lit(Spanned {
592+
node: LitKind::Int(0, _),
593+
..
594+
}),
595+
),
596+
_ => field_effects_string(field),
597+
})
598+
}
599+
600+
fn has_formatting(&self, callback: impl FnMut(&ExprField<'_>) -> bool) -> bool {
531601
self.spec.map_or(false, |spec| {
532602
// `!` because these conditions check that `self` is unformatted.
533603
!if_chain! {
@@ -536,21 +606,23 @@ impl<'tcx> FormatArgsArg<'tcx> {
536606
if let Some(format_field) = fields.iter().find(|f| f.ident.name == sym::format);
537607
// struct `core::fmt::rt::v1::FormatSpec`
538608
if let ExprKind::Struct(_, subfields, _) = format_field.expr.kind;
539-
if subfields.iter().all(|field| match field.ident.name {
540-
sym::precision | sym::width => match field.expr.kind {
541-
ExprKind::Path(QPath::Resolved(_, path)) => {
542-
path.segments.last().unwrap().ident.name == sym::Implied
543-
}
544-
_ => false,
545-
}
546-
_ => true,
547-
});
609+
if subfields.iter().all(callback);
548610
then { true } else { false }
549611
}
550612
})
551613
}
552614
}
553615

616+
fn field_effects_string(field: &ExprField<'_>) -> bool {
617+
match field.ident.name {
618+
sym::precision | sym::width => match field.expr.kind {
619+
ExprKind::Path(QPath::Resolved(_, path)) => path.segments.last().unwrap().ident.name == sym::Implied,
620+
_ => false,
621+
},
622+
_ => true,
623+
}
624+
}
625+
554626
/// A node with a `HirId` and a `Span`
555627
pub trait HirNode {
556628
fn hir_id(&self) -> HirId;

tests/ui/print_literal.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,15 @@ fn main() {
2626
print!("Hello {}", "world");
2727
println!("Hello {} {}", world, "world");
2828
println!("Hello {}", "world");
29+
println!("{} {:.4}", "a literal", 5);
2930

3031
// positional args don't change the fact
3132
// that we're using a literal -- this should
3233
// throw a warning
3334
println!("{0} {1}", "hello", "world");
35+
println!("{1} {0}", "hello", "world");
3436

3537
// named args shouldn't change anything either
3638
println!("{foo} {bar}", foo = "hello", bar = "world");
37-
38-
// expansions containing FormatSpecs are not currently supported
39-
println!("{1} {0}", "hello", "world");
4039
println!("{bar} {foo}", foo = "hello", bar = "world");
41-
println!("{} {:.4}", "a literal", 5);
4240
}

0 commit comments

Comments
 (0)