]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/write.rs
Merge remote-tracking branch 'upstream/master' into rustup
[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, 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     ///
151     /// // Bad
152     /// writeln!(buf, "");
153     ///
154     /// // Good
155     /// writeln!(buf);
156     /// ```
157     pub WRITELN_EMPTY_STRING,
158     style,
159     "using `writeln!(buf, \"\")` with an empty string"
160 }
161
162 declare_clippy_lint! {
163     /// **What it does:** This lint warns when you use `write!()` with a format
164     /// string that
165     /// ends in a newline.
166     ///
167     /// **Why is this bad?** You should use `writeln!()` instead, which appends the
168     /// newline.
169     ///
170     /// **Known problems:** None.
171     ///
172     /// **Example:**
173     /// ```rust
174     /// # use std::fmt::Write;
175     /// # let mut buf = String::new();
176     /// # let name = "World";
177     ///
178     /// // Bad
179     /// write!(buf, "Hello {}!\n", name);
180     ///
181     /// // Good
182     /// writeln!(buf, "Hello {}!", name);
183     /// ```
184     pub WRITE_WITH_NEWLINE,
185     style,
186     "using `write!()` with a format string that ends in a single newline"
187 }
188
189 declare_clippy_lint! {
190     /// **What it does:** This lint warns about the use of literals as `write!`/`writeln!` args.
191     ///
192     /// **Why is this bad?** Using literals as `writeln!` args is inefficient
193     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
194     /// (i.e., just put the literal in the format string)
195     ///
196     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
197     /// -- e.g., `writeln!(buf, "{}", env!("FOO"))`.
198     ///
199     /// **Example:**
200     /// ```rust
201     /// # use std::fmt::Write;
202     /// # let mut buf = String::new();
203     ///
204     /// // Bad
205     /// writeln!(buf, "{}", "foo");
206     ///
207     /// // Good
208     /// writeln!(buf, "foo");
209     /// ```
210     pub WRITE_LITERAL,
211     style,
212     "writing a literal with a format string"
213 }
214
215 #[derive(Default)]
216 pub struct Write {
217     in_debug_impl: bool,
218 }
219
220 impl_lint_pass!(Write => [
221     PRINT_WITH_NEWLINE,
222     PRINTLN_EMPTY_STRING,
223     PRINT_STDOUT,
224     PRINT_STDERR,
225     USE_DEBUG,
226     PRINT_LITERAL,
227     WRITE_WITH_NEWLINE,
228     WRITELN_EMPTY_STRING,
229     WRITE_LITERAL
230 ]);
231
232 impl EarlyLintPass for Write {
233     fn check_item(&mut self, _: &EarlyContext<'_>, item: &Item) {
234         if let ItemKind::Impl {
235             of_trait: Some(trait_ref),
236             ..
237         } = &item.kind
238         {
239             let trait_name = trait_ref
240                 .path
241                 .segments
242                 .iter()
243                 .last()
244                 .expect("path has at least one segment")
245                 .ident
246                 .name;
247             if trait_name == sym::Debug {
248                 self.in_debug_impl = true;
249             }
250         }
251     }
252
253     fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) {
254         self.in_debug_impl = false;
255     }
256
257     fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
258         fn is_build_script(cx: &EarlyContext<'_>) -> bool {
259             // Cargo sets the crate name for build scripts to `build_script_build`
260             cx.sess
261                 .opts
262                 .crate_name
263                 .as_ref()
264                 .map_or(false, |crate_name| crate_name == "build_script_build")
265         }
266
267         if mac.path == sym!(print) {
268             if !is_build_script(cx) {
269                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`");
270             }
271             self.lint_print_with_newline(cx, mac);
272         } else if mac.path == sym!(println) {
273             if !is_build_script(cx) {
274                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`");
275             }
276             self.lint_println_empty_string(cx, mac);
277         } else if mac.path == sym!(eprint) {
278             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprint!`");
279             self.lint_print_with_newline(cx, mac);
280         } else if mac.path == sym!(eprintln) {
281             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprintln!`");
282             self.lint_println_empty_string(cx, mac);
283         } else if mac.path == sym!(write) {
284             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), true) {
285                 if check_newlines(&fmt_str) {
286                     span_lint_and_then(
287                         cx,
288                         WRITE_WITH_NEWLINE,
289                         mac.span(),
290                         "using `write!()` with a format string that ends in a single newline",
291                         |err| {
292                             err.multipart_suggestion(
293                                 "use `writeln!()` instead",
294                                 vec![
295                                     (mac.path.span, String::from("writeln")),
296                                     (newline_span(&fmt_str), String::new()),
297                                 ],
298                                 Applicability::MachineApplicable,
299                             );
300                         },
301                     )
302                 }
303             }
304         } else if mac.path == sym!(writeln) {
305             if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) {
306                 if fmt_str.symbol == kw::Empty {
307                     let mut applicability = Applicability::MachineApplicable;
308                     // FIXME: remove this `#[allow(...)]` once the issue #5822 gets fixed
309                     #[allow(clippy::option_if_let_else)]
310                     let suggestion = if let Some(e) = expr {
311                         snippet_with_applicability(cx, e.span, "v", &mut applicability)
312                     } else {
313                         applicability = Applicability::HasPlaceholders;
314                         Cow::Borrowed("v")
315                     };
316
317                     span_lint_and_sugg(
318                         cx,
319                         WRITELN_EMPTY_STRING,
320                         mac.span(),
321                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
322                         "replace it with",
323                         format!("writeln!({})", suggestion),
324                         applicability,
325                     );
326                 }
327             }
328         }
329     }
330 }
331
332 /// Given a format string that ends in a newline and its span, calculates the span of the
333 /// newline, or the format string itself if the format string consists solely of a newline.
334 fn newline_span(fmtstr: &StrLit) -> Span {
335     let sp = fmtstr.span;
336     let contents = &fmtstr.symbol.as_str();
337
338     if *contents == r"\n" {
339         return sp;
340     }
341
342     let newline_sp_hi = sp.hi()
343         - match fmtstr.style {
344             StrStyle::Cooked => BytePos(1),
345             StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
346         };
347
348     let newline_sp_len = if contents.ends_with('\n') {
349         BytePos(1)
350     } else if contents.ends_with(r"\n") {
351         BytePos(2)
352     } else {
353         panic!("expected format string to contain a newline");
354     };
355
356     sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
357 }
358
359 impl Write {
360     /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
361     /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
362     /// the contents of the string, whether it's a raw string, and the span of the literal in the
363     /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
364     /// `format_str` should be written to.
365     ///
366     /// Example:
367     ///
368     /// Calling this function on
369     /// ```rust
370     /// # use std::fmt::Write;
371     /// # let mut buf = String::new();
372     /// # let something = "something";
373     /// writeln!(buf, "string to write: {}", something);
374     /// ```
375     /// will return
376     /// ```rust,ignore
377     /// (Some("string to write: {}"), Some(buf))
378     /// ```
379     #[allow(clippy::too_many_lines)]
380     fn check_tts<'a>(&self, cx: &EarlyContext<'a>, tts: TokenStream, is_write: bool) -> (Option<StrLit>, Option<Expr>) {
381         use rustc_parse_format::{
382             AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec, ParseMode, Parser,
383             Piece,
384         };
385
386         let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
387         let mut expr: Option<Expr> = None;
388         if is_write {
389             expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
390                 Ok(p) => Some(p.into_inner()),
391                 Err(_) => return (None, None),
392             };
393             // might be `writeln!(foo)`
394             if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
395                 return (None, expr);
396             }
397         }
398
399         let fmtstr = match parser.parse_str_lit() {
400             Ok(fmtstr) => fmtstr,
401             Err(_) => return (None, expr),
402         };
403         let tmp = fmtstr.symbol.as_str();
404         let mut args = vec![];
405         let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
406         while let Some(piece) = fmt_parser.next() {
407             if !fmt_parser.errors.is_empty() {
408                 return (None, expr);
409             }
410             if let Piece::NextArgument(arg) = piece {
411                 if !self.in_debug_impl && arg.format.ty == "?" {
412                     // FIXME: modify rustc's fmt string parser to give us the current span
413                     span_lint(cx, USE_DEBUG, parser.prev_token.span, "use of `Debug`-based formatting");
414                 }
415                 args.push(arg);
416             }
417         }
418         let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
419         let mut idx = 0;
420         loop {
421             const SIMPLE: FormatSpec<'_> = FormatSpec {
422                 fill: None,
423                 align: AlignUnknown,
424                 flags: 0,
425                 precision: CountImplied,
426                 precision_span: None,
427                 width: CountImplied,
428                 width_span: None,
429                 ty: "",
430                 ty_span: None,
431             };
432             if !parser.eat(&token::Comma) {
433                 return (Some(fmtstr), expr);
434             }
435             let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
436                 expr
437             } else {
438                 return (Some(fmtstr), None);
439             };
440             match &token_expr.kind {
441                 ExprKind::Lit(lit) if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..)) => {
442                     let mut all_simple = true;
443                     let mut seen = false;
444                     for arg in &args {
445                         match arg.position {
446                             ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
447                                 if n == idx {
448                                     all_simple &= arg.format == SIMPLE;
449                                     seen = true;
450                                 }
451                             },
452                             ArgumentNamed(_) => {},
453                         }
454                     }
455                     if all_simple && seen {
456                         span_lint(cx, lint, token_expr.span, "literal with an empty format string");
457                     }
458                     idx += 1;
459                 },
460                 ExprKind::Assign(lhs, rhs, _) => {
461                     if_chain! {
462                         if let ExprKind::Lit(ref lit) = rhs.kind;
463                         if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..));
464                         if let ExprKind::Path(_, p) = &lhs.kind;
465                         then {
466                             let mut all_simple = true;
467                             let mut seen = false;
468                             for arg in &args {
469                                 match arg.position {
470                                     ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
471                                     ArgumentNamed(name) => {
472                                         if *p == name {
473                                             seen = true;
474                                             all_simple &= arg.format == SIMPLE;
475                                         }
476                                     },
477                                 }
478                             }
479                             if all_simple && seen {
480                                 span_lint(cx, lint, rhs.span, "literal with an empty format string");
481                             }
482                         }
483                     }
484                 },
485                 _ => idx += 1,
486             }
487         }
488     }
489
490     fn lint_println_empty_string(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
491         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
492             if fmt_str.symbol == kw::Empty {
493                 let name = mac.path.segments[0].ident.name;
494                 span_lint_and_sugg(
495                     cx,
496                     PRINTLN_EMPTY_STRING,
497                     mac.span(),
498                     &format!("using `{}!(\"\")`", name),
499                     "replace it with",
500                     format!("{}!()", name),
501                     Applicability::MachineApplicable,
502                 );
503             }
504         }
505     }
506
507     fn lint_print_with_newline(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
508         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
509             if check_newlines(&fmt_str) {
510                 let name = mac.path.segments[0].ident.name;
511                 let suggested = format!("{}ln", name);
512                 span_lint_and_then(
513                     cx,
514                     PRINT_WITH_NEWLINE,
515                     mac.span(),
516                     &format!("using `{}!()` with a format string that ends in a single newline", name),
517                     |err| {
518                         err.multipart_suggestion(
519                             &format!("use `{}!` instead", suggested),
520                             vec![(mac.path.span, suggested), (newline_span(&fmt_str), String::new())],
521                             Applicability::MachineApplicable,
522                         );
523                     },
524                 );
525             }
526         }
527     }
528 }
529
530 /// Checks if the format string contains a single newline that terminates it.
531 ///
532 /// Literal and escaped newlines are both checked (only literal for raw strings).
533 fn check_newlines(fmtstr: &StrLit) -> bool {
534     let mut has_internal_newline = false;
535     let mut last_was_cr = false;
536     let mut should_lint = false;
537
538     let contents = &fmtstr.symbol.as_str();
539
540     let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| {
541         let c = c.unwrap();
542
543         if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline {
544             should_lint = true;
545         } else {
546             last_was_cr = c == '\r';
547             if c == '\n' {
548                 has_internal_newline = true;
549             }
550         }
551     };
552
553     match fmtstr.style {
554         StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb),
555         StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb),
556     }
557
558     should_lint
559 }