1 use clippy_utils
::diagnostics
::{span_lint, span_lint_and_then}
;
2 use clippy_utils
::macros
::{root_macro_call_first_node, FormatArgsExpn, MacroCall}
;
3 use clippy_utils
::source
::{expand_past_previous_comma, snippet_opt}
;
4 use clippy_utils
::{is_in_cfg_test, is_in_test_function}
;
5 use rustc_ast
::LitKind
;
6 use rustc_errors
::Applicability
;
7 use rustc_hir
::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind}
;
8 use rustc_lint
::{LateContext, LateLintPass, LintContext}
;
9 use rustc_session
::{declare_tool_lint, impl_lint_pass}
;
10 use rustc_span
::{sym, BytePos}
;
12 declare_clippy_lint
! {
14 /// This lint warns when you use `println!("")` to
17 /// ### Why is this bad?
18 /// You should use `println!()`, which is simpler.
29 #[clippy::version = "pre 1.29.0"]
30 pub PRINTLN_EMPTY_STRING
,
32 "using `println!(\"\")` with an empty string"
35 declare_clippy_lint
! {
37 /// This lint warns when you use `print!()` with a format
38 /// string that ends in a newline.
40 /// ### Why is this bad?
41 /// You should use `println!()` instead, which appends the
46 /// # let name = "World";
47 /// print!("Hello {}!\n", name);
49 /// use println!() instead
51 /// # let name = "World";
52 /// println!("Hello {}!", name);
54 #[clippy::version = "pre 1.29.0"]
55 pub PRINT_WITH_NEWLINE
,
57 "using `print!()` with a format string that ends in a single newline"
60 declare_clippy_lint
! {
62 /// Checks for printing on *stdout*. The purpose of this lint
63 /// is to catch debugging remnants.
65 /// ### Why is this bad?
66 /// People often print on *stdout* while debugging an
67 /// application and might forget to remove those prints afterward.
69 /// ### Known problems
70 /// Only catches `print!` and `println!` calls.
74 /// println!("Hello world!");
76 #[clippy::version = "pre 1.29.0"]
82 declare_clippy_lint
! {
84 /// Checks for printing on *stderr*. The purpose of this lint
85 /// is to catch debugging remnants.
87 /// ### Why is this bad?
88 /// People often print on *stderr* while debugging an
89 /// application and might forget to remove those prints afterward.
91 /// ### Known problems
92 /// Only catches `eprint!` and `eprintln!` calls.
96 /// eprintln!("Hello world!");
98 #[clippy::version = "1.50.0"]
104 declare_clippy_lint
! {
106 /// Checks for use of `Debug` formatting. The purpose of this
107 /// lint is to catch debugging remnants.
109 /// ### Why is this bad?
110 /// The purpose of the `Debug` trait is to facilitate
111 /// debugging Rust code. It should not be used in user-facing output.
115 /// # let foo = "bar";
116 /// println!("{:?}", foo);
118 #[clippy::version = "pre 1.29.0"]
121 "use of `Debug`-based formatting"
124 declare_clippy_lint
! {
126 /// This lint warns about the use of literals as `print!`/`println!` args.
128 /// ### Why is this bad?
129 /// Using literals as `println!` args is inefficient
130 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
131 /// (i.e., just put the literal in the format string)
135 /// println!("{}", "foo");
137 /// use the literal without formatting:
141 #[clippy::version = "pre 1.29.0"]
144 "printing a literal with a format string"
147 declare_clippy_lint
! {
149 /// This lint warns when you use `writeln!(buf, "")` to
152 /// ### Why is this bad?
153 /// You should use `writeln!(buf)`, which is simpler.
157 /// # use std::fmt::Write;
158 /// # let mut buf = String::new();
159 /// writeln!(buf, "");
164 /// # use std::fmt::Write;
165 /// # let mut buf = String::new();
168 #[clippy::version = "pre 1.29.0"]
169 pub WRITELN_EMPTY_STRING
,
171 "using `writeln!(buf, \"\")` with an empty string"
174 declare_clippy_lint
! {
176 /// This lint warns when you use `write!()` with a format
178 /// ends in a newline.
180 /// ### Why is this bad?
181 /// You should use `writeln!()` instead, which appends the
186 /// # use std::fmt::Write;
187 /// # let mut buf = String::new();
188 /// # let name = "World";
189 /// write!(buf, "Hello {}!\n", name);
194 /// # use std::fmt::Write;
195 /// # let mut buf = String::new();
196 /// # let name = "World";
197 /// writeln!(buf, "Hello {}!", name);
199 #[clippy::version = "pre 1.29.0"]
200 pub WRITE_WITH_NEWLINE
,
202 "using `write!()` with a format string that ends in a single newline"
205 declare_clippy_lint
! {
207 /// This lint warns about the use of literals as `write!`/`writeln!` args.
209 /// ### Why is this bad?
210 /// Using literals as `writeln!` args is inefficient
211 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
212 /// (i.e., just put the literal in the format string)
216 /// # use std::fmt::Write;
217 /// # let mut buf = String::new();
218 /// writeln!(buf, "{}", "foo");
223 /// # use std::fmt::Write;
224 /// # let mut buf = String::new();
225 /// writeln!(buf, "foo");
227 #[clippy::version = "pre 1.29.0"]
230 "writing a literal with a format string"
236 allow_print_in_tests
: bool
,
240 pub fn new(allow_print_in_tests
: bool
) -> Self {
242 allow_print_in_tests
,
248 impl_lint_pass
!(Write
=> [
250 PRINTLN_EMPTY_STRING
,
256 WRITELN_EMPTY_STRING
,
260 impl<'tcx
> LateLintPass
<'tcx
> for Write
{
261 fn check_item(&mut self, cx
: &LateContext
<'_
>, item
: &Item
<'_
>) {
262 if is_debug_impl(cx
, item
) {
263 self.in_debug_impl
= true;
267 fn check_item_post(&mut self, cx
: &LateContext
<'_
>, item
: &Item
<'_
>) {
268 if is_debug_impl(cx
, item
) {
269 self.in_debug_impl
= false;
273 fn check_expr(&mut self, cx
: &LateContext
<'tcx
>, expr
: &'tcx Expr
<'_
>) {
274 let Some(macro_call
) = root_macro_call_first_node(cx
, expr
) else { return }
;
275 let Some(diag_name
) = cx
.tcx
.get_diagnostic_name(macro_call
.def_id
) else { return }
;
276 let Some(name
) = diag_name
.as_str().strip_suffix("_macro") else { return }
;
278 let is_build_script
= cx
283 .map_or(false, |crate_name
| crate_name
== "build_script_build");
285 let allowed_in_tests
= self.allow_print_in_tests
286 && (is_in_test_function(cx
.tcx
, expr
.hir_id
) || is_in_cfg_test(cx
.tcx
, expr
.hir_id
));
288 sym
::print_macro
| sym
::println_macro
if !allowed_in_tests
=> {
289 if !is_build_script
{
290 span_lint(cx
, PRINT_STDOUT
, macro_call
.span
, &format
!("use of `{name}!`"));
293 sym
::eprint_macro
| sym
::eprintln_macro
if !allowed_in_tests
=> {
294 span_lint(cx
, PRINT_STDERR
, macro_call
.span
, &format
!("use of `{name}!`"));
296 sym
::write_macro
| sym
::writeln_macro
=> {}
,
300 let Some(format_args
) = FormatArgsExpn
::find_nested(cx
, expr
, macro_call
.expn
) else { return }
;
302 // ignore `writeln!(w)` and `write!(v, some_macro!())`
303 if format_args
.format_string
.span
.from_expansion() {
308 sym
::print_macro
| sym
::eprint_macro
| sym
::write_macro
=> {
309 check_newline(cx
, &format_args
, ¯o_call
, name
);
311 sym
::println_macro
| sym
::eprintln_macro
| sym
::writeln_macro
=> {
312 check_empty_string(cx
, &format_args
, ¯o_call
, name
);
317 check_literal(cx
, &format_args
, name
);
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");
328 fn is_debug_impl(cx
: &LateContext
<'_
>, item
: &Item
<'_
>) -> bool
{
329 if let ItemKind
::Impl(Impl { of_trait: Some(trait_ref), .. }
) = &item
.kind
330 && let Some(trait_id
) = trait_ref
.trait_def_id()
332 cx
.tcx
.is_diagnostic_item(sym
::Debug
, trait_id
)
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
;
342 let Some(last
) = format_string_parts
.last() else { return }
;
344 let count_vertical_whitespace
= || {
347 .flat_map(|part
| part
.as_str().chars())
348 .filter(|ch
| matches
!(ch
, '
\r'
| '
\n'
))
352 if last
.as_str().ends_with('
\n'
)
353 // ignore format strings with other internal vertical whitespace
354 && count_vertical_whitespace() == 1
356 // ignore trailing arguments: `print!("Issue\n{}", 1265);`
357 && format_string_parts
.len() > format_args
.args
.len()
359 let lint
= if name
== "write" {
360 format_string_span
= expand_past_previous_comma(cx
, format_string_span
);
371 &format
!("using `{name}!()` with a format string that ends in a single newline"),
373 let name_span
= cx
.sess().source_map().span_until_char(macro_call
.span
, '
!'
);
374 let Some(format_snippet
) = snippet_opt(cx
, format_string_span
) else { return }
;
376 if format_string_parts
.len() == 1 && last
.as_str() == "\n" {
377 // print!("\n"), write!(f, "\n")
379 diag
.multipart_suggestion(
380 format
!("use `{name}ln!` instead"),
381 vec
![(name_span
, format
!("{name}ln")), (format_string_span
, String
::new())],
382 Applicability
::MachineApplicable
,
384 } else if format_snippet
.ends_with("\\n\"") {
385 // print!("...\n"), write!(f, "...\n")
387 let hi
= format_string_span
.hi();
388 let newline_span
= format_string_span
.with_lo(hi
- BytePos(3)).with_hi(hi
- BytePos(1));
390 diag
.multipart_suggestion(
391 format
!("use `{name}ln!` instead"),
392 vec
![(name_span
, format
!("{name}ln")), (newline_span
, String
::new())],
393 Applicability
::MachineApplicable
,
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"
406 let lint
= if name
== "writeln" {
407 span
= expand_past_previous_comma(cx
, span
);
418 &format
!("empty string literal in `{name}!`"),
420 diag
.span_suggestion(
422 "remove the empty string",
424 Applicability
::MachineApplicable
,
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;
437 for arg
in &format_args
.args
{
438 let value
= arg
.param
.value
;
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
)
446 let (replacement
, replace_raw
) = match lit
.node
{
447 LitKind
::Str(..) => extract_str_literal(&value_string
),
448 LitKind
::Char(ch
) => (
452 _ => &value_string[1..value_string.len() - 1],
457 LitKind::Bool(b) => (b.to_string(), false),
461 let lint = if name.starts_with("write
") {
467 let format_string_is_raw = format_args.format_string.style.is_some();
468 let replacement = match (format_string_is_raw, replace_raw) {
469 (false, false) => Some(replacement),
470 (false, true) => Some(replacement.replace('"'
, "\\\"").replace('
\\'
, "\\\\")),
471 (true, false) => match conservative_unescape(&replacement
) {
472 Ok(unescaped
) => Some(unescaped
),
473 Err(UnescapeErr
::Lint
) => None
,
474 Err(UnescapeErr
::Ignore
) => continue,
477 if replacement
.contains(['
#', '"']) {
489 "literal with an empty format string",
491 if let Some(replacement) = replacement
492 // `format!("{}", "a")`, `format!("{named}", named = "b")
493 // ~~~~~ ~~~~~~~~~~~~~
494 && let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
496 let replacement = replacement.replace('{', "{{").replace('}', "}}");
497 diag.multipart_suggestion(
499 vec![(arg.span, replacement), (value_span, String::new())],
500 Applicability::MachineApplicable,
509 /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
511 /// `r#"a"#` -> (`a`, true)
513 /// `"b"` -> (`b`, false)
514 fn extract_str_literal(literal: &str) -> (String, bool) {
515 let (literal, raw) = match literal.strip_prefix('r') {
516 Some(stripped) => (stripped.trim_matches('#'), true),
517 None => (literal, false),
520 (literal[1..literal.len() - 1].to_string(), raw)
524 /// Should still be linted, can be manually resolved by author, e.g.
527 /// print!(r"{}", '"');
530 /// Should not be linted, e.g.
533 /// print!(r"{}", '\r');
538 /// Unescape a normal string into a raw string
539 fn conservative_unescape(literal
: &str) -> Result
<String
, UnescapeErr
> {
540 let mut unescaped
= String
::with_capacity(literal
.len());
541 let mut chars
= literal
.chars();
544 while let Some(ch
) = chars
.next() {
547 '
\\'
=> match chars
.next() {
548 Some('
\\'
) => unescaped
.push('
\\'
),
549 Some('
"') => err = true,
550 _ => return Err(UnescapeErr::Ignore),
552 _ => unescaped.push(ch),
556 if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }