-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add unnecessary_debug_formatting
lint
#13893
Merged
blyxyas
merged 1 commit into
rust-lang:master
from
smoelius:unnecessary-debug-formatting
Feb 26, 2025
+314
−14
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,27 @@ | ||
use arrayvec::ArrayVec; | ||
use clippy_config::Conf; | ||
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; | ||
use clippy_utils::is_diag_trait_item; | ||
use clippy_utils::macros::{ | ||
FormatArgsStorage, FormatParamUsage, MacroCall, find_format_arg_expr, format_arg_removal_span, | ||
format_placeholder_format_span, is_assert_macro, is_format_macro, is_panic, matching_root_macro_call, | ||
root_macro_call_first_node, | ||
}; | ||
use clippy_utils::msrvs::{self, Msrv}; | ||
use clippy_utils::source::SpanRangeExt; | ||
use clippy_utils::source::{SpanRangeExt, snippet}; | ||
use clippy_utils::ty::{implements_trait, is_type_lang_item}; | ||
use clippy_utils::{is_diag_trait_item, is_from_proc_macro}; | ||
use itertools::Itertools; | ||
use rustc_ast::{ | ||
FormatArgPosition, FormatArgPositionKind, FormatArgsPiece, FormatArgumentKind, FormatCount, FormatOptions, | ||
FormatPlaceholder, FormatTrait, | ||
}; | ||
use rustc_data_structures::fx::FxHashMap; | ||
use rustc_errors::Applicability; | ||
use rustc_errors::SuggestionStyle::{CompletelyHidden, ShowCode}; | ||
use rustc_hir::{Expr, ExprKind, LangItem}; | ||
use rustc_lint::{LateContext, LateLintPass, LintContext}; | ||
use rustc_middle::ty::Ty; | ||
use rustc_middle::ty::adjustment::{Adjust, Adjustment}; | ||
use rustc_middle::ty::{List, Ty, TyCtxt}; | ||
use rustc_session::impl_lint_pass; | ||
use rustc_span::edition::Edition::Edition2021; | ||
use rustc_span::{Span, Symbol, sym}; | ||
|
@@ -50,6 +51,36 @@ declare_clippy_lint! { | |
"`format!` used in a macro that does formatting" | ||
} | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// Checks for `Debug` formatting (`{:?}`) applied to an `OsStr` or `Path`. | ||
/// | ||
/// ### Why is this bad? | ||
/// Rust doesn't guarantee what `Debug` formatting looks like, and it could | ||
/// change in the future. `OsStr`s and `Path`s can be `Display` formatted | ||
/// using their `display` methods. | ||
/// | ||
/// Furthermore, with `Debug` formatting, certain characters are escaped. | ||
/// Thus, a `Debug` formatted `Path` is less likely to be clickable. | ||
/// | ||
/// ### Example | ||
/// ```no_run | ||
/// # use std::path::Path; | ||
/// let path = Path::new("..."); | ||
/// println!("The path is {:?}", path); | ||
/// ``` | ||
/// Use instead: | ||
/// ```no_run | ||
/// # use std::path::Path; | ||
/// let path = Path::new("…"); | ||
/// println!("The path is {}", path.display()); | ||
/// ``` | ||
#[clippy::version = "1.87.0"] | ||
pub UNNECESSARY_DEBUG_FORMATTING, | ||
pedantic, | ||
"`Debug` formatting applied to an `OsStr` or `Path` when `.display()` is available" | ||
} | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// Checks for [`ToString::to_string`](https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string) | ||
|
@@ -162,31 +193,35 @@ declare_clippy_lint! { | |
"use of a format specifier that has no effect" | ||
} | ||
|
||
impl_lint_pass!(FormatArgs => [ | ||
impl_lint_pass!(FormatArgs<'_> => [ | ||
FORMAT_IN_FORMAT_ARGS, | ||
TO_STRING_IN_FORMAT_ARGS, | ||
UNINLINED_FORMAT_ARGS, | ||
UNNECESSARY_DEBUG_FORMATTING, | ||
UNUSED_FORMAT_SPECS, | ||
]); | ||
|
||
#[allow(clippy::struct_field_names)] | ||
pub struct FormatArgs { | ||
pub struct FormatArgs<'tcx> { | ||
format_args: FormatArgsStorage, | ||
msrv: Msrv, | ||
ignore_mixed: bool, | ||
ty_feature_map: FxHashMap<Ty<'tcx>, Option<Symbol>>, | ||
} | ||
|
||
impl FormatArgs { | ||
pub fn new(conf: &'static Conf, format_args: FormatArgsStorage) -> Self { | ||
impl<'tcx> FormatArgs<'tcx> { | ||
pub fn new(tcx: TyCtxt<'tcx>, conf: &'static Conf, format_args: FormatArgsStorage) -> Self { | ||
let ty_feature_map = make_ty_feature_map(tcx); | ||
Self { | ||
format_args, | ||
msrv: conf.msrv.clone(), | ||
ignore_mixed: conf.allow_mixed_uninlined_format_args, | ||
ty_feature_map, | ||
} | ||
} | ||
} | ||
|
||
impl<'tcx> LateLintPass<'tcx> for FormatArgs { | ||
impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> { | ||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | ||
if let Some(macro_call) = root_macro_call_first_node(cx, expr) | ||
&& is_format_macro(cx, macro_call.def_id) | ||
|
@@ -198,6 +233,7 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs { | |
macro_call: ¯o_call, | ||
format_args, | ||
ignore_mixed: self.ignore_mixed, | ||
ty_feature_map: &self.ty_feature_map, | ||
}; | ||
|
||
linter.check_templates(); | ||
|
@@ -217,9 +253,10 @@ struct FormatArgsExpr<'a, 'tcx> { | |
macro_call: &'a MacroCall, | ||
format_args: &'a rustc_ast::FormatArgs, | ||
ignore_mixed: bool, | ||
ty_feature_map: &'a FxHashMap<Ty<'tcx>, Option<Symbol>>, | ||
} | ||
|
||
impl FormatArgsExpr<'_, '_> { | ||
impl<'tcx> FormatArgsExpr<'_, 'tcx> { | ||
fn check_templates(&self) { | ||
for piece in &self.format_args.template { | ||
if let FormatArgsPiece::Placeholder(placeholder) = piece | ||
|
@@ -237,6 +274,11 @@ impl FormatArgsExpr<'_, '_> { | |
self.check_format_in_format_args(name, arg_expr); | ||
self.check_to_string_in_format_args(name, arg_expr); | ||
} | ||
|
||
if placeholder.format_trait == FormatTrait::Debug { | ||
let name = self.cx.tcx.item_name(self.macro_call.def_id); | ||
self.check_unnecessary_debug_formatting(name, arg_expr); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -439,6 +481,33 @@ impl FormatArgsExpr<'_, '_> { | |
} | ||
} | ||
|
||
fn check_unnecessary_debug_formatting(&self, name: Symbol, value: &Expr<'tcx>) { | ||
let cx = self.cx; | ||
if !value.span.from_expansion() | ||
&& !is_from_proc_macro(cx, value) | ||
&& let ty = cx.typeck_results().expr_ty(value) | ||
&& self.can_display_format(ty) | ||
{ | ||
let snippet = snippet(cx.sess(), value.span, ".."); | ||
span_lint_and_then( | ||
cx, | ||
UNNECESSARY_DEBUG_FORMATTING, | ||
value.span, | ||
format!("unnecessary `Debug` formatting in `{name}!` args"), | ||
|diag| { | ||
diag.help(format!( | ||
"use `Display` formatting and change this to `{snippet}.display()`" | ||
)); | ||
diag.note( | ||
"switching to `Display` formatting will change how the value is shown; \ | ||
escaped characters will no longer be escaped and surrounding quotes will \ | ||
be removed", | ||
); | ||
}, | ||
); | ||
} | ||
} | ||
|
||
fn format_arg_positions(&self) -> impl Iterator<Item = (&FormatArgPosition, FormatParamUsage)> { | ||
self.format_args.template.iter().flat_map(|piece| match piece { | ||
FormatArgsPiece::Placeholder(placeholder) => { | ||
|
@@ -465,6 +534,41 @@ impl FormatArgsExpr<'_, '_> { | |
.at_most_one() | ||
.is_err() | ||
} | ||
|
||
fn can_display_format(&self, ty: Ty<'tcx>) -> bool { | ||
let ty = ty.peel_refs(); | ||
|
||
if let Some(feature) = self.ty_feature_map.get(&ty) | ||
&& feature.is_none_or(|feature| self.cx.tcx.features().enabled(feature)) | ||
{ | ||
return true; | ||
} | ||
|
||
// Even if `ty` is not in `self.ty_feature_map`, check whether `ty` implements `Deref` with | ||
// a `Target` that is in `self.ty_feature_map`. | ||
if let Some(deref_trait_id) = self.cx.tcx.lang_items().deref_trait() | ||
&& implements_trait(self.cx, ty, deref_trait_id, &[]) | ||
&& let Some(target_ty) = self.cx.get_associated_type(ty, deref_trait_id, "Target") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to check that |
||
&& let Some(feature) = self.ty_feature_map.get(&target_ty) | ||
&& feature.is_none_or(|feature| self.cx.tcx.features().enabled(feature)) | ||
{ | ||
return true; | ||
} | ||
|
||
false | ||
} | ||
} | ||
|
||
fn make_ty_feature_map(tcx: TyCtxt<'_>) -> FxHashMap<Ty<'_>, Option<Symbol>> { | ||
[(sym::OsStr, Some(Symbol::intern("os_str_display"))), (sym::Path, None)] | ||
.into_iter() | ||
.filter_map(|(name, feature)| { | ||
tcx.get_diagnostic_item(name).map(|def_id| { | ||
let ty = Ty::new_adt(tcx, tcx.adt_def(def_id), List::empty()); | ||
(ty, feature) | ||
}) | ||
}) | ||
.collect() | ||
} | ||
|
||
fn count_needed_derefs<'tcx, I>(mut ty: Ty<'tcx>, mut iter: I) -> (usize, Ty<'tcx>) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
#![feature(os_str_display)] | ||
#![warn(clippy::unnecessary_debug_formatting)] | ||
|
||
use std::ffi::{OsStr, OsString}; | ||
|
||
fn main() { | ||
let os_str = OsStr::new("abc"); | ||
let os_string = os_str.to_os_string(); | ||
|
||
// negative tests | ||
println!("{}", os_str.display()); | ||
println!("{}", os_string.display()); | ||
|
||
// positive tests | ||
println!("{:?}", os_str); //~ unnecessary_debug_formatting | ||
println!("{:?}", os_string); //~ unnecessary_debug_formatting | ||
|
||
println!("{os_str:?}"); //~ unnecessary_debug_formatting | ||
println!("{os_string:?}"); //~ unnecessary_debug_formatting | ||
|
||
let _: String = format!("{:?}", os_str); //~ unnecessary_debug_formatting | ||
let _: String = format!("{:?}", os_string); //~ unnecessary_debug_formatting | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
error: unnecessary `Debug` formatting in `println!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:15:22 | ||
| | ||
LL | println!("{:?}", os_str); | ||
| ^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_str.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
= note: `-D clippy::unnecessary-debug-formatting` implied by `-D warnings` | ||
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_debug_formatting)]` | ||
|
||
error: unnecessary `Debug` formatting in `println!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:16:22 | ||
| | ||
LL | println!("{:?}", os_string); | ||
| ^^^^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_string.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
|
||
error: unnecessary `Debug` formatting in `println!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:18:16 | ||
| | ||
LL | println!("{os_str:?}"); | ||
| ^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_str.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
|
||
error: unnecessary `Debug` formatting in `println!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:19:16 | ||
| | ||
LL | println!("{os_string:?}"); | ||
| ^^^^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_string.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
|
||
error: unnecessary `Debug` formatting in `format!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:21:37 | ||
| | ||
LL | let _: String = format!("{:?}", os_str); | ||
| ^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_str.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
|
||
error: unnecessary `Debug` formatting in `format!` args | ||
--> tests/ui/unnecessary_os_str_debug_formatting.rs:22:37 | ||
| | ||
LL | let _: String = format!("{:?}", os_string); | ||
| ^^^^^^^^^ | ||
| | ||
= help: use `Display` formatting and change this to `os_string.display()` | ||
= note: switching to `Display` formatting will change how the value is shown; escaped characters will no longer be escaped and surrounding quotes will be removed | ||
|
||
error: aborting due to 6 previous errors | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this also check for procedural macros? Usually new lints want to make sure we aren't linting in those cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frankly, I'm not sure, though I don't think it would hurt.
You mention "new lints" but are there larger guidelines for when
is_from_proc_macro
should be used?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, everywhere. There are not many lints that we want to lint in procedural macros. Proc macros are outside of the user's control and could produce any arbitrary output based on any arbitrary input.
For example, a while back we had an issue that a
clap
procedural macro producedlet _ = _
, Clippy warned about that and the user was confused because there was nothing they could do apart from reporting it to either Clap or Clippy. I'm not sure if there's a case that we want to explicitly lint in procedural macros.