Skip to content

Commit 7e796ef

Browse files
committed
Add print_in_format_impl lint
1 parent bcbb07f commit 7e796ef

File tree

7 files changed

+221
-48
lines changed

7 files changed

+221
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3370,6 +3370,7 @@ Released 2018-09-13
33703370
[`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
33713371
[`possible_missing_comma`]: https://rust-lang.github.io/rust-clippy/master/index.html#possible_missing_comma
33723372
[`precedence`]: https://rust-lang.github.io/rust-clippy/master/index.html#precedence
3373+
[`print_in_format_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#print_in_format_impl
33733374
[`print_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#print_literal
33743375
[`print_stderr`]: https://rust-lang.github.io/rust-clippy/master/index.html#print_stderr
33753376
[`print_stdout`]: https://rust-lang.github.io/rust-clippy/master/index.html#print_stdout

clippy_lints/src/format_impl.rs

Lines changed: 113 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,13 @@
1-
use clippy_utils::diagnostics::span_lint;
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
22
use clippy_utils::macros::{is_format_macro, root_macro_call_first_node, FormatArgsArg, FormatArgsExpn};
3-
use clippy_utils::{is_diag_trait_item, path_to_local, peel_ref_operators};
3+
use clippy_utils::{get_parent_as_impl, is_diag_trait_item, path_to_local, peel_ref_operators};
44
use if_chain::if_chain;
5-
use rustc_hir::{Expr, ExprKind, Impl, Item, ItemKind, QPath};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{Expr, ExprKind, Impl, ImplItem, ImplItemKind, QPath};
67
use rustc_lint::{LateContext, LateLintPass};
78
use rustc_session::{declare_tool_lint, impl_lint_pass};
89
use rustc_span::{sym, symbol::kw, Symbol};
910

10-
#[derive(Clone, Copy)]
11-
enum FormatTrait {
12-
Debug,
13-
Display,
14-
}
15-
16-
impl FormatTrait {
17-
fn name(self) -> Symbol {
18-
match self {
19-
FormatTrait::Debug => sym::Debug,
20-
FormatTrait::Display => sym::Display,
21-
}
22-
}
23-
}
2411
declare_clippy_lint! {
2512
/// ### What it does
2613
/// Checks for format trait implementations (e.g. `Display`) with a recursive call to itself
@@ -60,6 +47,56 @@ declare_clippy_lint! {
6047
"Format trait method called while implementing the same Format trait"
6148
}
6249

50+
declare_clippy_lint! {
51+
/// ### What it does
52+
/// Checks for use of `println`, `print`, `eprintln` or `eprint` in an
53+
/// implementation of a formatting trait.
54+
///
55+
/// ### Why is this bad?
56+
/// Printing during a formatting trait impl is likely unintentional. It can
57+
/// cause output to be split between the wrong streams. If `format!` is used
58+
/// text would go to stdout/stderr even if not desired.
59+
///
60+
/// ### Example
61+
/// ```rust
62+
/// use std::fmt::{Display, Error, Formatter};
63+
///
64+
/// struct S;
65+
/// impl Display for S {
66+
/// fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
67+
/// println!("S");
68+
///
69+
/// Ok(())
70+
/// }
71+
/// }
72+
/// ```
73+
/// Use instead:
74+
/// ```rust
75+
/// use std::fmt::{Display, Error, Formatter};
76+
///
77+
/// struct S;
78+
/// impl Display for S {
79+
/// fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
80+
/// writeln!(f, "S");
81+
///
82+
/// Ok(())
83+
/// }
84+
/// }
85+
/// ```
86+
#[clippy::version = "1.60.0"]
87+
pub PRINT_IN_FORMAT_IMPL,
88+
suspicious,
89+
"use of a print macro in a formatting trait impl"
90+
}
91+
92+
#[derive(Clone, Copy)]
93+
struct FormatTrait {
94+
/// e.g. `sym::Display`
95+
name: Symbol,
96+
/// `f` in `fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {}`
97+
formatter_name: Option<Symbol>,
98+
}
99+
63100
#[derive(Default)]
64101
pub struct FormatImpl {
65102
// Whether we are inside Display or Debug trait impl - None for neither
@@ -74,33 +111,29 @@ impl FormatImpl {
74111
}
75112
}
76113

77-
impl_lint_pass!(FormatImpl => [RECURSIVE_FORMAT_IMPL]);
114+
impl_lint_pass!(FormatImpl => [RECURSIVE_FORMAT_IMPL, PRINT_IN_FORMAT_IMPL]);
78115

79116
impl<'tcx> LateLintPass<'tcx> for FormatImpl {
80-
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
81-
if let Some(format_trait_impl) = is_format_trait_impl(cx, item) {
82-
self.format_trait_impl = Some(format_trait_impl);
83-
}
117+
fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
118+
self.format_trait_impl = is_format_trait_impl(cx, impl_item);
84119
}
85120

86-
fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
121+
fn check_impl_item_post(&mut self, cx: &LateContext<'_>, impl_item: &ImplItem<'_>) {
87122
// Assume no nested Impl of Debug and Display within eachother
88-
if is_format_trait_impl(cx, item).is_some() {
123+
if is_format_trait_impl(cx, impl_item).is_some() {
89124
self.format_trait_impl = None;
90125
}
91126
}
92127

93128
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
94-
match self.format_trait_impl {
95-
Some(FormatTrait::Display) => {
96-
check_to_string_in_display(cx, expr);
97-
check_self_in_format_args(cx, expr, FormatTrait::Display);
98-
},
99-
Some(FormatTrait::Debug) => {
100-
check_self_in_format_args(cx, expr, FormatTrait::Debug);
101-
},
102-
None => {},
129+
let Some(format_trait_impl) = self.format_trait_impl else { return };
130+
131+
if format_trait_impl.name == sym::Display {
132+
check_to_string_in_display(cx, expr);
103133
}
134+
135+
check_self_in_format_args(cx, expr, format_trait_impl);
136+
check_print_in_format_impl(cx, expr, format_trait_impl);
104137
}
105138
}
106139

@@ -139,7 +172,7 @@ fn check_self_in_format_args<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>,
139172
if let Some(args) = format_args.args();
140173
then {
141174
for arg in args {
142-
if arg.format_trait != impl_trait.name() {
175+
if arg.format_trait != impl_trait.name {
143176
continue;
144177
}
145178
check_format_arg_self(cx, expr, &arg, impl_trait);
@@ -155,33 +188,65 @@ fn check_format_arg_self(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &FormatArgs
155188
let reference = peel_ref_operators(cx, arg.value);
156189
let map = cx.tcx.hir();
157190
// Is the reference self?
158-
let symbol_ident = impl_trait.name().to_ident_string();
159191
if path_to_local(reference).map(|x| map.name(x)) == Some(kw::SelfLower) {
192+
let FormatTrait { name, .. } = impl_trait;
160193
span_lint(
161194
cx,
162195
RECURSIVE_FORMAT_IMPL,
163196
expr.span,
164-
&format!(
165-
"using `self` as `{}` in `impl {}` will cause infinite recursion",
166-
&symbol_ident, &symbol_ident
167-
),
197+
&format!("using `self` as `{name}` in `impl {name}` will cause infinite recursion"),
168198
);
169199
}
170200
}
171201

172-
fn is_format_trait_impl(cx: &LateContext<'_>, item: &Item<'_>) -> Option<FormatTrait> {
202+
fn check_print_in_format_impl(cx: &LateContext<'_>, expr: &Expr<'_>, impl_trait: FormatTrait) {
203+
if_chain! {
204+
if let Some(macro_call) = root_macro_call_first_node(cx, expr);
205+
if let Some(name) = cx.tcx.get_diagnostic_name(macro_call.def_id);
206+
then {
207+
let replacement = match name {
208+
sym::print_macro | sym::eprint_macro => "write",
209+
sym::println_macro | sym::eprintln_macro => "writeln",
210+
_ => return,
211+
};
212+
213+
let name = name.as_str().strip_suffix("_macro").unwrap();
214+
215+
span_lint_and_sugg(
216+
cx,
217+
PRINT_IN_FORMAT_IMPL,
218+
macro_call.span,
219+
&format!("use of `{}!` in `{}` impl", name, impl_trait.name),
220+
"replace with",
221+
if let Some(formatter_name) = impl_trait.formatter_name {
222+
format!("{}!({}, ..)", replacement, formatter_name)
223+
} else {
224+
format!("{}!(..)", replacement)
225+
},
226+
Applicability::HasPlaceholders,
227+
);
228+
}
229+
}
230+
}
231+
232+
fn is_format_trait_impl(cx: &LateContext<'_>, impl_item: &ImplItem<'_>) -> Option<FormatTrait> {
173233
if_chain! {
174-
// Are we at an Impl?
175-
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind;
234+
if impl_item.ident.name == sym::fmt;
235+
if let ImplItemKind::Fn(_, body_id) = impl_item.kind;
236+
if let Some(Impl { of_trait: Some(trait_ref),..}) = get_parent_as_impl(cx.tcx, impl_item.hir_id());
176237
if let Some(did) = trait_ref.trait_def_id();
177238
if let Some(name) = cx.tcx.get_diagnostic_name(did);
239+
if matches!(name, sym::Debug | sym::Display);
178240
then {
179-
// Is Impl for Debug or Display?
180-
match name {
181-
sym::Debug => Some(FormatTrait::Debug),
182-
sym::Display => Some(FormatTrait::Display),
183-
_ => None,
184-
}
241+
let body = cx.tcx.hir().body(body_id);
242+
let formatter_name = body.params.get(1)
243+
.and_then(|param| param.pat.simple_ident())
244+
.map(|ident| ident.name);
245+
246+
Some(FormatTrait {
247+
name,
248+
formatter_name,
249+
})
185250
} else {
186251
None
187252
}

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
6868
LintId::of(format::USELESS_FORMAT),
6969
LintId::of(format_args::FORMAT_IN_FORMAT_ARGS),
7070
LintId::of(format_args::TO_STRING_IN_FORMAT_ARGS),
71+
LintId::of(format_impl::PRINT_IN_FORMAT_IMPL),
7172
LintId::of(format_impl::RECURSIVE_FORMAT_IMPL),
7273
LintId::of(formatting::POSSIBLE_MISSING_COMMA),
7374
LintId::of(formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ store.register_lints(&[
152152
format::USELESS_FORMAT,
153153
format_args::FORMAT_IN_FORMAT_ARGS,
154154
format_args::TO_STRING_IN_FORMAT_ARGS,
155+
format_impl::PRINT_IN_FORMAT_IMPL,
155156
format_impl::RECURSIVE_FORMAT_IMPL,
156157
formatting::POSSIBLE_MISSING_COMMA,
157158
formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING,

clippy_lints/src/lib.register_suspicious.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec!
1010
LintId::of(casts::CAST_ENUM_TRUNCATION),
1111
LintId::of(eval_order_dependence::EVAL_ORDER_DEPENDENCE),
1212
LintId::of(float_equality_without_abs::FLOAT_EQUALITY_WITHOUT_ABS),
13+
LintId::of(format_impl::PRINT_IN_FORMAT_IMPL),
1314
LintId::of(formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING),
1415
LintId::of(formatting::SUSPICIOUS_ELSE_FORMATTING),
1516
LintId::of(formatting::SUSPICIOUS_UNARY_OP_FORMATTING),

tests/ui/print_in_format_impl.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#![allow(unused, clippy::print_literal, clippy::write_literal)]
2+
#![warn(clippy::print_in_format_impl)]
3+
use std::fmt::{Debug, Display, Error, Formatter};
4+
5+
macro_rules! indirect {
6+
() => {{ println!() }};
7+
}
8+
9+
macro_rules! nested {
10+
($($tt:tt)*) => {
11+
$($tt)*
12+
};
13+
}
14+
15+
struct Foo;
16+
impl Debug for Foo {
17+
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
18+
static WORKS_WITH_NESTED_ITEMS: bool = true;
19+
20+
print!("{}", 1);
21+
println!("{}", 2);
22+
eprint!("{}", 3);
23+
eprintln!("{}", 4);
24+
nested! {
25+
println!("nested");
26+
};
27+
28+
write!(f, "{}", 5);
29+
writeln!(f, "{}", 6);
30+
indirect!();
31+
32+
Ok(())
33+
}
34+
}
35+
36+
impl Display for Foo {
37+
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
38+
print!("Display");
39+
write!(f, "Display");
40+
41+
Ok(())
42+
}
43+
}
44+
45+
struct UnnamedFormatter;
46+
impl Debug for UnnamedFormatter {
47+
fn fmt(&self, _: &mut Formatter) -> Result<(), Error> {
48+
println!("UnnamedFormatter");
49+
Ok(())
50+
}
51+
}
52+
53+
fn main() {
54+
print!("outside fmt");
55+
println!("outside fmt");
56+
eprint!("outside fmt");
57+
eprintln!("outside fmt");
58+
}

tests/ui/print_in_format_impl.stderr

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
error: use of `print!` in `Debug` impl
2+
--> $DIR/print_in_format_impl.rs:20:9
3+
|
4+
LL | print!("{}", 1);
5+
| ^^^^^^^^^^^^^^^ help: replace with: `write!(f, ..)`
6+
|
7+
= note: `-D clippy::print-in-format-impl` implied by `-D warnings`
8+
9+
error: use of `println!` in `Debug` impl
10+
--> $DIR/print_in_format_impl.rs:21:9
11+
|
12+
LL | println!("{}", 2);
13+
| ^^^^^^^^^^^^^^^^^ help: replace with: `writeln!(f, ..)`
14+
15+
error: use of `eprint!` in `Debug` impl
16+
--> $DIR/print_in_format_impl.rs:22:9
17+
|
18+
LL | eprint!("{}", 3);
19+
| ^^^^^^^^^^^^^^^^ help: replace with: `write!(f, ..)`
20+
21+
error: use of `eprintln!` in `Debug` impl
22+
--> $DIR/print_in_format_impl.rs:23:9
23+
|
24+
LL | eprintln!("{}", 4);
25+
| ^^^^^^^^^^^^^^^^^^ help: replace with: `writeln!(f, ..)`
26+
27+
error: use of `println!` in `Debug` impl
28+
--> $DIR/print_in_format_impl.rs:25:13
29+
|
30+
LL | println!("nested");
31+
| ^^^^^^^^^^^^^^^^^^ help: replace with: `writeln!(f, ..)`
32+
33+
error: use of `print!` in `Display` impl
34+
--> $DIR/print_in_format_impl.rs:38:9
35+
|
36+
LL | print!("Display");
37+
| ^^^^^^^^^^^^^^^^^ help: replace with: `write!(f, ..)`
38+
39+
error: use of `println!` in `Debug` impl
40+
--> $DIR/print_in_format_impl.rs:48:9
41+
|
42+
LL | println!("UnnamedFormatter");
43+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `writeln!(..)`
44+
45+
error: aborting due to 7 previous errors
46+

0 commit comments

Comments
 (0)