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