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