]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/clippy_lints/src/write.rs
New upstream version 1.68.2+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / write.rs
1 use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2 use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn, MacroCall};
3 use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
4 use clippy_utils::{is_in_cfg_test, is_in_test_function};
5 use rustc_ast::LitKind;
6 use rustc_errors::Applicability;
7 use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
8 use rustc_lint::{LateContext, LateLintPass, LintContext};
9 use rustc_session::{declare_tool_lint, impl_lint_pass};
10 use rustc_span::{sym, BytePos};
11
12 declare_clippy_lint! {
13 /// ### What it does
14 /// This lint warns when you use `println!("")` to
15 /// print a newline.
16 ///
17 /// ### Why is this bad?
18 /// You should use `println!()`, which is simpler.
19 ///
20 /// ### Example
21 /// ```rust
22 /// println!("");
23 /// ```
24 ///
25 /// Use instead:
26 /// ```rust
27 /// println!();
28 /// ```
29 #[clippy::version = "pre 1.29.0"]
30 pub PRINTLN_EMPTY_STRING,
31 style,
32 "using `println!(\"\")` with an empty string"
33 }
34
35 declare_clippy_lint! {
36 /// ### What it does
37 /// This lint warns when you use `print!()` with a format
38 /// string that ends in a newline.
39 ///
40 /// ### Why is this bad?
41 /// You should use `println!()` instead, which appends the
42 /// newline.
43 ///
44 /// ### Example
45 /// ```rust
46 /// # let name = "World";
47 /// print!("Hello {}!\n", name);
48 /// ```
49 /// use println!() instead
50 /// ```rust
51 /// # let name = "World";
52 /// println!("Hello {}!", name);
53 /// ```
54 #[clippy::version = "pre 1.29.0"]
55 pub PRINT_WITH_NEWLINE,
56 style,
57 "using `print!()` with a format string that ends in a single newline"
58 }
59
60 declare_clippy_lint! {
61 /// ### What it does
62 /// Checks for printing on *stdout*. The purpose of this lint
63 /// is to catch debugging remnants.
64 ///
65 /// ### Why is this bad?
66 /// People often print on *stdout* while debugging an
67 /// application and might forget to remove those prints afterward.
68 ///
69 /// ### Known problems
70 /// Only catches `print!` and `println!` calls.
71 ///
72 /// ### Example
73 /// ```rust
74 /// println!("Hello world!");
75 /// ```
76 #[clippy::version = "pre 1.29.0"]
77 pub PRINT_STDOUT,
78 restriction,
79 "printing on stdout"
80 }
81
82 declare_clippy_lint! {
83 /// ### What it does
84 /// Checks for printing on *stderr*. The purpose of this lint
85 /// is to catch debugging remnants.
86 ///
87 /// ### Why is this bad?
88 /// People often print on *stderr* while debugging an
89 /// application and might forget to remove those prints afterward.
90 ///
91 /// ### Known problems
92 /// Only catches `eprint!` and `eprintln!` calls.
93 ///
94 /// ### Example
95 /// ```rust
96 /// eprintln!("Hello world!");
97 /// ```
98 #[clippy::version = "1.50.0"]
99 pub PRINT_STDERR,
100 restriction,
101 "printing on stderr"
102 }
103
104 declare_clippy_lint! {
105 /// ### What it does
106 /// Checks for use of `Debug` formatting. The purpose of this
107 /// lint is to catch debugging remnants.
108 ///
109 /// ### Why is this bad?
110 /// The purpose of the `Debug` trait is to facilitate
111 /// debugging Rust code. It should not be used in user-facing output.
112 ///
113 /// ### Example
114 /// ```rust
115 /// # let foo = "bar";
116 /// println!("{:?}", foo);
117 /// ```
118 #[clippy::version = "pre 1.29.0"]
119 pub USE_DEBUG,
120 restriction,
121 "use of `Debug`-based formatting"
122 }
123
124 declare_clippy_lint! {
125 /// ### What it does
126 /// This lint warns about the use of literals as `print!`/`println!` args.
127 ///
128 /// ### Why is this bad?
129 /// Using literals as `println!` args is inefficient
130 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
131 /// (i.e., just put the literal in the format string)
132 ///
133 /// ### Example
134 /// ```rust
135 /// println!("{}", "foo");
136 /// ```
137 /// use the literal without formatting:
138 /// ```rust
139 /// println!("foo");
140 /// ```
141 #[clippy::version = "pre 1.29.0"]
142 pub PRINT_LITERAL,
143 style,
144 "printing a literal with a format string"
145 }
146
147 declare_clippy_lint! {
148 /// ### What it does
149 /// This lint warns when you use `writeln!(buf, "")` to
150 /// print a newline.
151 ///
152 /// ### Why is this bad?
153 /// You should use `writeln!(buf)`, which is simpler.
154 ///
155 /// ### Example
156 /// ```rust
157 /// # use std::fmt::Write;
158 /// # let mut buf = String::new();
159 /// writeln!(buf, "");
160 /// ```
161 ///
162 /// Use instead:
163 /// ```rust
164 /// # use std::fmt::Write;
165 /// # let mut buf = String::new();
166 /// writeln!(buf);
167 /// ```
168 #[clippy::version = "pre 1.29.0"]
169 pub WRITELN_EMPTY_STRING,
170 style,
171 "using `writeln!(buf, \"\")` with an empty string"
172 }
173
174 declare_clippy_lint! {
175 /// ### What it does
176 /// This lint warns when you use `write!()` with a format
177 /// string that
178 /// ends in a newline.
179 ///
180 /// ### Why is this bad?
181 /// You should use `writeln!()` instead, which appends the
182 /// newline.
183 ///
184 /// ### Example
185 /// ```rust
186 /// # use std::fmt::Write;
187 /// # let mut buf = String::new();
188 /// # let name = "World";
189 /// write!(buf, "Hello {}!\n", name);
190 /// ```
191 ///
192 /// Use instead:
193 /// ```rust
194 /// # use std::fmt::Write;
195 /// # let mut buf = String::new();
196 /// # let name = "World";
197 /// writeln!(buf, "Hello {}!", name);
198 /// ```
199 #[clippy::version = "pre 1.29.0"]
200 pub WRITE_WITH_NEWLINE,
201 style,
202 "using `write!()` with a format string that ends in a single newline"
203 }
204
205 declare_clippy_lint! {
206 /// ### What it does
207 /// This lint warns about the use of literals as `write!`/`writeln!` args.
208 ///
209 /// ### Why is this bad?
210 /// Using literals as `writeln!` args is inefficient
211 /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
212 /// (i.e., just put the literal in the format string)
213 ///
214 /// ### Example
215 /// ```rust
216 /// # use std::fmt::Write;
217 /// # let mut buf = String::new();
218 /// writeln!(buf, "{}", "foo");
219 /// ```
220 ///
221 /// Use instead:
222 /// ```rust
223 /// # use std::fmt::Write;
224 /// # let mut buf = String::new();
225 /// writeln!(buf, "foo");
226 /// ```
227 #[clippy::version = "pre 1.29.0"]
228 pub WRITE_LITERAL,
229 style,
230 "writing a literal with a format string"
231 }
232
233 #[derive(Default)]
234 pub struct Write {
235 in_debug_impl: bool,
236 allow_print_in_tests: bool,
237 }
238
239 impl Write {
240 pub fn new(allow_print_in_tests: bool) -> Self {
241 Self {
242 allow_print_in_tests,
243 ..Default::default()
244 }
245 }
246 }
247
248 impl_lint_pass!(Write => [
249 PRINT_WITH_NEWLINE,
250 PRINTLN_EMPTY_STRING,
251 PRINT_STDOUT,
252 PRINT_STDERR,
253 USE_DEBUG,
254 PRINT_LITERAL,
255 WRITE_WITH_NEWLINE,
256 WRITELN_EMPTY_STRING,
257 WRITE_LITERAL,
258 ]);
259
260 impl<'tcx> LateLintPass<'tcx> for Write {
261 fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
262 if is_debug_impl(cx, item) {
263 self.in_debug_impl = true;
264 }
265 }
266
267 fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
268 if is_debug_impl(cx, item) {
269 self.in_debug_impl = false;
270 }
271 }
272
273 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
274 let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
275 let Some(diag_name) = cx.tcx.get_diagnostic_name(macro_call.def_id) else { return };
276 let Some(name) = diag_name.as_str().strip_suffix("_macro") else { return };
277
278 let is_build_script = cx
279 .sess()
280 .opts
281 .crate_name
282 .as_ref()
283 .map_or(false, |crate_name| crate_name == "build_script_build");
284
285 let allowed_in_tests = self.allow_print_in_tests
286 && (is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id));
287 match diag_name {
288 sym::print_macro | sym::println_macro if !allowed_in_tests => {
289 if !is_build_script {
290 span_lint(cx, PRINT_STDOUT, macro_call.span, &format!("use of `{name}!`"));
291 }
292 },
293 sym::eprint_macro | sym::eprintln_macro if !allowed_in_tests => {
294 span_lint(cx, PRINT_STDERR, macro_call.span, &format!("use of `{name}!`"));
295 },
296 sym::write_macro | sym::writeln_macro => {},
297 _ => return,
298 }
299
300 let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn) else { return };
301
302 // ignore `writeln!(w)` and `write!(v, some_macro!())`
303 if format_args.format_string.span.from_expansion() {
304 return;
305 }
306
307 match diag_name {
308 sym::print_macro | sym::eprint_macro | sym::write_macro => {
309 check_newline(cx, &format_args, &macro_call, name);
310 },
311 sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
312 check_empty_string(cx, &format_args, &macro_call, name);
313 },
314 _ => {},
315 }
316
317 check_literal(cx, &format_args, name);
318
319 if !self.in_debug_impl {
320 for arg in &format_args.args {
321 if arg.format.r#trait == sym::Debug {
322 span_lint(cx, USE_DEBUG, arg.span, "use of `Debug`-based formatting");
323 }
324 }
325 }
326 }
327 }
328 fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
329 if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind
330 && let Some(trait_id) = trait_ref.trait_def_id()
331 {
332 cx.tcx.is_diagnostic_item(sym::Debug, trait_id)
333 } else {
334 false
335 }
336 }
337
338 fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
339 let format_string_parts = &format_args.format_string.parts;
340 let mut format_string_span = format_args.format_string.span;
341
342 let Some(last) = format_string_parts.last() else { return };
343
344 let count_vertical_whitespace = || {
345 format_string_parts
346 .iter()
347 .flat_map(|part| part.as_str().chars())
348 .filter(|ch| matches!(ch, '\r' | '\n'))
349 .count()
350 };
351
352 if last.as_str().ends_with('\n')
353 // ignore format strings with other internal vertical whitespace
354 && count_vertical_whitespace() == 1
355
356 // ignore trailing arguments: `print!("Issue\n{}", 1265);`
357 && format_string_parts.len() > format_args.args.len()
358 {
359 let lint = if name == "write" {
360 format_string_span = expand_past_previous_comma(cx, format_string_span);
361
362 WRITE_WITH_NEWLINE
363 } else {
364 PRINT_WITH_NEWLINE
365 };
366
367 span_lint_and_then(
368 cx,
369 lint,
370 macro_call.span,
371 &format!("using `{name}!()` with a format string that ends in a single newline"),
372 |diag| {
373 let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
374 let Some(format_snippet) = snippet_opt(cx, format_string_span) else { return };
375
376 if format_string_parts.len() == 1 && last.as_str() == "\n" {
377 // print!("\n"), write!(f, "\n")
378
379 diag.multipart_suggestion(
380 format!("use `{name}ln!` instead"),
381 vec![(name_span, format!("{name}ln")), (format_string_span, String::new())],
382 Applicability::MachineApplicable,
383 );
384 } else if format_snippet.ends_with("\\n\"") {
385 // print!("...\n"), write!(f, "...\n")
386
387 let hi = format_string_span.hi();
388 let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1));
389
390 diag.multipart_suggestion(
391 format!("use `{name}ln!` instead"),
392 vec![(name_span, format!("{name}ln")), (newline_span, String::new())],
393 Applicability::MachineApplicable,
394 );
395 }
396 },
397 );
398 }
399 }
400
401 fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
402 if let [part] = &format_args.format_string.parts[..]
403 && let mut span = format_args.format_string.span
404 && part.as_str() == "\n"
405 {
406 let lint = if name == "writeln" {
407 span = expand_past_previous_comma(cx, span);
408
409 WRITELN_EMPTY_STRING
410 } else {
411 PRINTLN_EMPTY_STRING
412 };
413
414 span_lint_and_then(
415 cx,
416 lint,
417 macro_call.span,
418 &format!("empty string literal in `{name}!`"),
419 |diag| {
420 diag.span_suggestion(
421 span,
422 "remove the empty string",
423 String::new(),
424 Applicability::MachineApplicable,
425 );
426 },
427 );
428 }
429 }
430
431 fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &str) {
432 let mut counts = HirIdMap::<usize>::default();
433 for param in format_args.params() {
434 *counts.entry(param.value.hir_id).or_default() += 1;
435 }
436
437 for arg in &format_args.args {
438 let value = arg.param.value;
439
440 if counts[&value.hir_id] == 1
441 && arg.format.is_default()
442 && let ExprKind::Lit(lit) = &value.kind
443 && !value.span.from_expansion()
444 && let Some(value_string) = snippet_opt(cx, value.span)
445 {
446 let (replacement, replace_raw) = match lit.node {
447 LitKind::Str(..) => extract_str_literal(&value_string),
448 LitKind::Char(ch) => (
449 match ch {
450 '"' => "\\\"",
451 '\'' => "'",
452 _ => &value_string[1..value_string.len() - 1],
453 }
454 .to_string(),
455 false,
456 ),
457 LitKind::Bool(b) => (b.to_string(), false),
458 _ => continue,
459 };
460
461 let lint = if name.starts_with("write") {
462 WRITE_LITERAL
463 } else {
464 PRINT_LITERAL
465 };
466
467 let format_string_is_raw = format_args.format_string.style.is_some();
468 let replacement = match (format_string_is_raw, replace_raw) {
469 (false, false) => Some(replacement),
470 (false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")),
471 (true, false) => match conservative_unescape(&replacement) {
472 Ok(unescaped) => Some(unescaped),
473 Err(UnescapeErr::Lint) => None,
474 Err(UnescapeErr::Ignore) => continue,
475 },
476 (true, true) => {
477 if replacement.contains(['#', '"']) {
478 None
479 } else {
480 Some(replacement)
481 }
482 },
483 };
484
485 span_lint_and_then(
486 cx,
487 lint,
488 value.span,
489 "literal with an empty format string",
490 |diag| {
491 if let Some(replacement) = replacement
492 // `format!("{}", "a")`, `format!("{named}", named = "b")
493 // ~~~~~ ~~~~~~~~~~~~~
494 && let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
495 {
496 let replacement = replacement.replace('{', "{{").replace('}', "}}");
497 diag.multipart_suggestion(
498 "try this",
499 vec![(arg.span, replacement), (value_span, String::new())],
500 Applicability::MachineApplicable,
501 );
502 }
503 },
504 );
505 }
506 }
507 }
508
509 /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
510 ///
511 /// `r#"a"#` -> (`a`, true)
512 ///
513 /// `"b"` -> (`b`, false)
514 fn extract_str_literal(literal: &str) -> (String, bool) {
515 let (literal, raw) = match literal.strip_prefix('r') {
516 Some(stripped) => (stripped.trim_matches('#'), true),
517 None => (literal, false),
518 };
519
520 (literal[1..literal.len() - 1].to_string(), raw)
521 }
522
523 enum UnescapeErr {
524 /// Should still be linted, can be manually resolved by author, e.g.
525 ///
526 /// ```ignore
527 /// print!(r"{}", '"');
528 /// ```
529 Lint,
530 /// Should not be linted, e.g.
531 ///
532 /// ```ignore
533 /// print!(r"{}", '\r');
534 /// ```
535 Ignore,
536 }
537
538 /// Unescape a normal string into a raw string
539 fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
540 let mut unescaped = String::with_capacity(literal.len());
541 let mut chars = literal.chars();
542 let mut err = false;
543
544 while let Some(ch) = chars.next() {
545 match ch {
546 '#' => err = true,
547 '\\' => match chars.next() {
548 Some('\\') => unescaped.push('\\'),
549 Some('"') => err = true,
550 _ => return Err(UnescapeErr::Ignore),
551 },
552 _ => unescaped.push(ch),
553 }
554 }
555
556 if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
557 }