Skip to content

Commit e88ec71

Browse files
committed
Add recursive_display_impl lint
The to_string_in_display lint is renamed to recursive_display_impl A check is added for the use of self formatted with Display inside any format string in the Display impl The to_string_in_display check is kept as is - like in the format_in_format_args lint
1 parent fea103d commit e88ec71

11 files changed

+477
-210
lines changed

CHANGELOG.md

+3-3
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_display_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#recursive_display_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

+1-1
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_display_impl::RECURSIVE_DISPLAY_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

+1-1
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_display_impl::RECURSIVE_DISPLAY_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

+1-1
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_display_impl::RECURSIVE_DISPLAY_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

+2-2
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_display_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_display_impl::RecursiveDisplayImpl::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));
+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[sym::format_macro, sym::std_panic_macro];
24+
25+
fn outermost_expn_data(expn_data: ExpnData) -> ExpnData {
26+
if expn_data.call_site.from_expansion() {
27+
outermost_expn_data(expn_data.call_site.ctxt().outer_expn_data())
28+
} else {
29+
expn_data
30+
}
31+
}
32+
33+
declare_clippy_lint! {
34+
/// ### What it does
35+
/// Checks for recursive use of `Display` trait inside its implementation.
36+
///
37+
/// ### Why is this bad?
38+
/// This is unconditional recursion and so will lead to infinite
39+
/// recursion and a stack overflow.
40+
///
41+
/// ### Example
42+
///
43+
/// ```rust
44+
/// use std::fmt;
45+
///
46+
/// struct Structure(i32);
47+
/// impl fmt::Display for Structure {
48+
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49+
/// write!(f, "{}", self.to_string())
50+
/// }
51+
/// }
52+
///
53+
/// ```
54+
/// Use instead:
55+
/// ```rust
56+
/// use std::fmt;
57+
///
58+
/// struct Structure(i32);
59+
/// impl fmt::Display for Structure {
60+
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61+
/// write!(f, "{}", self.0)
62+
/// }
63+
/// }
64+
/// ```
65+
#[clippy::version = "1.48.0"]
66+
pub RECURSIVE_DISPLAY_IMPL,
67+
correctness,
68+
"`Display` trait method called while implementing `Display` trait"
69+
}
70+
71+
#[derive(Default)]
72+
pub struct RecursiveDisplayImpl {
73+
in_display_impl: bool,
74+
// hir_id of self parameter of method inside Display Impl - i.e. fmt(&self)
75+
self_hir_id: Option<HirId>,
76+
}
77+
78+
impl RecursiveDisplayImpl {
79+
pub fn new() -> Self {
80+
Self {
81+
in_display_impl: false,
82+
self_hir_id: None,
83+
}
84+
}
85+
}
86+
87+
impl_lint_pass!(RecursiveDisplayImpl => [RECURSIVE_DISPLAY_IMPL]);
88+
89+
impl LateLintPass<'_> for RecursiveDisplayImpl {
90+
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
91+
if is_display_impl(cx, item) {
92+
self.in_display_impl = true;
93+
}
94+
}
95+
96+
fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
97+
if is_display_impl(cx, item) {
98+
self.in_display_impl = false;
99+
self.self_hir_id = None;
100+
}
101+
}
102+
103+
fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
104+
if_chain! {
105+
// If we are in Display impl, then get hir_id for self in method impl - i.e. fmt(&self)
106+
if self.in_display_impl;
107+
if let ImplItemKind::Fn(.., body_id) = &impl_item.kind;
108+
let body = cx.tcx.hir().body(*body_id);
109+
if !body.params.is_empty();
110+
then {
111+
let self_param = &body.params[0];
112+
self.self_hir_id = Some(self_param.pat.hir_id);
113+
}
114+
}
115+
}
116+
117+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
118+
if_chain! {
119+
if self.in_display_impl;
120+
if let Some(self_hir_id) = self.self_hir_id;
121+
then {
122+
check_to_string_in_display(cx, expr, self_hir_id);
123+
check_self_in_format_args(cx, expr, self_hir_id);
124+
}
125+
}
126+
}
127+
}
128+
129+
fn check_to_string_in_display(cx: &LateContext<'_>, expr: &Expr<'_>, self_hir_id: HirId) {
130+
if_chain! {
131+
// Get the hir_id of the object we are calling the method on
132+
if let ExprKind::MethodCall(path, _, [ref self_arg, ..], _) = expr.kind;
133+
// Is the method to_string() ?
134+
if path.ident.name == sym!(to_string);
135+
// Is the method a part of the ToString trait? (i.e. not to_string() implemented
136+
// separately)
137+
if let Some(expr_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
138+
if is_diag_trait_item(cx, expr_def_id, sym::ToString);
139+
// Is the method is called on self
140+
if path_to_local_id(self_arg, self_hir_id);
141+
then {
142+
span_lint(
143+
cx,
144+
RECURSIVE_DISPLAY_IMPL,
145+
expr.span,
146+
"using `to_string` in `fmt::Display` implementation might lead to infinite recursion",
147+
);
148+
}
149+
}
150+
}
151+
152+
fn check_self_in_format_args(cx: &LateContext<'_>, expr: &Expr<'_>, self_hir_id: HirId) {
153+
// Check each arg in format calls - do we ever use Display on self (directly or via deref)?
154+
if_chain! {
155+
if let Some(format_args) = FormatArgsExpn::parse(expr);
156+
let expr_expn_data = expr.span.ctxt().outer_expn_data();
157+
let outermost_expn_data = outermost_expn_data(expr_expn_data);
158+
if let Some(macro_def_id) = outermost_expn_data.macro_def_id;
159+
if FORMAT_MACRO_PATHS
160+
.iter()
161+
.any(|path| match_def_path(cx, macro_def_id, path))
162+
|| FORMAT_MACRO_DIAG_ITEMS
163+
.iter()
164+
.any(|diag_item| cx.tcx.is_diagnostic_item(*diag_item, macro_def_id));
165+
if let ExpnKind::Macro(_, _name) = outermost_expn_data.kind;
166+
if let Some(args) = format_args.args();
167+
then {
168+
for (_i, arg) in args.iter().enumerate() {
169+
// We only care about Display, okay to use Debug here
170+
if !arg.is_display() {
171+
continue;
172+
}
173+
check_format_arg_self(cx, expr, self_hir_id, arg);
174+
}
175+
}
176+
}
177+
}
178+
179+
fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, self_hir_id: HirId, arg: &FormatArgsArg<'_>) {
180+
// Handle multiple dereferencing of references e.g. &&self
181+
// Handle single dereference of &self -> self that is equivalent (i.e. via *self in fmt() impl)
182+
// Since the argument to fmt is itself a reference: &self
183+
let reference = single_deref(deref_expr(arg.value));
184+
if path_to_local_id(reference, self_hir_id) {
185+
span_lint(
186+
cx,
187+
RECURSIVE_DISPLAY_IMPL,
188+
expr.span,
189+
"using `self` in `fmt::Display` implementation might lead to infinite recursion",
190+
);
191+
}
192+
}
193+
194+
fn deref_expr<'a, 'b>(expr: &'a Expr<'b>) -> &'a Expr<'b> {
195+
if let ExprKind::AddrOf(_, _, reference) = expr.kind {
196+
deref_expr(reference)
197+
} else {
198+
expr
199+
}
200+
}
201+
202+
fn single_deref<'a, 'b>(expr: &'a Expr<'b>) -> &'a Expr<'b> {
203+
if let ExprKind::Unary(UnOp::Deref, ex) = expr.kind {
204+
ex
205+
} else {
206+
expr
207+
}
208+
}
209+
210+
fn is_display_impl(cx: &LateContext<'_>, item: &'hir Item<'_>) -> bool {
211+
if_chain! {
212+
// Are we at an Impl?
213+
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind;
214+
if let Some(did) = trait_ref.trait_def_id();
215+
then {
216+
// Is it for Display trait?
217+
match_def_path(cx, did, &paths::DISPLAY_TRAIT)
218+
} else {
219+
false
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)