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