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