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