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