]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/write.rs
Merge commit 'e636b88aa180e8cab9e28802aac90adbc984234d' 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::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     /// // 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         if mac.path == sym!(println) {
239             span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`");
240             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
241                 if fmt_str.symbol == Symbol::intern("") {
242                     span_lint_and_sugg(
243                         cx,
244                         PRINTLN_EMPTY_STRING,
245                         mac.span(),
246                         "using `println!(\"\")`",
247                         "replace it with",
248                         "println!()".to_string(),
249                         Applicability::MachineApplicable,
250                     );
251                 }
252             }
253         } else if mac.path == sym!(print) {
254             span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`");
255             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
256                 if check_newlines(&fmt_str) {
257                     span_lint_and_then(
258                         cx,
259                         PRINT_WITH_NEWLINE,
260                         mac.span(),
261                         "using `print!()` with a format string that ends in a single newline",
262                         |err| {
263                             err.multipart_suggestion(
264                                 "use `println!` instead",
265                                 vec![
266                                     (mac.path.span, String::from("println")),
267                                     (newline_span(&fmt_str), String::new()),
268                                 ],
269                                 Applicability::MachineApplicable,
270                             );
271                         },
272                     );
273                 }
274             }
275         } else if mac.path == sym!(write) {
276             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), true) {
277                 if check_newlines(&fmt_str) {
278                     span_lint_and_then(
279                         cx,
280                         WRITE_WITH_NEWLINE,
281                         mac.span(),
282                         "using `write!()` with a format string that ends in a single newline",
283                         |err| {
284                             err.multipart_suggestion(
285                                 "use `writeln!()` instead",
286                                 vec![
287                                     (mac.path.span, String::from("writeln")),
288                                     (newline_span(&fmt_str), String::new()),
289                                 ],
290                                 Applicability::MachineApplicable,
291                             );
292                         },
293                     )
294                 }
295             }
296         } else if mac.path == sym!(writeln) {
297             if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) {
298                 if fmt_str.symbol == Symbol::intern("") {
299                     let mut applicability = Applicability::MachineApplicable;
300                     // FIXME: remove this `#[allow(...)]` once the issue #5822 gets fixed
301                     #[allow(clippy::option_if_let_else)]
302                     let suggestion = if let Some(e) = expr {
303                         snippet_with_applicability(cx, e.span, "v", &mut applicability)
304                     } else {
305                         applicability = Applicability::HasPlaceholders;
306                         Cow::Borrowed("v")
307                     };
308
309                     span_lint_and_sugg(
310                         cx,
311                         WRITELN_EMPTY_STRING,
312                         mac.span(),
313                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
314                         "replace it with",
315                         format!("writeln!({})", suggestion),
316                         applicability,
317                     );
318                 }
319             }
320         }
321     }
322 }
323
324 /// Given a format string that ends in a newline and its span, calculates the span of the
325 /// newline, or the format string itself if the format string consists solely of a newline.
326 fn newline_span(fmtstr: &StrLit) -> Span {
327     let sp = fmtstr.span;
328     let contents = &fmtstr.symbol.as_str();
329
330     if *contents == r"\n" {
331         return sp;
332     }
333
334     let newline_sp_hi = sp.hi()
335         - match fmtstr.style {
336             StrStyle::Cooked => BytePos(1),
337             StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
338         };
339
340     let newline_sp_len = if contents.ends_with('\n') {
341         BytePos(1)
342     } else if contents.ends_with(r"\n") {
343         BytePos(2)
344     } else {
345         panic!("expected format string to contain a newline");
346     };
347
348     sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
349 }
350
351 impl Write {
352     /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
353     /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
354     /// the contents of the string, whether it's a raw string, and the span of the literal in the
355     /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
356     /// `format_str` should be written to.
357     ///
358     /// Example:
359     ///
360     /// Calling this function on
361     /// ```rust
362     /// # use std::fmt::Write;
363     /// # let mut buf = String::new();
364     /// # let something = "something";
365     /// writeln!(buf, "string to write: {}", something);
366     /// ```
367     /// will return
368     /// ```rust,ignore
369     /// (Some("string to write: {}"), Some(buf))
370     /// ```
371     #[allow(clippy::too_many_lines)]
372     fn check_tts<'a>(&self, cx: &EarlyContext<'a>, tts: TokenStream, is_write: bool) -> (Option<StrLit>, Option<Expr>) {
373         use rustc_parse_format::{
374             AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied, FormatSpec, ParseMode, Parser,
375             Piece,
376         };
377
378         let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
379         let mut expr: Option<Expr> = None;
380         if is_write {
381             expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
382                 Ok(p) => Some(p.into_inner()),
383                 Err(_) => return (None, None),
384             };
385             // might be `writeln!(foo)`
386             if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
387                 return (None, expr);
388             }
389         }
390
391         let fmtstr = match parser.parse_str_lit() {
392             Ok(fmtstr) => fmtstr,
393             Err(_) => return (None, expr),
394         };
395         let tmp = fmtstr.symbol.as_str();
396         let mut args = vec![];
397         let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
398         while let Some(piece) = fmt_parser.next() {
399             if !fmt_parser.errors.is_empty() {
400                 return (None, expr);
401             }
402             if let Piece::NextArgument(arg) = piece {
403                 if !self.in_debug_impl && arg.format.ty == "?" {
404                     // FIXME: modify rustc's fmt string parser to give us the current span
405                     span_lint(cx, USE_DEBUG, parser.prev_token.span, "use of `Debug`-based formatting");
406                 }
407                 args.push(arg);
408             }
409         }
410         let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
411         let mut idx = 0;
412         loop {
413             const SIMPLE: FormatSpec<'_> = FormatSpec {
414                 fill: None,
415                 align: AlignUnknown,
416                 flags: 0,
417                 precision: CountImplied,
418                 precision_span: None,
419                 width: CountImplied,
420                 width_span: None,
421                 ty: "",
422                 ty_span: None,
423             };
424             if !parser.eat(&token::Comma) {
425                 return (Some(fmtstr), expr);
426             }
427             let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
428                 expr
429             } else {
430                 return (Some(fmtstr), None);
431             };
432             match &token_expr.kind {
433                 ExprKind::Lit(_) => {
434                     let mut all_simple = true;
435                     let mut seen = false;
436                     for arg in &args {
437                         match arg.position {
438                             ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
439                                 if n == idx {
440                                     all_simple &= arg.format == SIMPLE;
441                                     seen = true;
442                                 }
443                             },
444                             ArgumentNamed(_) => {},
445                         }
446                     }
447                     if all_simple && seen {
448                         span_lint(cx, lint, token_expr.span, "literal with an empty format string");
449                     }
450                     idx += 1;
451                 },
452                 ExprKind::Assign(lhs, rhs, _) => {
453                     if let ExprKind::Lit(_) = rhs.kind {
454                         if let ExprKind::Path(_, p) = &lhs.kind {
455                             let mut all_simple = true;
456                             let mut seen = false;
457                             for arg in &args {
458                                 match arg.position {
459                                     ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
460                                     ArgumentNamed(name) => {
461                                         if *p == name {
462                                             seen = true;
463                                             all_simple &= arg.format == SIMPLE;
464                                         }
465                                     },
466                                 }
467                             }
468                             if all_simple && seen {
469                                 span_lint(cx, lint, rhs.span, "literal with an empty format string");
470                             }
471                         }
472                     }
473                 },
474                 _ => idx += 1,
475             }
476         }
477     }
478 }
479
480 /// Checks if the format string contains a single newline that terminates it.
481 ///
482 /// Literal and escaped newlines are both checked (only literal for raw strings).
483 fn check_newlines(fmtstr: &StrLit) -> bool {
484     let mut has_internal_newline = false;
485     let mut last_was_cr = false;
486     let mut should_lint = false;
487
488     let contents = &fmtstr.symbol.as_str();
489
490     let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| {
491         let c = c.unwrap();
492
493         if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline {
494             should_lint = true;
495         } else {
496             last_was_cr = c == '\r';
497             if c == '\n' {
498                 has_internal_newline = true;
499             }
500         }
501     };
502
503     match fmtstr.style {
504         StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb),
505         StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb),
506     }
507
508     should_lint
509 }