Skip to content

Commit da2cd64

Browse files
committed
Add recursive_format_trait_impl lint
The to_string_in_display lint is renamed to recursive_format_trait_impl A check is added for the use of self formatted with Display or Debug inside any format string in the same impl The to_string_in_display check is kept as is - like in the format_in_format_args lint For now only Display and Debug are checked This could also be extended to other Format traits (Binary, etc.)
1 parent fea103d commit da2cd64

13 files changed

+606
-210
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,7 @@ Released 2020-11-19
12671267
* [`manual_strip`] [#6038](https://github.com/rust-lang/rust-clippy/pull/6038)
12681268
* [`map_err_ignore`] [#5998](https://github.com/rust-lang/rust-clippy/pull/5998)
12691269
* [`rc_buffer`] [#6044](https://github.com/rust-lang/rust-clippy/pull/6044)
1270-
* [`to_string_in_display`] [#5831](https://github.com/rust-lang/rust-clippy/pull/5831)
1270+
* `to_string_in_display` [#5831](https://github.com/rust-lang/rust-clippy/pull/5831)
12711271
* `single_char_push_str` [#5881](https://github.com/rust-lang/rust-clippy/pull/5881)
12721272

12731273
### Moves and Deprecations
@@ -1310,7 +1310,7 @@ Released 2020-11-19
13101310
[#5949](https://github.com/rust-lang/rust-clippy/pull/5949)
13111311
* [`doc_markdown`]: allow using "GraphQL" without backticks
13121312
[#5996](https://github.com/rust-lang/rust-clippy/pull/5996)
1313-
* [`to_string_in_display`]: avoid linting when calling `to_string()` on anything that is not `self`
1313+
* `to_string_in_display`: avoid linting when calling `to_string()` on anything that is not `self`
13141314
[#5971](https://github.com/rust-lang/rust-clippy/pull/5971)
13151315
* [`indexing_slicing`] and [`out_of_bounds_indexing`] treat references to arrays as arrays
13161316
[#6034](https://github.com/rust-lang/rust-clippy/pull/6034)
@@ -3209,6 +3209,7 @@ Released 2018-09-13
32093209
[`range_zip_with_len`]: https://rust-lang.github.io/rust-clippy/master/index.html#range_zip_with_len
32103210
[`rc_buffer`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer
32113211
[`rc_mutex`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex
3212+
[`recursive_format_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#recursive_format_trait_impl
32123213
[`redundant_allocation`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_allocation
32133214
[`redundant_clone`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone
32143215
[`redundant_closure`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure
@@ -3283,7 +3284,6 @@ Released 2018-09-13
32833284
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments
32843285
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
32853286
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
3286-
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display
32873287
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
32883288
[`todo`]: https://rust-lang.github.io/rust-clippy/master/index.html#todo
32893289
[`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
237237
LintId::of(ranges::MANUAL_RANGE_CONTAINS),
238238
LintId::of(ranges::RANGE_ZIP_WITH_LEN),
239239
LintId::of(ranges::REVERSED_EMPTY_RANGES),
240+
LintId::of(recursive_format_trait_impl::RECURSIVE_FORMAT_TRAIT_IMPL),
240241
LintId::of(redundant_clone::REDUNDANT_CLONE),
241242
LintId::of(redundant_closure_call::REDUNDANT_CLOSURE_CALL),
242243
LintId::of(redundant_field_names::REDUNDANT_FIELD_NAMES),
@@ -265,7 +266,6 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
265266
LintId::of(tabs_in_doc_comments::TABS_IN_DOC_COMMENTS),
266267
LintId::of(temporary_assignment::TEMPORARY_ASSIGNMENT),
267268
LintId::of(to_digit_is_some::TO_DIGIT_IS_SOME),
268-
LintId::of(to_string_in_display::TO_STRING_IN_DISPLAY),
269269
LintId::of(transmute::CROSSPOINTER_TRANSMUTE),
270270
LintId::of(transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS),
271271
LintId::of(transmute::TRANSMUTE_BYTES_TO_STR),

clippy_lints/src/lib.register_correctness.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ store.register_group(true, "clippy::correctness", Some("clippy_correctness"), ve
5252
LintId::of(ptr::INVALID_NULL_PTR_USAGE),
5353
LintId::of(ptr::MUT_FROM_REF),
5454
LintId::of(ranges::REVERSED_EMPTY_RANGES),
55+
LintId::of(recursive_format_trait_impl::RECURSIVE_FORMAT_TRAIT_IMPL),
5556
LintId::of(regex::INVALID_REGEX),
5657
LintId::of(self_assignment::SELF_ASSIGNMENT),
5758
LintId::of(serde_api::SERDE_API_MISUSE),
5859
LintId::of(size_of_in_element_count::SIZE_OF_IN_ELEMENT_COUNT),
5960
LintId::of(swap::ALMOST_SWAPPED),
60-
LintId::of(to_string_in_display::TO_STRING_IN_DISPLAY),
6161
LintId::of(transmute::UNSOUND_COLLECTION_TRANSMUTE),
6262
LintId::of(transmute::WRONG_TRANSMUTE),
6363
LintId::of(transmuting_null::TRANSMUTING_NULL),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ store.register_lints(&[
410410
ranges::RANGE_PLUS_ONE,
411411
ranges::RANGE_ZIP_WITH_LEN,
412412
ranges::REVERSED_EMPTY_RANGES,
413+
recursive_format_trait_impl::RECURSIVE_FORMAT_TRAIT_IMPL,
413414
redundant_clone::REDUNDANT_CLONE,
414415
redundant_closure_call::REDUNDANT_CLOSURE_CALL,
415416
redundant_else::REDUNDANT_ELSE,
@@ -454,7 +455,6 @@ store.register_lints(&[
454455
tabs_in_doc_comments::TABS_IN_DOC_COMMENTS,
455456
temporary_assignment::TEMPORARY_ASSIGNMENT,
456457
to_digit_is_some::TO_DIGIT_IS_SOME,
457-
to_string_in_display::TO_STRING_IN_DISPLAY,
458458
trailing_empty_array::TRAILING_EMPTY_ARRAY,
459459
trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS,
460460
trait_bounds::TYPE_REPETITION_IN_BOUNDS,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ mod ptr_eq;
329329
mod ptr_offset_with_cast;
330330
mod question_mark;
331331
mod ranges;
332+
mod recursive_format_trait_impl;
332333
mod redundant_clone;
333334
mod redundant_closure_call;
334335
mod redundant_else;
@@ -360,7 +361,6 @@ mod swap;
360361
mod tabs_in_doc_comments;
361362
mod temporary_assignment;
362363
mod to_digit_is_some;
363-
mod to_string_in_display;
364364
mod trailing_empty_array;
365365
mod trait_bounds;
366366
mod transmute;
@@ -703,7 +703,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
703703
store.register_early_pass(|| Box::new(reference::DerefAddrOf));
704704
store.register_early_pass(|| Box::new(reference::RefInDeref));
705705
store.register_early_pass(|| Box::new(double_parens::DoubleParens));
706-
store.register_late_pass(|| Box::new(to_string_in_display::ToStringInDisplay::new()));
706+
store.register_late_pass(|| Box::new(recursive_format_trait_impl::RecursiveFormatTraitImpl::new()));
707707
store.register_early_pass(|| Box::new(unsafe_removed_from_name::UnsafeNameRemoval));
708708
store.register_early_pass(|| Box::new(else_if_without_else::ElseIfWithoutElse));
709709
store.register_early_pass(|| Box::new(int_plus_one::IntPlusOne));
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::higher::{FormatArgsArg, FormatArgsExpn};
3+
use clippy_utils::{is_diag_trait_item, match_def_path, path_to_local_id, paths};
4+
use if_chain::if_chain;
5+
use rustc_hir::{Expr, ExprKind, HirId, Impl, ImplItem, ImplItemKind, Item, ItemKind, UnOp};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::{declare_tool_lint, impl_lint_pass};
8+
use rustc_span::{sym, ExpnData, ExpnKind, Symbol};
9+
10+
const FORMAT_MACRO_PATHS: &[&[&str]] = &[
11+
&paths::FORMAT_ARGS_MACRO,
12+
&paths::ASSERT_EQ_MACRO,
13+
&paths::ASSERT_MACRO,
14+
&paths::ASSERT_NE_MACRO,
15+
&paths::EPRINT_MACRO,
16+
&paths::EPRINTLN_MACRO,
17+
&paths::PRINT_MACRO,
18+
&paths::PRINTLN_MACRO,
19+
&paths::WRITE_MACRO,
20+
&paths::WRITELN_MACRO,
21+
];
22+
23+
#[derive(Clone, Copy)]
24+
enum ImplTrait {
25+
Debug,
26+
Display,
27+
}
28+
29+
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[sym::format_macro, sym::std_panic_macro];
30+
31+
fn outermost_expn_data(expn_data: ExpnData) -> ExpnData {
32+
if expn_data.call_site.from_expansion() {
33+
outermost_expn_data(expn_data.call_site.ctxt().outer_expn_data())
34+
} else {
35+
expn_data
36+
}
37+
}
38+
39+
declare_clippy_lint! {
40+
/// ### What it does
41+
/// Checks for recursive use of `Display` or `Debug` traits inside their implementation.
42+
///
43+
/// ### Why is this bad?
44+
/// This is unconditional recursion and so will lead to infinite
45+
/// recursion and a stack overflow.
46+
///
47+
/// ### Example
48+
///
49+
/// ```rust
50+
/// use std::fmt;
51+
///
52+
/// struct Structure(i32);
53+
/// impl fmt::Display for Structure {
54+
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55+
/// write!(f, "{}", self.to_string())
56+
/// }
57+
/// }
58+
///
59+
/// ```
60+
/// Use instead:
61+
/// ```rust
62+
/// use std::fmt;
63+
///
64+
/// struct Structure(i32);
65+
/// impl fmt::Display for Structure {
66+
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67+
/// write!(f, "{}", self.0)
68+
/// }
69+
/// }
70+
/// ```
71+
#[clippy::version = "1.48.0"]
72+
pub RECURSIVE_FORMAT_TRAIT_IMPL,
73+
correctness,
74+
"Format trait method called while implementing the same Format trait"
75+
}
76+
77+
#[derive(Default)]
78+
pub struct RecursiveFormatTraitImpl {
79+
// Whether we are inside Display or Debug trait impl - None for neither
80+
format_trait_impl: Option<ImplTrait>,
81+
// hir_id of self parameter of method inside Display Impl - i.e. fmt(&self)
82+
self_hir_id: Option<HirId>,
83+
}
84+
85+
impl RecursiveFormatTraitImpl {
86+
pub fn new() -> Self {
87+
Self {
88+
format_trait_impl: None,
89+
self_hir_id: None,
90+
}
91+
}
92+
}
93+
94+
impl_lint_pass!(RecursiveFormatTraitImpl => [RECURSIVE_FORMAT_TRAIT_IMPL]);
95+
96+
impl LateLintPass<'_> for RecursiveFormatTraitImpl {
97+
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
98+
if let Some(format_trait_impl) = is_format_trait_impl(cx, item) {
99+
self.format_trait_impl = Some(format_trait_impl);
100+
}
101+
}
102+
103+
fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
104+
// Assume no nested Impl of Debug and Display within eachother
105+
if is_format_trait_impl(cx, item).is_some() {
106+
self.format_trait_impl = None;
107+
self.self_hir_id = None;
108+
}
109+
}
110+
111+
fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
112+
if_chain! {
113+
// If we are in Display or Debug impl, then get hir_id for self in method impl - i.e. fmt(&self)
114+
if self.format_trait_impl.is_some();
115+
if let ImplItemKind::Fn(.., body_id) = &impl_item.kind;
116+
let body = cx.tcx.hir().body(*body_id);
117+
if !body.params.is_empty();
118+
then {
119+
let self_param = &body.params[0];
120+
self.self_hir_id = Some(self_param.pat.hir_id);
121+
}
122+
}
123+
}
124+
125+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
126+
if let Some(self_hir_id) = self.self_hir_id {
127+
match self.format_trait_impl {
128+
Some(ImplTrait::Display) => {
129+
check_to_string_in_display(cx, expr, self_hir_id);
130+
check_self_in_format_args(cx, expr, self_hir_id, ImplTrait::Display);
131+
},
132+
Some(ImplTrait::Debug) => {
133+
check_self_in_format_args(cx, expr, self_hir_id, ImplTrait::Debug);
134+
},
135+
None => {},
136+
}
137+
}
138+
}
139+
}
140+
141+
fn check_to_string_in_display(cx: &LateContext<'_>, expr: &Expr<'_>, self_hir_id: HirId) {
142+
if_chain! {
143+
// Get the hir_id of the object we are calling the method on
144+
if let ExprKind::MethodCall(path, _, [ref self_arg, ..], _) = expr.kind;
145+
// Is the method to_string() ?
146+
if path.ident.name == sym!(to_string);
147+
// Is the method a part of the ToString trait? (i.e. not to_string() implemented
148+
// separately)
149+
if let Some(expr_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
150+
if is_diag_trait_item(cx, expr_def_id, sym::ToString);
151+
// Is the method is called on self
152+
if path_to_local_id(self_arg, self_hir_id);
153+
then {
154+
span_lint(
155+
cx,
156+
RECURSIVE_FORMAT_TRAIT_IMPL,
157+
expr.span,
158+
"using `to_string` in `fmt::Display` implementation might lead to infinite recursion",
159+
);
160+
}
161+
}
162+
}
163+
164+
fn check_self_in_format_args(cx: &LateContext<'_>, expr: &Expr<'_>, self_hir_id: HirId, impl_trait: ImplTrait) {
165+
// Check each arg in format calls - do we ever use Display on self (directly or via deref)?
166+
if_chain! {
167+
if let Some(format_args) = FormatArgsExpn::parse(expr);
168+
let expr_expn_data = expr.span.ctxt().outer_expn_data();
169+
let outermost_expn_data = outermost_expn_data(expr_expn_data);
170+
if let Some(macro_def_id) = outermost_expn_data.macro_def_id;
171+
if FORMAT_MACRO_PATHS
172+
.iter()
173+
.any(|path| match_def_path(cx, macro_def_id, path))
174+
|| FORMAT_MACRO_DIAG_ITEMS
175+
.iter()
176+
.any(|diag_item| cx.tcx.is_diagnostic_item(*diag_item, macro_def_id));
177+
if let ExpnKind::Macro(_, _name) = outermost_expn_data.kind;
178+
if let Some(args) = format_args.args();
179+
then {
180+
for (_i, arg) in args.iter().enumerate() {
181+
match impl_trait {
182+
// In Display, we only care about Display (it is okay to use Debug)
183+
ImplTrait::Display => {
184+
if !arg.is_display() {
185+
continue;
186+
}},
187+
// In Debug, we only care about Debug (it is okay to use Display and ToString)
188+
ImplTrait::Debug => {
189+
if !arg.is_debug() {
190+
continue;
191+
}},
192+
193+
};
194+
check_format_arg_self(cx, expr, self_hir_id, arg, impl_trait);
195+
}
196+
}
197+
}
198+
}
199+
200+
fn check_format_arg_self(
201+
cx: &LateContext<'_>,
202+
expr: &Expr<'_>,
203+
self_hir_id: HirId,
204+
arg: &FormatArgsArg<'_>,
205+
impl_trait: ImplTrait,
206+
) {
207+
// Handle multiple dereferencing of references e.g. &&self
208+
// Handle single dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl)
209+
// Since the argument to fmt is itself a reference: &self
210+
let reference = single_deref(deref_expr(arg.value));
211+
if path_to_local_id(reference, self_hir_id) {
212+
match impl_trait {
213+
ImplTrait::Display => {
214+
span_lint(
215+
cx,
216+
RECURSIVE_FORMAT_TRAIT_IMPL,
217+
expr.span,
218+
"using `self` as Display in `fmt::Display` implementation might lead to infinite recursion",
219+
);
220+
},
221+
ImplTrait::Debug => {
222+
span_lint(
223+
cx,
224+
RECURSIVE_FORMAT_TRAIT_IMPL,
225+
expr.span,
226+
"using `self` as Debug in `fmt::Debug` implementation might lead to infinite recursion",
227+
);
228+
},
229+
}
230+
}
231+
}
232+
233+
fn deref_expr<'a, 'b>(expr: &'a Expr<'b>) -> &'a Expr<'b> {
234+
if let ExprKind::AddrOf(_, _, reference) = expr.kind {
235+
deref_expr(reference)
236+
} else {
237+
expr
238+
}
239+
}
240+
241+
fn single_deref<'a, 'b>(expr: &'a Expr<'b>) -> &'a Expr<'b> {
242+
if let ExprKind::Unary(UnOp::Deref, ex) = expr.kind {
243+
ex
244+
} else {
245+
expr
246+
}
247+
}
248+
249+
fn is_format_trait_impl(cx: &LateContext<'_>, item: &'hir Item<'_>) -> Option<ImplTrait> {
250+
if_chain! {
251+
// Are we at an Impl?
252+
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind;
253+
if let Some(did) = trait_ref.trait_def_id();
254+
then {
255+
// Is it for Display trait?
256+
if match_def_path(cx, did, &paths::DISPLAY_TRAIT) {
257+
Some(ImplTrait::Display)
258+
}
259+
// Is it for Debug trait?
260+
else if match_def_path(cx, did, &paths::DEBUG_TRAIT) {
261+
Some(ImplTrait::Debug)
262+
} else {
263+
None
264+
}
265+
} else {
266+
None
267+
}
268+
}
269+
}

0 commit comments

Comments
 (0)