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