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