]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/write.rs
Auto merge of #3946 - rchaser53:issue-3920, r=flip1995
[rust.git] / clippy_lints / src / write.rs
1 use crate::utils::{snippet_with_applicability, span_lint, span_lint_and_sugg};
2 use rustc::lint::{EarlyContext, EarlyLintPass, LintArray, LintPass};
3 use rustc::{declare_tool_lint, lint_array};
4 use rustc_errors::Applicability;
5 use std::borrow::Cow;
6 use syntax::ast::*;
7 use syntax::parse::{parser, token};
8 use syntax::tokenstream::{TokenStream, TokenTree};
9
10 declare_clippy_lint! {
11     /// **What it does:** This lint warns when you use `println!("")` to
12     /// print a newline.
13     ///
14     /// **Why is this bad?** You should use `println!()`, which is simpler.
15     ///
16     /// **Known problems:** None.
17     ///
18     /// **Example:**
19     /// ```rust
20     /// println!("");
21     /// ```
22     pub PRINTLN_EMPTY_STRING,
23     style,
24     "using `println!(\"\")` with an empty string"
25 }
26
27 declare_clippy_lint! {
28     /// **What it does:** This lint warns when you use `print!()` with a format
29     /// string that
30     /// ends in a newline.
31     ///
32     /// **Why is this bad?** You should use `println!()` instead, which appends the
33     /// newline.
34     ///
35     /// **Known problems:** None.
36     ///
37     /// **Example:**
38     /// ```rust
39     /// # let name = "World";
40     /// print!("Hello {}!\n", name);
41     /// ```
42     /// use println!() instead
43     /// ```rust
44     /// # let name = "World";
45     /// println!("Hello {}!", name);
46     /// ```
47     pub PRINT_WITH_NEWLINE,
48     style,
49     "using `print!()` with a format string that ends in a single newline"
50 }
51
52 declare_clippy_lint! {
53     /// **What it does:** Checks for printing on *stdout*. The purpose of this lint
54     /// is to catch debugging remnants.
55     ///
56     /// **Why is this bad?** People often print on *stdout* while debugging an
57     /// application and might forget to remove those prints afterward.
58     ///
59     /// **Known problems:** Only catches `print!` and `println!` calls.
60     ///
61     /// **Example:**
62     /// ```rust
63     /// println!("Hello world!");
64     /// ```
65     pub PRINT_STDOUT,
66     restriction,
67     "printing on stdout"
68 }
69
70 declare_clippy_lint! {
71     /// **What it does:** Checks for use of `Debug` formatting. The purpose of this
72     /// lint is to catch debugging remnants.
73     ///
74     /// **Why is this bad?** The purpose of the `Debug` trait is to facilitate
75     /// debugging Rust code. It should not be used in in user-facing output.
76     ///
77     /// **Example:**
78     /// ```rust
79     /// println!("{:?}", foo);
80     /// ```
81     pub USE_DEBUG,
82     restriction,
83     "use of `Debug`-based formatting"
84 }
85
86 declare_clippy_lint! {
87     /// **What it does:** This lint warns about the use of literals as `print!`/`println!` args.
88     ///
89     /// **Why is this bad?** Using literals as `println!` args is inefficient
90     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
91     /// (i.e., just put the literal in the format string)
92     ///
93     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
94     /// -- e.g., `println!("{}", env!("FOO"))`.
95     ///
96     /// **Example:**
97     /// ```rust
98     /// println!("{}", "foo");
99     /// ```
100     /// use the literal without formatting:
101     /// ```rust
102     /// println!("foo");
103     /// ```
104     pub PRINT_LITERAL,
105     style,
106     "printing a literal with a format string"
107 }
108
109 declare_clippy_lint! {
110     /// **What it does:** This lint warns when you use `writeln!(buf, "")` to
111     /// print a newline.
112     ///
113     /// **Why is this bad?** You should use `writeln!(buf)`, which is simpler.
114     ///
115     /// **Known problems:** None.
116     ///
117     /// **Example:**
118     /// ```rust
119     /// # use std::fmt::Write;
120     /// # let mut buf = String::new();
121     /// writeln!(buf, "");
122     /// ```
123     pub WRITELN_EMPTY_STRING,
124     style,
125     "using `writeln!(buf, \"\")` with an empty string"
126 }
127
128 declare_clippy_lint! {
129     /// **What it does:** This lint warns when you use `write!()` with a format
130     /// string that
131     /// ends in a newline.
132     ///
133     /// **Why is this bad?** You should use `writeln!()` instead, which appends the
134     /// newline.
135     ///
136     /// **Known problems:** None.
137     ///
138     /// **Example:**
139     /// ```rust
140     /// # use std::fmt::Write;
141     /// # let mut buf = String::new();
142     /// # let name = "World";
143     /// write!(buf, "Hello {}!\n", name);
144     /// ```
145     pub WRITE_WITH_NEWLINE,
146     style,
147     "using `write!()` with a format string that ends in a single newline"
148 }
149
150 declare_clippy_lint! {
151     /// **What it does:** This lint warns about the use of literals as `write!`/`writeln!` args.
152     ///
153     /// **Why is this bad?** Using literals as `writeln!` args is inefficient
154     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
155     /// (i.e., just put the literal in the format string)
156     ///
157     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
158     /// -- e.g., `writeln!(buf, "{}", env!("FOO"))`.
159     ///
160     /// **Example:**
161     /// ```rust
162     /// # use std::fmt::Write;
163     /// # let mut buf = String::new();
164     /// writeln!(buf, "{}", "foo");
165     /// ```
166     pub WRITE_LITERAL,
167     style,
168     "writing a literal with a format string"
169 }
170
171 #[derive(Copy, Clone, Debug)]
172 pub struct Pass;
173
174 impl LintPass for Pass {
175     fn get_lints(&self) -> LintArray {
176         lint_array!(
177             PRINT_WITH_NEWLINE,
178             PRINTLN_EMPTY_STRING,
179             PRINT_STDOUT,
180             USE_DEBUG,
181             PRINT_LITERAL,
182             WRITE_WITH_NEWLINE,
183             WRITELN_EMPTY_STRING,
184             WRITE_LITERAL
185         )
186     }
187
188     fn name(&self) -> &'static str {
189         "Write"
190     }
191 }
192
193 impl EarlyLintPass for Pass {
194     fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &Mac) {
195         if mac.node.path == "println" {
196             span_lint(cx, PRINT_STDOUT, mac.span, "use of `println!`");
197             if let Some(fmtstr) = check_tts(cx, &mac.node.tts, false).0 {
198                 if fmtstr == "" {
199                     span_lint_and_sugg(
200                         cx,
201                         PRINTLN_EMPTY_STRING,
202                         mac.span,
203                         "using `println!(\"\")`",
204                         "replace it with",
205                         "println!()".to_string(),
206                         Applicability::MachineApplicable,
207                     );
208                 }
209             }
210         } else if mac.node.path == "print" {
211             span_lint(cx, PRINT_STDOUT, mac.span, "use of `print!`");
212             if let (Some(fmtstr), _, is_raw) = check_tts(cx, &mac.node.tts, false) {
213                 if check_newlines(&fmtstr, is_raw) {
214                     span_lint(
215                         cx,
216                         PRINT_WITH_NEWLINE,
217                         mac.span,
218                         "using `print!()` with a format string that ends in a \
219                          single newline, consider using `println!()` instead",
220                     );
221                 }
222             }
223         } else if mac.node.path == "write" {
224             if let (Some(fmtstr), _, is_raw) = check_tts(cx, &mac.node.tts, true) {
225                 if check_newlines(&fmtstr, is_raw) {
226                     span_lint(
227                         cx,
228                         WRITE_WITH_NEWLINE,
229                         mac.span,
230                         "using `write!()` with a format string that ends in a \
231                          single newline, consider using `writeln!()` instead",
232                     );
233                 }
234             }
235         } else if mac.node.path == "writeln" {
236             let check_tts = check_tts(cx, &mac.node.tts, true);
237             if let Some(fmtstr) = check_tts.0 {
238                 if fmtstr == "" {
239                     let mut applicability = Applicability::MachineApplicable;
240                     let suggestion = check_tts.1.map_or_else(
241                         move || {
242                             applicability = Applicability::HasPlaceholders;
243                             Cow::Borrowed("v")
244                         },
245                         move |expr| snippet_with_applicability(cx, expr.span, "v", &mut applicability),
246                     );
247
248                     span_lint_and_sugg(
249                         cx,
250                         WRITELN_EMPTY_STRING,
251                         mac.span,
252                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
253                         "replace it with",
254                         format!("writeln!({})", suggestion),
255                         applicability,
256                     );
257                 }
258             }
259         }
260     }
261 }
262
263 /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
264 /// options and a bool. The first part of the tuple is `format_str` of the macros. The second part
265 /// of the tuple is in the `write[ln]!` case the expression the `format_str` should be written to.
266 /// The final part is a boolean flag indicating if the string is a raw string.
267 ///
268 /// Example:
269 ///
270 /// Calling this function on
271 /// ```rust
272 /// # use std::fmt::Write;
273 /// # let mut buf = String::new();
274 /// # let something = "something";
275 /// writeln!(buf, "string to write: {}", something);
276 /// ```
277 /// will return
278 /// ```rust,ignore
279 /// (Some("string to write: {}"), Some(buf), false)
280 /// ```
281 fn check_tts<'a>(cx: &EarlyContext<'a>, tts: &TokenStream, is_write: bool) -> (Option<String>, Option<Expr>, bool) {
282     use fmt_macros::*;
283     let tts = tts.clone();
284     let mut is_raw = false;
285     if let TokenStream(Some(tokens)) = &tts {
286         for token in tokens.iter() {
287             if let (TokenTree::Token(_, token::Token::Literal(lit, _)), _) = token {
288                 match lit {
289                     token::Lit::Str_(_) => break,
290                     token::Lit::StrRaw(_, _) => {
291                         is_raw = true;
292                         break;
293                     },
294                     _ => {},
295                 }
296             }
297         }
298     }
299     let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, None, false, false);
300     let mut expr: Option<Expr> = None;
301     if is_write {
302         expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
303             Ok(p) => Some(p.into_inner()),
304             Err(_) => return (None, None, is_raw),
305         };
306         // might be `writeln!(foo)`
307         if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
308             return (None, expr, is_raw);
309         }
310     }
311
312     let fmtstr = match parser.parse_str().map_err(|mut err| err.cancel()) {
313         Ok(token) => token.0.to_string(),
314         Err(_) => return (None, expr, is_raw),
315     };
316     let tmp = fmtstr.clone();
317     let mut args = vec![];
318     let mut fmt_parser = Parser::new(&tmp, None, Vec::new(), false);
319     while let Some(piece) = fmt_parser.next() {
320         if !fmt_parser.errors.is_empty() {
321             return (None, expr, is_raw);
322         }
323         if let Piece::NextArgument(arg) = piece {
324             if arg.format.ty == "?" {
325                 // FIXME: modify rustc's fmt string parser to give us the current span
326                 span_lint(cx, USE_DEBUG, parser.prev_span, "use of `Debug`-based formatting");
327             }
328             args.push(arg);
329         }
330     }
331     let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
332     let mut idx = 0;
333     loop {
334         const SIMPLE: FormatSpec<'_> = FormatSpec {
335             fill: None,
336             align: AlignUnknown,
337             flags: 0,
338             precision: CountImplied,
339             width: CountImplied,
340             ty: "",
341         };
342         if !parser.eat(&token::Comma) {
343             return (Some(fmtstr), expr, is_raw);
344         }
345         let token_expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
346             Ok(expr) => expr,
347             Err(_) => return (Some(fmtstr), None, is_raw),
348         };
349         match &token_expr.node {
350             ExprKind::Lit(_) => {
351                 let mut all_simple = true;
352                 let mut seen = false;
353                 for arg in &args {
354                     match arg.position {
355                         ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
356                             if n == idx {
357                                 all_simple &= arg.format == SIMPLE;
358                                 seen = true;
359                             }
360                         },
361                         ArgumentNamed(_) => {},
362                     }
363                 }
364                 if all_simple && seen {
365                     span_lint(cx, lint, token_expr.span, "literal with an empty format string");
366                 }
367                 idx += 1;
368             },
369             ExprKind::Assign(lhs, rhs) => {
370                 if let ExprKind::Lit(_) = rhs.node {
371                     if let ExprKind::Path(_, p) = &lhs.node {
372                         let mut all_simple = true;
373                         let mut seen = false;
374                         for arg in &args {
375                             match arg.position {
376                                 ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
377                                 ArgumentNamed(name) => {
378                                     if *p == name {
379                                         seen = true;
380                                         all_simple &= arg.format == SIMPLE;
381                                     }
382                                 },
383                             }
384                         }
385                         if all_simple && seen {
386                             span_lint(cx, lint, rhs.span, "literal with an empty format string");
387                         }
388                     }
389                 }
390             },
391             _ => idx += 1,
392         }
393     }
394 }
395
396 // Checks if `s` constains a single newline that terminates it
397 // Literal and escaped newlines are both checked (only literal for raw strings)
398 fn check_newlines(s: &str, is_raw: bool) -> bool {
399     if s.ends_with('\n') {
400         return true;
401     } else if is_raw {
402         return false;
403     }
404
405     if s.len() < 2 {
406         return false;
407     }
408
409     let bytes = s.as_bytes();
410     if bytes[bytes.len() - 2] != b'\\' || bytes[bytes.len() - 1] != b'n' {
411         return false;
412     }
413
414     let mut escaping = false;
415     for (index, &byte) in bytes.iter().enumerate() {
416         if escaping {
417             if byte == b'n' {
418                 return index == bytes.len() - 1;
419             }
420             escaping = false;
421         } else if byte == b'\\' {
422             escaping = true;
423         }
424     }
425
426     false
427 }