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