use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; use clippy_utils::macros::{find_format_args, format_arg_removal_span, root_macro_call_first_node, MacroCall}; use clippy_utils::source::{expand_past_previous_comma, snippet_opt}; use clippy_utils::{is_in_cfg_test, is_in_test_function}; use rustc_ast::token::LitKind; use rustc_ast::{ FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder, FormatTrait, }; use rustc_errors::Applicability; use rustc_hir::{Expr, Impl, Item, ItemKind}; use rustc_lint::{LateContext, LateLintPass, LintContext}; use rustc_session::impl_lint_pass; use rustc_span::{sym, BytePos, Span}; declare_clippy_lint! { /// ### What it does /// This lint warns when you use `println!("")` to /// print a newline. /// /// ### Why is this bad? /// You should use `println!()`, which is simpler. /// /// ### Example /// ```no_run /// println!(""); /// ``` /// /// Use instead: /// ```no_run /// println!(); /// ``` #[clippy::version = "pre 1.29.0"] pub PRINTLN_EMPTY_STRING, style, "using `println!(\"\")` with an empty string" } declare_clippy_lint! { /// ### What it does /// This lint warns when you use `print!()` with a format /// string that ends in a newline. /// /// ### Why is this bad? /// You should use `println!()` instead, which appends the /// newline. /// /// ### Example /// ```no_run /// # let name = "World"; /// print!("Hello {}!\n", name); /// ``` /// use println!() instead /// ```no_run /// # let name = "World"; /// println!("Hello {}!", name); /// ``` #[clippy::version = "pre 1.29.0"] pub PRINT_WITH_NEWLINE, style, "using `print!()` with a format string that ends in a single newline" } declare_clippy_lint! { /// ### What it does /// Checks for printing on *stdout*. The purpose of this lint /// is to catch debugging remnants. /// /// ### Why is this bad? /// People often print on *stdout* while debugging an /// application and might forget to remove those prints afterward. /// /// ### Known problems /// Only catches `print!` and `println!` calls. /// /// ### Example /// ```no_run /// println!("Hello world!"); /// ``` #[clippy::version = "pre 1.29.0"] pub PRINT_STDOUT, restriction, "printing on stdout" } declare_clippy_lint! { /// ### What it does /// Checks for printing on *stderr*. The purpose of this lint /// is to catch debugging remnants. /// /// ### Why is this bad? /// People often print on *stderr* while debugging an /// application and might forget to remove those prints afterward. /// /// ### Known problems /// Only catches `eprint!` and `eprintln!` calls. /// /// ### Example /// ```no_run /// eprintln!("Hello world!"); /// ``` #[clippy::version = "1.50.0"] pub PRINT_STDERR, restriction, "printing on stderr" } declare_clippy_lint! { /// ### What it does /// Checks for usage of `Debug` formatting. The purpose of this /// lint is to catch debugging remnants. /// /// ### Why is this bad? /// The purpose of the `Debug` trait is to facilitate /// debugging Rust code. It should not be used in user-facing output. /// /// ### Example /// ```no_run /// # let foo = "bar"; /// println!("{:?}", foo); /// ``` #[clippy::version = "pre 1.29.0"] pub USE_DEBUG, restriction, "use of `Debug`-based formatting" } declare_clippy_lint! { /// ### What it does /// This lint warns about the use of literals as `print!`/`println!` args. /// /// ### Why is this bad? /// Using literals as `println!` args is inefficient /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary /// (i.e., just put the literal in the format string) /// /// ### Example /// ```no_run /// println!("{}", "foo"); /// ``` /// use the literal without formatting: /// ```no_run /// println!("foo"); /// ``` #[clippy::version = "pre 1.29.0"] pub PRINT_LITERAL, style, "printing a literal with a format string" } declare_clippy_lint! { /// ### What it does /// This lint warns when you use `writeln!(buf, "")` to /// print a newline. /// /// ### Why is this bad? /// You should use `writeln!(buf)`, which is simpler. /// /// ### Example /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// writeln!(buf, ""); /// ``` /// /// Use instead: /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// writeln!(buf); /// ``` #[clippy::version = "pre 1.29.0"] pub WRITELN_EMPTY_STRING, style, "using `writeln!(buf, \"\")` with an empty string" } declare_clippy_lint! { /// ### What it does /// This lint warns when you use `write!()` with a format /// string that /// ends in a newline. /// /// ### Why is this bad? /// You should use `writeln!()` instead, which appends the /// newline. /// /// ### Example /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// # let name = "World"; /// write!(buf, "Hello {}!\n", name); /// ``` /// /// Use instead: /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// # let name = "World"; /// writeln!(buf, "Hello {}!", name); /// ``` #[clippy::version = "pre 1.29.0"] pub WRITE_WITH_NEWLINE, style, "using `write!()` with a format string that ends in a single newline" } declare_clippy_lint! { /// ### What it does /// This lint warns about the use of literals as `write!`/`writeln!` args. /// /// ### Why is this bad? /// Using literals as `writeln!` args is inefficient /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary /// (i.e., just put the literal in the format string) /// /// ### Example /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// writeln!(buf, "{}", "foo"); /// ``` /// /// Use instead: /// ```no_run /// # use std::fmt::Write; /// # let mut buf = String::new(); /// writeln!(buf, "foo"); /// ``` #[clippy::version = "pre 1.29.0"] pub WRITE_LITERAL, style, "writing a literal with a format string" } #[derive(Default)] pub struct Write { in_debug_impl: bool, allow_print_in_tests: bool, } impl Write { pub fn new(allow_print_in_tests: bool) -> Self { Self { allow_print_in_tests, ..Default::default() } } } impl_lint_pass!(Write => [ PRINT_WITH_NEWLINE, PRINTLN_EMPTY_STRING, PRINT_STDOUT, PRINT_STDERR, USE_DEBUG, PRINT_LITERAL, WRITE_WITH_NEWLINE, WRITELN_EMPTY_STRING, WRITE_LITERAL, ]); impl<'tcx> LateLintPass<'tcx> for Write { fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { if is_debug_impl(cx, item) { self.in_debug_impl = true; } } fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) { if is_debug_impl(cx, item) { self.in_debug_impl = false; } } fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return; }; let Some(diag_name) = cx.tcx.get_diagnostic_name(macro_call.def_id) else { return; }; let Some(name) = diag_name.as_str().strip_suffix("_macro") else { return; }; let is_build_script = cx .sess() .opts .crate_name .as_ref() .map_or(false, |crate_name| crate_name == "build_script_build"); let allowed_in_tests = self.allow_print_in_tests && (is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id)); match diag_name { sym::print_macro | sym::println_macro if !allowed_in_tests => { if !is_build_script { span_lint(cx, PRINT_STDOUT, macro_call.span, &format!("use of `{name}!`")); } }, sym::eprint_macro | sym::eprintln_macro if !allowed_in_tests => { span_lint(cx, PRINT_STDERR, macro_call.span, &format!("use of `{name}!`")); }, sym::write_macro | sym::writeln_macro => {}, _ => return, } if let Some(format_args) = find_format_args(cx, expr, macro_call.expn) { // ignore `writeln!(w)` and `write!(v, some_macro!())` if format_args.span.from_expansion() { return; } match diag_name { sym::print_macro | sym::eprint_macro | sym::write_macro => { check_newline(cx, &format_args, ¯o_call, name); }, sym::println_macro | sym::eprintln_macro | sym::writeln_macro => { check_empty_string(cx, &format_args, ¯o_call, name); }, _ => {}, } check_literal(cx, &format_args, name); if !self.in_debug_impl { for piece in &format_args.template { if let &FormatArgsPiece::Placeholder(FormatPlaceholder { span: Some(span), format_trait: FormatTrait::Debug, .. }) = piece { span_lint(cx, USE_DEBUG, span, "use of `Debug`-based formatting"); } } } } } } fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool { if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind && let Some(trait_id) = trait_ref.trait_def_id() { cx.tcx.is_diagnostic_item(sym::Debug, trait_id) } else { false } } fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) { let Some(FormatArgsPiece::Literal(last)) = format_args.template.last() else { return; }; let count_vertical_whitespace = || { format_args .template .iter() .filter_map(|piece| match piece { FormatArgsPiece::Literal(literal) => Some(literal), FormatArgsPiece::Placeholder(_) => None, }) .flat_map(|literal| literal.as_str().chars()) .filter(|ch| matches!(ch, '\r' | '\n')) .count() }; if last.as_str().ends_with('\n') // ignore format strings with other internal vertical whitespace && count_vertical_whitespace() == 1 { let mut format_string_span = format_args.span; let lint = if name == "write" { format_string_span = expand_past_previous_comma(cx, format_string_span); WRITE_WITH_NEWLINE } else { PRINT_WITH_NEWLINE }; span_lint_and_then( cx, lint, macro_call.span, &format!("using `{name}!()` with a format string that ends in a single newline"), |diag| { let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!'); let Some(format_snippet) = snippet_opt(cx, format_string_span) else { return; }; if format_args.template.len() == 1 && last.as_str() == "\n" { // print!("\n"), write!(f, "\n") diag.multipart_suggestion( format!("use `{name}ln!` instead"), vec![(name_span, format!("{name}ln")), (format_string_span, String::new())], Applicability::MachineApplicable, ); } else if format_snippet.ends_with("\\n\"") { // print!("...\n"), write!(f, "...\n") let hi = format_string_span.hi(); let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1)); diag.multipart_suggestion( format!("use `{name}ln!` instead"), vec![(name_span, format!("{name}ln")), (newline_span, String::new())], Applicability::MachineApplicable, ); } }, ); } } fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call: &MacroCall, name: &str) { if let [FormatArgsPiece::Literal(literal)] = &format_args.template[..] && literal.as_str() == "\n" { let mut span = format_args.span; let lint = if name == "writeln" { span = expand_past_previous_comma(cx, span); WRITELN_EMPTY_STRING } else { PRINTLN_EMPTY_STRING }; span_lint_and_then( cx, lint, macro_call.span, &format!("empty string literal in `{name}!`"), |diag| { diag.span_suggestion( span, "remove the empty string", String::new(), Applicability::MachineApplicable, ); }, ); } } fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) { let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos); let lint_name = if name.starts_with("write") { WRITE_LITERAL } else { PRINT_LITERAL }; let mut counts = vec![0u32; format_args.arguments.all_args().len()]; for piece in &format_args.template { if let FormatArgsPiece::Placeholder(placeholder) = piece { counts[arg_index(&placeholder.argument)] += 1; } } let mut suggestion: Vec<(Span, String)> = vec![]; // holds index of replaced positional arguments; used to decrement the index of the remaining // positional arguments. let mut replaced_position: Vec = vec![]; let mut sug_span: Option = None; for piece in &format_args.template { if let FormatArgsPiece::Placeholder(FormatPlaceholder { argument, span: Some(placeholder_span), format_trait: FormatTrait::Display, format_options, }) = piece && *format_options == FormatOptions::default() && let index = arg_index(argument) && counts[index] == 1 && let Some(arg) = format_args.arguments.by_index(index) && let rustc_ast::ExprKind::Lit(lit) = &arg.expr.kind && !arg.expr.span.from_expansion() && let Some(value_string) = snippet_opt(cx, arg.expr.span) { let (replacement, replace_raw) = match lit.kind { LitKind::Str | LitKind::StrRaw(_) => match extract_str_literal(&value_string) { Some(extracted) => extracted, None => return, }, LitKind::Char => ( match lit.symbol.as_str() { "\"" => "\\\"", "\\'" => "'", _ => match value_string.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) { Some(stripped) => stripped, None => return, }, } .to_string(), false, ), LitKind::Bool => (lit.symbol.to_string(), false), _ => continue, }; let Some(format_string_snippet) = snippet_opt(cx, format_args.span) else { continue; }; let format_string_is_raw = format_string_snippet.starts_with('r'); let replacement = match (format_string_is_raw, replace_raw) { (false, false) => Some(replacement), (false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")), (true, false) => match conservative_unescape(&replacement) { Ok(unescaped) => Some(unescaped), Err(UnescapeErr::Lint) => None, Err(UnescapeErr::Ignore) => continue, }, (true, true) => { if replacement.contains(['#', '"']) { None } else { Some(replacement) } }, }; sug_span = Some(sug_span.unwrap_or(arg.expr.span).to(arg.expr.span)); if let Some((_, index)) = positional_arg_piece_span(piece) { replaced_position.push(index); } if let Some(replacement) = replacement // `format!("{}", "a")`, `format!("{named}", named = "b") // ~~~~~ ~~~~~~~~~~~~~ && let Some(removal_span) = format_arg_removal_span(format_args, index) { let replacement = escape_braces(&replacement, !format_string_is_raw && !replace_raw); suggestion.push((*placeholder_span, replacement)); suggestion.push((removal_span, String::new())); } } } // Decrement the index of the remaining by the number of replaced positional arguments if !suggestion.is_empty() { for piece in &format_args.template { if let Some((span, index)) = positional_arg_piece_span(piece) && suggestion.iter().all(|(s, _)| *s != span) { let decrement = replaced_position.iter().filter(|i| **i < index).count(); suggestion.push((span, format!("{{{}}}", index.saturating_sub(decrement)))); } } } if let Some(span) = sug_span { span_lint_and_then(cx, lint_name, span, "literal with an empty format string", |diag| { if !suggestion.is_empty() { diag.multipart_suggestion("try", suggestion, Applicability::MachineApplicable); } }); } } /// Extract Span and its index from the given `piece`, iff it's positional argument. fn positional_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> { match piece { FormatArgsPiece::Placeholder(FormatPlaceholder { argument: FormatArgPosition { index: Ok(index), kind: FormatArgPositionKind::Number, .. }, span: Some(span), .. }) => Some((*span, *index)), _ => None, } } /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw /// /// `r#"a"#` -> (`a`, true) /// /// `"b"` -> (`b`, false) fn extract_str_literal(literal: &str) -> Option<(String, bool)> { let (literal, raw) = match literal.strip_prefix('r') { Some(stripped) => (stripped.trim_matches('#'), true), None => (literal, false), }; Some((literal.strip_prefix('"')?.strip_suffix('"')?.to_string(), raw)) } enum UnescapeErr { /// Should still be linted, can be manually resolved by author, e.g. /// /// ```ignore /// print!(r"{}", '"'); /// ``` Lint, /// Should not be linted, e.g. /// /// ```ignore /// print!(r"{}", '\r'); /// ``` Ignore, } /// Unescape a normal string into a raw string fn conservative_unescape(literal: &str) -> Result { let mut unescaped = String::with_capacity(literal.len()); let mut chars = literal.chars(); let mut err = false; while let Some(ch) = chars.next() { match ch { '#' => err = true, '\\' => match chars.next() { Some('\\') => unescaped.push('\\'), Some('"') => err = true, _ => return Err(UnescapeErr::Ignore), }, _ => unescaped.push(ch), } } if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) } } /// Replaces `{` with `{{` and `}` with `}}`. If `preserve_unicode_escapes` is `true` the braces in /// `\u{xxxx}` are left unmodified #[expect(clippy::match_same_arms)] fn escape_braces(literal: &str, preserve_unicode_escapes: bool) -> String { #[derive(Clone, Copy)] enum State { Normal, Backslash, UnicodeEscape, } let mut escaped = String::with_capacity(literal.len()); let mut state = State::Normal; for ch in literal.chars() { state = match (ch, state) { // Escape braces outside of unicode escapes by doubling them up ('{' | '}', State::Normal) => { escaped.push(ch); State::Normal }, // If `preserve_unicode_escapes` isn't enabled stay in `State::Normal`, otherwise: // // \u{aaaa} \\ \x01 // ^ ^ ^ ('\\', State::Normal) if preserve_unicode_escapes => State::Backslash, // \u{aaaa} // ^ ('u', State::Backslash) => State::UnicodeEscape, // \xAA \\ // ^ ^ (_, State::Backslash) => State::Normal, // \u{aaaa} // ^ ('}', State::UnicodeEscape) => State::Normal, _ => state, }; escaped.push(ch); } escaped }