]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/write.rs
Auto merge of #6601 - mdm:fix-unit-args-false-positive, r=camsteffen
[rust.git] / clippy_lints / src / write.rs
1 use std::borrow::Cow;
2 use std::ops::Range;
3
4 use crate::utils::{snippet_with_applicability, span_lint, span_lint_and_sugg, span_lint_and_then};
5 use if_chain::if_chain;
6 use rustc_ast::ast::{Expr, ExprKind, ImplKind, Item, ItemKind, LitKind, MacCall, StrLit, StrStyle};
7 use rustc_ast::token;
8 use rustc_ast::tokenstream::TokenStream;
9 use rustc_errors::Applicability;
10 use rustc_lexer::unescape::{self, EscapeError};
11 use rustc_lint::{EarlyContext, EarlyLintPass};
12 use rustc_parse::parser;
13 use rustc_session::{declare_tool_lint, impl_lint_pass};
14 use rustc_span::symbol::kw;
15 use rustc_span::{sym, BytePos, Span};
16
17 declare_clippy_lint! {
18     /// **What it does:** This lint warns when you use `println!("")` to
19     /// print a newline.
20     ///
21     /// **Why is this bad?** You should use `println!()`, which is simpler.
22     ///
23     /// **Known problems:** None.
24     ///
25     /// **Example:**
26     /// ```rust
27     /// // Bad
28     /// println!("");
29     ///
30     /// // Good
31     /// println!();
32     /// ```
33     pub PRINTLN_EMPTY_STRING,
34     style,
35     "using `println!(\"\")` with an empty string"
36 }
37
38 declare_clippy_lint! {
39     /// **What it does:** This lint warns when you use `print!()` with a format
40     /// string that ends in a newline.
41     ///
42     /// **Why is this bad?** You should use `println!()` instead, which appends the
43     /// newline.
44     ///
45     /// **Known problems:** None.
46     ///
47     /// **Example:**
48     /// ```rust
49     /// # let name = "World";
50     /// print!("Hello {}!\n", name);
51     /// ```
52     /// use println!() instead
53     /// ```rust
54     /// # let name = "World";
55     /// println!("Hello {}!", name);
56     /// ```
57     pub PRINT_WITH_NEWLINE,
58     style,
59     "using `print!()` with a format string that ends in a single newline"
60 }
61
62 declare_clippy_lint! {
63     /// **What it does:** Checks for printing on *stdout*. The purpose of this lint
64     /// is to catch debugging remnants.
65     ///
66     /// **Why is this bad?** People often print on *stdout* while debugging an
67     /// application and might forget to remove those prints afterward.
68     ///
69     /// **Known problems:** Only catches `print!` and `println!` calls.
70     ///
71     /// **Example:**
72     /// ```rust
73     /// println!("Hello world!");
74     /// ```
75     pub PRINT_STDOUT,
76     restriction,
77     "printing on stdout"
78 }
79
80 declare_clippy_lint! {
81     /// **What it does:** Checks for printing on *stderr*. The purpose of this lint
82     /// is to catch debugging remnants.
83     ///
84     /// **Why is this bad?** People often print on *stderr* while debugging an
85     /// application and might forget to remove those prints afterward.
86     ///
87     /// **Known problems:** Only catches `eprint!` and `eprintln!` calls.
88     ///
89     /// **Example:**
90     /// ```rust
91     /// eprintln!("Hello world!");
92     /// ```
93     pub PRINT_STDERR,
94     restriction,
95     "printing on stderr"
96 }
97
98 declare_clippy_lint! {
99     /// **What it does:** Checks for use of `Debug` formatting. The purpose of this
100     /// lint is to catch debugging remnants.
101     ///
102     /// **Why is this bad?** The purpose of the `Debug` trait is to facilitate
103     /// debugging Rust code. It should not be used in user-facing output.
104     ///
105     /// **Example:**
106     /// ```rust
107     /// # let foo = "bar";
108     /// println!("{:?}", foo);
109     /// ```
110     pub USE_DEBUG,
111     restriction,
112     "use of `Debug`-based formatting"
113 }
114
115 declare_clippy_lint! {
116     /// **What it does:** This lint warns about the use of literals as `print!`/`println!` args.
117     ///
118     /// **Why is this bad?** Using literals as `println!` args is inefficient
119     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
120     /// (i.e., just put the literal in the format string)
121     ///
122     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
123     /// -- e.g., `println!("{}", env!("FOO"))`.
124     ///
125     /// **Example:**
126     /// ```rust
127     /// println!("{}", "foo");
128     /// ```
129     /// use the literal without formatting:
130     /// ```rust
131     /// println!("foo");
132     /// ```
133     pub PRINT_LITERAL,
134     style,
135     "printing a literal with a format string"
136 }
137
138 declare_clippy_lint! {
139     /// **What it does:** This lint warns when you use `writeln!(buf, "")` to
140     /// print a newline.
141     ///
142     /// **Why is this bad?** You should use `writeln!(buf)`, which is simpler.
143     ///
144     /// **Known problems:** None.
145     ///
146     /// **Example:**
147     /// ```rust
148     /// # use std::fmt::Write;
149     /// # let mut buf = String::new();
150     /// // Bad
151     /// writeln!(buf, "");
152     ///
153     /// // Good
154     /// writeln!(buf);
155     /// ```
156     pub WRITELN_EMPTY_STRING,
157     style,
158     "using `writeln!(buf, \"\")` with an empty string"
159 }
160
161 declare_clippy_lint! {
162     /// **What it does:** This lint warns when you use `write!()` with a format
163     /// string that
164     /// ends in a newline.
165     ///
166     /// **Why is this bad?** You should use `writeln!()` instead, which appends the
167     /// newline.
168     ///
169     /// **Known problems:** None.
170     ///
171     /// **Example:**
172     /// ```rust
173     /// # use std::fmt::Write;
174     /// # let mut buf = String::new();
175     /// # let name = "World";
176     /// // Bad
177     /// write!(buf, "Hello {}!\n", name);
178     ///
179     /// // Good
180     /// writeln!(buf, "Hello {}!", name);
181     /// ```
182     pub WRITE_WITH_NEWLINE,
183     style,
184     "using `write!()` with a format string that ends in a single newline"
185 }
186
187 declare_clippy_lint! {
188     /// **What it does:** This lint warns about the use of literals as `write!`/`writeln!` args.
189     ///
190     /// **Why is this bad?** Using literals as `writeln!` args is inefficient
191     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
192     /// (i.e., just put the literal in the format string)
193     ///
194     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
195     /// -- e.g., `writeln!(buf, "{}", env!("FOO"))`.
196     ///
197     /// **Example:**
198     /// ```rust
199     /// # use std::fmt::Write;
200     /// # let mut buf = String::new();
201     /// // Bad
202     /// writeln!(buf, "{}", "foo");
203     ///
204     /// // Good
205     /// writeln!(buf, "foo");
206     /// ```
207     pub WRITE_LITERAL,
208     style,
209     "writing a literal with a format string"
210 }
211
212 #[derive(Default)]
213 pub struct Write {
214     in_debug_impl: bool,
215 }
216
217 impl_lint_pass!(Write => [
218     PRINT_WITH_NEWLINE,
219     PRINTLN_EMPTY_STRING,
220     PRINT_STDOUT,
221     PRINT_STDERR,
222     USE_DEBUG,
223     PRINT_LITERAL,
224     WRITE_WITH_NEWLINE,
225     WRITELN_EMPTY_STRING,
226     WRITE_LITERAL
227 ]);
228
229 impl EarlyLintPass for Write {
230     fn check_item(&mut self, _: &EarlyContext<'_>, item: &Item) {
231         if let ItemKind::Impl(box ImplKind {
232             of_trait: Some(trait_ref),
233             ..
234         }) = &item.kind
235         {
236             let trait_name = trait_ref
237                 .path
238                 .segments
239                 .iter()
240                 .last()
241                 .expect("path has at least one segment")
242                 .ident
243                 .name;
244             if trait_name == sym::Debug {
245                 self.in_debug_impl = true;
246             }
247         }
248     }
249
250     fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) {
251         self.in_debug_impl = false;
252     }
253
254     fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
255         fn is_build_script(cx: &EarlyContext<'_>) -> bool {
256             // Cargo sets the crate name for build scripts to `build_script_build`
257             cx.sess
258                 .opts
259                 .crate_name
260                 .as_ref()
261                 .map_or(false, |crate_name| crate_name == "build_script_build")
262         }
263
264         if mac.path == sym!(print) {
265             if !is_build_script(cx) {
266                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`");
267             }
268             self.lint_print_with_newline(cx, mac);
269         } else if mac.path == sym!(println) {
270             if !is_build_script(cx) {
271                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`");
272             }
273             self.lint_println_empty_string(cx, mac);
274         } else if mac.path == sym!(eprint) {
275             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprint!`");
276             self.lint_print_with_newline(cx, mac);
277         } else if mac.path == sym!(eprintln) {
278             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprintln!`");
279             self.lint_println_empty_string(cx, mac);
280         } else if mac.path == sym!(write) {
281             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), true) {
282                 if check_newlines(&fmt_str) {
283                     span_lint_and_then(
284                         cx,
285                         WRITE_WITH_NEWLINE,
286                         mac.span(),
287                         "using `write!()` with a format string that ends in a single newline",
288                         |err| {
289                             err.multipart_suggestion(
290                                 "use `writeln!()` instead",
291                                 vec![
292                                     (mac.path.span, String::from("writeln")),
293                                     (newline_span(&fmt_str), String::new()),
294                                 ],
295                                 Applicability::MachineApplicable,
296                             );
297                         },
298                     )
299                 }
300             }
301         } else if mac.path == sym!(writeln) {
302             if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) {
303                 if fmt_str.symbol == kw::Empty {
304                     let mut applicability = Applicability::MachineApplicable;
305                     // FIXME: remove this `#[allow(...)]` once the issue #5822 gets fixed
306                     #[allow(clippy::option_if_let_else)]
307                     let suggestion = if let Some(e) = expr {
308                         snippet_with_applicability(cx, e.span, "v", &mut applicability)
309                     } else {
310                         applicability = Applicability::HasPlaceholders;
311                         Cow::Borrowed("v")
312                     };
313
314                     span_lint_and_sugg(
315                         cx,
316                         WRITELN_EMPTY_STRING,
317                         mac.span(),
318                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
319                         "replace it with",
320                         format!("writeln!({})", suggestion),
321                         applicability,
322                     );
323                 }
324             }
325         }
326     }
327 }
328
329 /// Given a format string that ends in a newline and its span, calculates the span of the
330 /// newline, or the format string itself if the format string consists solely of a newline.
331 fn newline_span(fmtstr: &StrLit) -> Span {
332     let sp = fmtstr.span;
333     let contents = &fmtstr.symbol.as_str();
334
335     if *contents == r"\n" {
336         return sp;
337     }
338
339     let newline_sp_hi = sp.hi()
340         - match fmtstr.style {
341             StrStyle::Cooked => BytePos(1),
342             StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
343         };
344
345     let newline_sp_len = if contents.ends_with('\n') {
346         BytePos(1)
347     } else if contents.ends_with(r"\n") {
348         BytePos(2)
349     } else {
350         panic!("expected format string to contain a newline");
351     };
352
353     sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
354 }
355
356 impl Write {
357     /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
358     /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
359     /// the contents of the string, whether it's a raw string, and the span of the literal in the
360     /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
361     /// `format_str` should be written to.
362     ///
363     /// Example:
364     ///
365     /// Calling this function on
366     /// ```rust
367     /// # use std::fmt::Write;
368     /// # let mut buf = String::new();
369     /// # let something = "something";
370     /// writeln!(buf, "string to write: {}", something);
371     /// ```
372     /// will return
373     /// ```rust,ignore
374     /// (Some("string to write: {}"), Some(buf))
375     /// ```
376     #[allow(clippy::too_many_lines)]
377     fn check_tts<'a>(&self, cx: &EarlyContext<'a>, tts: TokenStream, is_write: bool) -> (Option<StrLit>, Option<Expr>) {
378         use rustc_parse_format::{
379             AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec, ParseMode, Parser,
380             Piece,
381         };
382
383         let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
384         let mut expr: Option<Expr> = None;
385         if is_write {
386             expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
387                 Ok(p) => Some(p.into_inner()),
388                 Err(_) => return (None, None),
389             };
390             // might be `writeln!(foo)`
391             if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
392                 return (None, expr);
393             }
394         }
395
396         let fmtstr = match parser.parse_str_lit() {
397             Ok(fmtstr) => fmtstr,
398             Err(_) => return (None, expr),
399         };
400         let tmp = fmtstr.symbol.as_str();
401         let mut args = vec![];
402         let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
403         while let Some(piece) = fmt_parser.next() {
404             if !fmt_parser.errors.is_empty() {
405                 return (None, expr);
406             }
407             if let Piece::NextArgument(arg) = piece {
408                 if !self.in_debug_impl && arg.format.ty == "?" {
409                     // FIXME: modify rustc's fmt string parser to give us the current span
410                     span_lint(cx, USE_DEBUG, parser.prev_token.span, "use of `Debug`-based formatting");
411                 }
412                 args.push(arg);
413             }
414         }
415         let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
416         let mut idx = 0;
417         loop {
418             const SIMPLE: FormatSpec<'_> = FormatSpec {
419                 fill: None,
420                 align: AlignUnknown,
421                 flags: 0,
422                 precision: CountImplied,
423                 precision_span: None,
424                 width: CountImplied,
425                 width_span: None,
426                 ty: "",
427                 ty_span: None,
428             };
429             if !parser.eat(&token::Comma) {
430                 return (Some(fmtstr), expr);
431             }
432             let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
433                 expr
434             } else {
435                 return (Some(fmtstr), None);
436             };
437             match &token_expr.kind {
438                 ExprKind::Lit(lit) if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..)) => {
439                     let mut all_simple = true;
440                     let mut seen = false;
441                     for arg in &args {
442                         match arg.position {
443                             ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
444                                 if n == idx {
445                                     all_simple &= arg.format == SIMPLE;
446                                     seen = true;
447                                 }
448                             },
449                             ArgumentNamed(_) => {},
450                         }
451                     }
452                     if all_simple && seen {
453                         span_lint(cx, lint, token_expr.span, "literal with an empty format string");
454                     }
455                     idx += 1;
456                 },
457                 ExprKind::Assign(lhs, rhs, _) => {
458                     if_chain! {
459                         if let ExprKind::Lit(ref lit) = rhs.kind;
460                         if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..));
461                         if let ExprKind::Path(_, p) = &lhs.kind;
462                         then {
463                             let mut all_simple = true;
464                             let mut seen = false;
465                             for arg in &args {
466                                 match arg.position {
467                                     ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
468                                     ArgumentNamed(name) => {
469                                         if *p == name {
470                                             seen = true;
471                                             all_simple &= arg.format == SIMPLE;
472                                         }
473                                     },
474                                 }
475                             }
476                             if all_simple && seen {
477                                 span_lint(cx, lint, rhs.span, "literal with an empty format string");
478                             }
479                         }
480                     }
481                 },
482                 _ => idx += 1,
483             }
484         }
485     }
486
487     fn lint_println_empty_string(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
488         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
489             if fmt_str.symbol == kw::Empty {
490                 let name = mac.path.segments[0].ident.name;
491                 span_lint_and_sugg(
492                     cx,
493                     PRINTLN_EMPTY_STRING,
494                     mac.span(),
495                     &format!("using `{}!(\"\")`", name),
496                     "replace it with",
497                     format!("{}!()", name),
498                     Applicability::MachineApplicable,
499                 );
500             }
501         }
502     }
503
504     fn lint_print_with_newline(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
505         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
506             if check_newlines(&fmt_str) {
507                 let name = mac.path.segments[0].ident.name;
508                 let suggested = format!("{}ln", name);
509                 span_lint_and_then(
510                     cx,
511                     PRINT_WITH_NEWLINE,
512                     mac.span(),
513                     &format!("using `{}!()` with a format string that ends in a single newline", name),
514                     |err| {
515                         err.multipart_suggestion(
516                             &format!("use `{}!` instead", suggested),
517                             vec![(mac.path.span, suggested), (newline_span(&fmt_str), String::new())],
518                             Applicability::MachineApplicable,
519                         );
520                     },
521                 );
522             }
523         }
524     }
525 }
526
527 /// Checks if the format string contains a single newline that terminates it.
528 ///
529 /// Literal and escaped newlines are both checked (only literal for raw strings).
530 fn check_newlines(fmtstr: &StrLit) -> bool {
531     let mut has_internal_newline = false;
532     let mut last_was_cr = false;
533     let mut should_lint = false;
534
535     let contents = &fmtstr.symbol.as_str();
536
537     let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| {
538         let c = c.unwrap();
539
540         if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline {
541             should_lint = true;
542         } else {
543             last_was_cr = c == '\r';
544             if c == '\n' {
545                 has_internal_newline = true;
546             }
547         }
548     };
549
550     match fmtstr.style {
551         StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb),
552         StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb),
553     }
554
555     should_lint
556 }