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 rustc_ast::LitKind;
5 use rustc_errors::Applicability;
6 use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
7 use rustc_lint::{LateContext, LateLintPass, LintContext};
8 use rustc_session::{declare_tool_lint, impl_lint_pass};
9 use rustc_span::{sym, BytePos};
11 declare_clippy_lint! {
13 /// This lint warns when you use `println!("")` to
16 /// ### Why is this bad?
17 /// You should use `println!()`, which is simpler.
28 #[clippy::version = "pre 1.29.0"]
29 pub PRINTLN_EMPTY_STRING,
31 "using `println!(\"\")` with an empty string"
34 declare_clippy_lint! {
36 /// This lint warns when you use `print!()` with a format
37 /// string that ends in a newline.
39 /// ### Why is this bad?
40 /// You should use `println!()` instead, which appends the
45 /// # let name = "World";
46 /// print!("Hello {}!\n", name);
48 /// use println!() instead
50 /// # let name = "World";
51 /// println!("Hello {}!", name);
53 #[clippy::version = "pre 1.29.0"]
54 pub PRINT_WITH_NEWLINE,
56 "using `print!()` with a format string that ends in a single newline"
59 declare_clippy_lint! {
61 /// Checks for printing on *stdout*. The purpose of this lint
62 /// is to catch debugging remnants.
64 /// ### Why is this bad?
65 /// People often print on *stdout* while debugging an
66 /// application and might forget to remove those prints afterward.
68 /// ### Known problems
69 /// Only catches `print!` and `println!` calls.
73 /// println!("Hello world!");
75 #[clippy::version = "pre 1.29.0"]
81 declare_clippy_lint! {
83 /// Checks for printing on *stderr*. The purpose of this lint
84 /// is to catch debugging remnants.
86 /// ### Why is this bad?
87 /// People often print on *stderr* while debugging an
88 /// application and might forget to remove those prints afterward.
90 /// ### Known problems
91 /// Only catches `eprint!` and `eprintln!` calls.
95 /// eprintln!("Hello world!");
97 #[clippy::version = "1.50.0"]
103 declare_clippy_lint! {
105 /// Checks for use of `Debug` formatting. The purpose of this
106 /// lint is to catch debugging remnants.
108 /// ### Why is this bad?
109 /// The purpose of the `Debug` trait is to facilitate
110 /// debugging Rust code. It should not be used in user-facing output.
114 /// # let foo = "bar";
115 /// println!("{:?}", foo);
117 #[clippy::version = "pre 1.29.0"]
120 "use of `Debug`-based formatting"
123 declare_clippy_lint! {
125 /// This lint warns about the use of literals as `print!`/`println!` args.
127 /// ### Why is this bad?
128 /// Using literals as `println!` args is inefficient
129 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
130 /// (i.e., just put the literal in the format string)
134 /// println!("{}", "foo");
136 /// use the literal without formatting:
140 #[clippy::version = "pre 1.29.0"]
143 "printing a literal with a format string"
146 declare_clippy_lint! {
148 /// This lint warns when you use `writeln!(buf, "")` to
151 /// ### Why is this bad?
152 /// You should use `writeln!(buf)`, which is simpler.
156 /// # use std::fmt::Write;
157 /// # let mut buf = String::new();
158 /// writeln!(buf, "");
163 /// # use std::fmt::Write;
164 /// # let mut buf = String::new();
167 #[clippy::version = "pre 1.29.0"]
168 pub WRITELN_EMPTY_STRING,
170 "using `writeln!(buf, \"\")` with an empty string"
173 declare_clippy_lint! {
175 /// This lint warns when you use `write!()` with a format
177 /// ends in a newline.
179 /// ### Why is this bad?
180 /// You should use `writeln!()` instead, which appends the
185 /// # use std::fmt::Write;
186 /// # let mut buf = String::new();
187 /// # let name = "World";
188 /// write!(buf, "Hello {}!\n", name);
193 /// # use std::fmt::Write;
194 /// # let mut buf = String::new();
195 /// # let name = "World";
196 /// writeln!(buf, "Hello {}!", name);
198 #[clippy::version = "pre 1.29.0"]
199 pub WRITE_WITH_NEWLINE,
201 "using `write!()` with a format string that ends in a single newline"
204 declare_clippy_lint! {
206 /// This lint warns about the use of literals as `write!`/`writeln!` args.
208 /// ### Why is this bad?
209 /// Using literals as `writeln!` args is inefficient
210 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
211 /// (i.e., just put the literal in the format string)
215 /// # use std::fmt::Write;
216 /// # let mut buf = String::new();
217 /// writeln!(buf, "{}", "foo");
222 /// # use std::fmt::Write;
223 /// # let mut buf = String::new();
224 /// writeln!(buf, "foo");
226 #[clippy::version = "pre 1.29.0"]
229 "writing a literal with a format string"
237 impl_lint_pass!(Write => [
239 PRINTLN_EMPTY_STRING,
245 WRITELN_EMPTY_STRING,
249 impl<'tcx> LateLintPass<'tcx> for Write {
250 fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
251 if is_debug_impl(cx, item) {
252 self.in_debug_impl = true;
256 fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
257 if is_debug_impl(cx, item) {
258 self.in_debug_impl = false;
262 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
263 let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
264 let Some(diag_name) = cx.tcx.get_diagnostic_name(macro_call.def_id) else { return };
265 let Some(name) = diag_name.as_str().strip_suffix("_macro") else { return };
267 let is_build_script = cx
272 .map_or(false, |crate_name| crate_name == "build_script_build");
275 sym::print_macro | sym::println_macro => {
276 if !is_build_script {
277 span_lint(cx, PRINT_STDOUT, macro_call.span, &format!("use of `{name}!`"));
280 sym::eprint_macro | sym::eprintln_macro => {
281 span_lint(cx, PRINT_STDERR, macro_call.span, &format!("use of `{name}!`"));
283 sym::write_macro | sym::writeln_macro => {},
287 let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn) else { return };
289 // ignore `writeln!(w)` and `write!(v, some_macro!())`
290 if format_args.format_string.span.from_expansion() {
295 sym::print_macro | sym::eprint_macro | sym::write_macro => {
296 check_newline(cx, &format_args, ¯o_call, name);
298 sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
299 check_empty_string(cx, &format_args, ¯o_call, name);
304 check_literal(cx, &format_args, name);
306 if !self.in_debug_impl {
307 for arg in &format_args.args {
308 if arg.format.r#trait == sym::Debug {
309 span_lint(cx, USE_DEBUG, arg.span, "use of `Debug`-based formatting");
315 fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
316 if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind
317 && let Some(trait_id) = trait_ref.trait_def_id()
319 cx.tcx.is_diagnostic_item(sym::Debug, trait_id)
325 fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
326 let format_string_parts = &format_args.format_string.parts;
327 let mut format_string_span = format_args.format_string.span;
329 let Some(last) = format_string_parts.last() else { return };
331 let count_vertical_whitespace = || {
334 .flat_map(|part| part.as_str().chars())
335 .filter(|ch| matches!(ch, '\r' | '\n'))
339 if last.as_str().ends_with('\n')
340 // ignore format strings with other internal vertical whitespace
341 && count_vertical_whitespace() == 1
343 // ignore trailing arguments: `print!("Issue\n{}", 1265);`
344 && format_string_parts.len() > format_args.args.len()
346 let lint = if name == "write" {
347 format_string_span = expand_past_previous_comma(cx, format_string_span);
358 &format!("using `{name}!()` with a format string that ends in a single newline"),
360 let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
361 let Some(format_snippet) = snippet_opt(cx, format_string_span) else { return };
363 if format_string_parts.len() == 1 && last.as_str() == "\n" {
364 // print!("\n"), write!(f, "\n")
366 diag.multipart_suggestion(
367 &format!("use `{name}ln!` instead"),
368 vec![(name_span, format!("{name}ln")), (format_string_span, String::new())],
369 Applicability::MachineApplicable,
371 } else if format_snippet.ends_with("\\n\"") {
372 // print!("...\n"), write!(f, "...\n")
374 let hi = format_string_span.hi();
375 let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1));
377 diag.multipart_suggestion(
378 &format!("use `{name}ln!` instead"),
379 vec![(name_span, format!("{name}ln")), (newline_span, String::new())],
380 Applicability::MachineApplicable,
388 fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
389 if let [part] = &format_args.format_string.parts[..]
390 && let mut span = format_args.format_string.span
391 && part.as_str() == "\n"
393 let lint = if name == "writeln" {
394 span = expand_past_previous_comma(cx, span);
405 &format!("empty string literal in `{name}!`"),
407 diag.span_suggestion(
409 "remove the empty string",
411 Applicability::MachineApplicable,
418 fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &str) {
419 let mut counts = HirIdMap::<usize>::default();
420 for param in format_args.params() {
421 *counts.entry(param.value.hir_id).or_default() += 1;
424 for arg in &format_args.args {
425 let value = arg.param.value;
427 if counts[&value.hir_id] == 1
428 && arg.format.is_default()
429 && let ExprKind::Lit(lit) = &value.kind
430 && !value.span.from_expansion()
431 && let Some(value_string) = snippet_opt(cx, value.span)
433 let (replacement, replace_raw) = match lit.node {
434 LitKind::Str(..) => extract_str_literal(&value_string),
435 LitKind::Char(ch) => (
439 _ => &value_string[1..value_string.len() - 1],
444 LitKind::Bool(b) => (b.to_string(), false),
448 let lint = if name.starts_with("write") {
454 let format_string_is_raw = format_args.format_string.style.is_some();
455 let replacement = match (format_string_is_raw, replace_raw) {
456 (false, false) => Some(replacement),
457 (false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")),
458 (true, false) => match conservative_unescape(&replacement) {
459 Ok(unescaped) => Some(unescaped),
460 Err(UnescapeErr::Lint) => None,
461 Err(UnescapeErr::Ignore) => continue,
464 if replacement.contains(['#', '"']) {
476 "literal with an empty format string",
478 if let Some(replacement) = replacement
479 // `format!("{}", "a")`, `format!("{named}", named = "b")
480 // ~~~~~ ~~~~~~~~~~~~~
481 && let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
483 let replacement = replacement.replace('{', "{{").replace('}', "}}");
484 diag.multipart_suggestion(
486 vec![(arg.span, replacement), (value_span, String::new())],
487 Applicability::MachineApplicable,
496 /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
498 /// `r#"a"#` -> (`a`, true)
500 /// `"b"` -> (`b`, false)
501 fn extract_str_literal(literal: &str) -> (String, bool) {
502 let (literal, raw) = match literal.strip_prefix('r') {
503 Some(stripped) => (stripped.trim_matches('#'), true),
504 None => (literal, false),
507 (literal[1..literal.len() - 1].to_string(), raw)
511 /// Should still be linted, can be manually resolved by author, e.g.
514 /// print!(r"{}", '"');
517 /// Should not be linted, e.g.
520 /// print!(r"{}", '\r');
525 /// Unescape a normal string into a raw string
526 fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
527 let mut unescaped = String::with_capacity(literal.len());
528 let mut chars = literal.chars();
531 while let Some(ch) = chars.next() {
534 '\\' => match chars.next() {
535 Some('\\') => unescaped.push('\\'),
536 Some('"') => err = true,
537 _ => return Err(UnescapeErr::Ignore),
539 _ => unescaped.push(ch),
543 if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }