Skip to content

Commit d695bfc

Browse files
committed
Auto merge of #6821 - Jarcho:write_literal_suggestion, r=flip1995
Write literal suggestion fixes: #6768 changelog: Add suggestion to `write_literal` and `print_literal` lints changelog: Change `use_debug` to point only at the format string
2 parents a2ee849 + d45873b commit d695bfc

File tree

6 files changed

+454
-115
lines changed

6 files changed

+454
-115
lines changed

clippy_lints/src/write.rs

+191-93
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
use std::borrow::Cow;
2-
use std::ops::Range;
2+
use std::iter;
3+
use std::ops::{Deref, Range};
34

45
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg, span_lint_and_then};
5-
use clippy_utils::source::snippet_with_applicability;
6-
use if_chain::if_chain;
7-
use rustc_ast::ast::{Expr, ExprKind, ImplKind, Item, ItemKind, LitKind, MacCall, StrLit, StrStyle};
8-
use rustc_ast::token;
6+
use clippy_utils::source::{snippet_opt, snippet_with_applicability};
7+
use rustc_ast::ast::{Expr, ExprKind, ImplKind, Item, ItemKind, MacCall, Path, StrLit, StrStyle};
8+
use rustc_ast::token::{self, LitKind};
99
use rustc_ast::tokenstream::TokenStream;
1010
use rustc_errors::Applicability;
1111
use rustc_lexer::unescape::{self, EscapeError};
1212
use rustc_lint::{EarlyContext, EarlyLintPass};
1313
use rustc_parse::parser;
1414
use rustc_session::{declare_tool_lint, impl_lint_pass};
15-
use rustc_span::symbol::kw;
16-
use rustc_span::{sym, BytePos, Span};
15+
use rustc_span::symbol::{kw, Symbol};
16+
use rustc_span::{sym, BytePos, Span, DUMMY_SP};
1717

1818
declare_clippy_lint! {
1919
/// **What it does:** This lint warns when you use `println!("")` to
@@ -354,7 +354,120 @@ fn newline_span(fmtstr: &StrLit) -> Span {
354354
sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
355355
}
356356

357+
/// Stores a list of replacement spans for each argument, but only if all the replacements used an
358+
/// empty format string.
359+
#[derive(Default)]
360+
struct SimpleFormatArgs {
361+
unnamed: Vec<Vec<Span>>,
362+
named: Vec<(Symbol, Vec<Span>)>,
363+
}
364+
impl SimpleFormatArgs {
365+
fn get_unnamed(&self) -> impl Iterator<Item = &[Span]> {
366+
self.unnamed.iter().map(|x| match x.as_slice() {
367+
// Ignore the dummy span added from out of order format arguments.
368+
[DUMMY_SP] => &[],
369+
x => x,
370+
})
371+
}
372+
373+
fn get_named(&self, n: &Path) -> &[Span] {
374+
self.named.iter().find(|x| *n == x.0).map_or(&[], |x| x.1.as_slice())
375+
}
376+
377+
fn push(&mut self, arg: rustc_parse_format::Argument<'_>, span: Span) {
378+
use rustc_parse_format::{
379+
AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec,
380+
};
381+
382+
const SIMPLE: FormatSpec<'_> = FormatSpec {
383+
fill: None,
384+
align: AlignUnknown,
385+
flags: 0,
386+
precision: CountImplied,
387+
precision_span: None,
388+
width: CountImplied,
389+
width_span: None,
390+
ty: "",
391+
ty_span: None,
392+
};
393+
394+
match arg.position {
395+
ArgumentIs(n) | ArgumentImplicitlyIs(n) => {
396+
if self.unnamed.len() <= n {
397+
// Use a dummy span to mark all unseen arguments.
398+
self.unnamed.resize_with(n, || vec![DUMMY_SP]);
399+
if arg.format == SIMPLE {
400+
self.unnamed.push(vec![span]);
401+
} else {
402+
self.unnamed.push(Vec::new());
403+
}
404+
} else {
405+
let args = &mut self.unnamed[n];
406+
match (args.as_mut_slice(), arg.format == SIMPLE) {
407+
// A non-empty format string has been seen already.
408+
([], _) => (),
409+
// Replace the dummy span, if it exists.
410+
([dummy @ DUMMY_SP], true) => *dummy = span,
411+
([_, ..], true) => args.push(span),
412+
([_, ..], false) => *args = Vec::new(),
413+
}
414+
}
415+
},
416+
ArgumentNamed(n) => {
417+
if let Some(x) = self.named.iter_mut().find(|x| x.0 == n) {
418+
match x.1.as_slice() {
419+
// A non-empty format string has been seen already.
420+
[] => (),
421+
[_, ..] if arg.format == SIMPLE => x.1.push(span),
422+
[_, ..] => x.1 = Vec::new(),
423+
}
424+
} else if arg.format == SIMPLE {
425+
self.named.push((n, vec![span]));
426+
} else {
427+
self.named.push((n, Vec::new()));
428+
}
429+
},
430+
};
431+
}
432+
}
433+
357434
impl Write {
435+
/// Parses a format string into a collection of spans for each argument. This only keeps track
436+
/// of empty format arguments. Will also lint usages of debug format strings outside of debug
437+
/// impls.
438+
fn parse_fmt_string(&self, cx: &EarlyContext<'_>, str_lit: &StrLit) -> Option<SimpleFormatArgs> {
439+
use rustc_parse_format::{ParseMode, Parser, Piece};
440+
441+
let str_sym = str_lit.symbol_unescaped.as_str();
442+
let style = match str_lit.style {
443+
StrStyle::Cooked => None,
444+
StrStyle::Raw(n) => Some(n as usize),
445+
};
446+
447+
let mut parser = Parser::new(&str_sym, style, snippet_opt(cx, str_lit.span), false, ParseMode::Format);
448+
let mut args = SimpleFormatArgs::default();
449+
450+
while let Some(arg) = parser.next() {
451+
let arg = match arg {
452+
Piece::String(_) => continue,
453+
Piece::NextArgument(arg) => arg,
454+
};
455+
let span = parser
456+
.arg_places
457+
.last()
458+
.map_or(DUMMY_SP, |&x| str_lit.span.from_inner(x));
459+
460+
if !self.in_debug_impl && arg.format.ty == "?" {
461+
// FIXME: modify rustc's fmt string parser to give us the current span
462+
span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting");
463+
}
464+
465+
args.push(arg, span);
466+
}
467+
468+
parser.errors.is_empty().then(move || args)
469+
}
470+
358471
/// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
359472
/// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
360473
/// the contents of the string, whether it's a raw string, and the span of the literal in the
@@ -376,111 +489,96 @@ impl Write {
376489
/// ```
377490
#[allow(clippy::too_many_lines)]
378491
fn check_tts<'a>(&self, cx: &EarlyContext<'a>, tts: TokenStream, is_write: bool) -> (Option<StrLit>, Option<Expr>) {
379-
use rustc_parse_format::{
380-
AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec, ParseMode, Parser,
381-
Piece,
382-
};
383-
384492
let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
385-
let mut expr: Option<Expr> = None;
386-
if is_write {
387-
expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
388-
Ok(p) => Some(p.into_inner()),
389-
Err(_) => return (None, None),
390-
};
391-
// might be `writeln!(foo)`
392-
if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
393-
return (None, expr);
493+
let expr = if is_write {
494+
match parser
495+
.parse_expr()
496+
.map(rustc_ast::ptr::P::into_inner)
497+
.map_err(|mut e| e.cancel())
498+
{
499+
// write!(e, ...)
500+
Ok(p) if parser.eat(&token::Comma) => Some(p),
501+
// write!(e) or error
502+
e => return (None, e.ok()),
394503
}
395-
}
504+
} else {
505+
None
506+
};
396507

397508
let fmtstr = match parser.parse_str_lit() {
398509
Ok(fmtstr) => fmtstr,
399510
Err(_) => return (None, expr),
400511
};
401-
let tmp = fmtstr.symbol.as_str();
402-
let mut args = vec![];
403-
let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
404-
while let Some(piece) = fmt_parser.next() {
405-
if !fmt_parser.errors.is_empty() {
406-
return (None, expr);
407-
}
408-
if let Piece::NextArgument(arg) = piece {
409-
if !self.in_debug_impl && arg.format.ty == "?" {
410-
// FIXME: modify rustc's fmt string parser to give us the current span
411-
span_lint(cx, USE_DEBUG, parser.prev_token.span, "use of `Debug`-based formatting");
412-
}
413-
args.push(arg);
414-
}
415-
}
512+
513+
let args = match self.parse_fmt_string(cx, &fmtstr) {
514+
Some(args) => args,
515+
None => return (Some(fmtstr), expr),
516+
};
517+
416518
let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
417-
let mut idx = 0;
519+
let mut unnamed_args = args.get_unnamed();
418520
loop {
419-
const SIMPLE: FormatSpec<'_> = FormatSpec {
420-
fill: None,
421-
align: AlignUnknown,
422-
flags: 0,
423-
precision: CountImplied,
424-
precision_span: None,
425-
width: CountImplied,
426-
width_span: None,
427-
ty: "",
428-
ty_span: None,
429-
};
430521
if !parser.eat(&token::Comma) {
431522
return (Some(fmtstr), expr);
432523
}
524+
525+
let comma_span = parser.prev_token.span;
433526
let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
434527
expr
435528
} else {
436529
return (Some(fmtstr), None);
437530
};
438-
match &token_expr.kind {
439-
ExprKind::Lit(lit) if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..)) => {
440-
let mut all_simple = true;
441-
let mut seen = false;
442-
for arg in &args {
443-
match arg.position {
444-
ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
445-
if n == idx {
446-
all_simple &= arg.format == SIMPLE;
447-
seen = true;
448-
}
449-
},
450-
ArgumentNamed(_) => {},
451-
}
452-
}
453-
if all_simple && seen {
454-
span_lint(cx, lint, token_expr.span, "literal with an empty format string");
455-
}
456-
idx += 1;
531+
let (fmt_spans, lit) = match &token_expr.kind {
532+
ExprKind::Lit(lit) => (unnamed_args.next().unwrap_or(&[]), lit),
533+
ExprKind::Assign(lhs, rhs, _) => match (&lhs.kind, &rhs.kind) {
534+
(ExprKind::Path(_, p), ExprKind::Lit(lit)) => (args.get_named(p), lit),
535+
_ => continue,
457536
},
458-
ExprKind::Assign(lhs, rhs, _) => {
459-
if_chain! {
460-
if let ExprKind::Lit(ref lit) = rhs.kind;
461-
if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..));
462-
if let ExprKind::Path(_, p) = &lhs.kind;
463-
then {
464-
let mut all_simple = true;
465-
let mut seen = false;
466-
for arg in &args {
467-
match arg.position {
468-
ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
469-
ArgumentNamed(name) => {
470-
if *p == name {
471-
seen = true;
472-
all_simple &= arg.format == SIMPLE;
473-
}
474-
},
475-
}
476-
}
477-
if all_simple && seen {
478-
span_lint(cx, lint, rhs.span, "literal with an empty format string");
479-
}
480-
}
481-
}
537+
_ => {
538+
unnamed_args.next();
539+
continue;
540+
},
541+
};
542+
543+
let replacement: String = match lit.token.kind {
544+
LitKind::Integer | LitKind::Float | LitKind::Err => continue,
545+
LitKind::StrRaw(_) | LitKind::ByteStrRaw(_) if matches!(fmtstr.style, StrStyle::Raw(_)) => {
546+
lit.token.symbol.as_str().replace("{", "{{").replace("}", "}}")
482547
},
483-
_ => idx += 1,
548+
LitKind::Str | LitKind::ByteStr if matches!(fmtstr.style, StrStyle::Cooked) => {
549+
lit.token.symbol.as_str().replace("{", "{{").replace("}", "}}")
550+
},
551+
LitKind::StrRaw(_) | LitKind::Str | LitKind::ByteStrRaw(_) | LitKind::ByteStr => continue,
552+
LitKind::Byte | LitKind::Char => match &*lit.token.symbol.as_str() {
553+
"\"" if matches!(fmtstr.style, StrStyle::Cooked) => "\\\"",
554+
"\"" if matches!(fmtstr.style, StrStyle::Raw(0)) => continue,
555+
"\\\\" if matches!(fmtstr.style, StrStyle::Raw(_)) => "\\",
556+
"\\'" => "'",
557+
"{" => "{{",
558+
"}" => "}}",
559+
x if matches!(fmtstr.style, StrStyle::Raw(_)) && x.starts_with('\\') => continue,
560+
x => x,
561+
}
562+
.into(),
563+
LitKind::Bool => lit.token.symbol.as_str().deref().into(),
564+
};
565+
566+
if !fmt_spans.is_empty() {
567+
span_lint_and_then(
568+
cx,
569+
lint,
570+
token_expr.span,
571+
"literal with an empty format string",
572+
|diag| {
573+
diag.multipart_suggestion(
574+
"try this",
575+
iter::once((comma_span.to(token_expr.span), String::new()))
576+
.chain(fmt_spans.iter().cloned().zip(iter::repeat(replacement)))
577+
.collect(),
578+
Applicability::MachineApplicable,
579+
);
580+
},
581+
);
484582
}
485583
}
486584
}

tests/ui/print.stderr

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
error: use of `Debug`-based formatting
2-
--> $DIR/print.rs:11:19
2+
--> $DIR/print.rs:11:20
33
|
44
LL | write!(f, "{:?}", 43.1415)
5-
| ^^^^^^
5+
| ^^^^
66
|
77
= note: `-D clippy::use-debug` implied by `-D warnings`
88

@@ -33,10 +33,10 @@ LL | print!("Hello {:?}", "World");
3333
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3434

3535
error: use of `Debug`-based formatting
36-
--> $DIR/print.rs:28:12
36+
--> $DIR/print.rs:28:19
3737
|
3838
LL | print!("Hello {:?}", "World");
39-
| ^^^^^^^^^^^^
39+
| ^^^^
4040

4141
error: use of `print!`
4242
--> $DIR/print.rs:30:5
@@ -45,10 +45,10 @@ LL | print!("Hello {:#?}", "#orld");
4545
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4646

4747
error: use of `Debug`-based formatting
48-
--> $DIR/print.rs:30:12
48+
--> $DIR/print.rs:30:19
4949
|
5050
LL | print!("Hello {:#?}", "#orld");
51-
| ^^^^^^^^^^^^^
51+
| ^^^^^
5252

5353
error: aborting due to 8 previous errors
5454

0 commit comments

Comments
 (0)