]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/write.rs
Auto merge of #82680 - jturner314:div_euclid-docs, r=JohnTitor
[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 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     /// // Bad
153     /// writeln!(buf, "");
154     ///
155     /// // Good
156     /// writeln!(buf);
157     /// ```
158     pub WRITELN_EMPTY_STRING,
159     style,
160     "using `writeln!(buf, \"\")` with an empty string"
161 }
162
163 declare_clippy_lint! {
164     /// **What it does:** This lint warns when you use `write!()` with a format
165     /// string that
166     /// ends in a newline.
167     ///
168     /// **Why is this bad?** You should use `writeln!()` instead, which appends the
169     /// newline.
170     ///
171     /// **Known problems:** None.
172     ///
173     /// **Example:**
174     /// ```rust
175     /// # use std::fmt::Write;
176     /// # let mut buf = String::new();
177     /// # let name = "World";
178     /// // Bad
179     /// write!(buf, "Hello {}!\n", name);
180     ///
181     /// // Good
182     /// writeln!(buf, "Hello {}!", name);
183     /// ```
184     pub WRITE_WITH_NEWLINE,
185     style,
186     "using `write!()` with a format string that ends in a single newline"
187 }
188
189 declare_clippy_lint! {
190     /// **What it does:** This lint warns about the use of literals as `write!`/`writeln!` args.
191     ///
192     /// **Why is this bad?** Using literals as `writeln!` args is inefficient
193     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
194     /// (i.e., just put the literal in the format string)
195     ///
196     /// **Known problems:** Will also warn with macro calls as arguments that expand to literals
197     /// -- e.g., `writeln!(buf, "{}", env!("FOO"))`.
198     ///
199     /// **Example:**
200     /// ```rust
201     /// # use std::fmt::Write;
202     /// # let mut buf = String::new();
203     /// // Bad
204     /// writeln!(buf, "{}", "foo");
205     ///
206     /// // Good
207     /// writeln!(buf, "foo");
208     /// ```
209     pub WRITE_LITERAL,
210     style,
211     "writing a literal with a format string"
212 }
213
214 #[derive(Default)]
215 pub struct Write {
216     in_debug_impl: bool,
217 }
218
219 impl_lint_pass!(Write => [
220     PRINT_WITH_NEWLINE,
221     PRINTLN_EMPTY_STRING,
222     PRINT_STDOUT,
223     PRINT_STDERR,
224     USE_DEBUG,
225     PRINT_LITERAL,
226     WRITE_WITH_NEWLINE,
227     WRITELN_EMPTY_STRING,
228     WRITE_LITERAL
229 ]);
230
231 impl EarlyLintPass for Write {
232     fn check_item(&mut self, _: &EarlyContext<'_>, item: &Item) {
233         if let ItemKind::Impl(box ImplKind {
234             of_trait: Some(trait_ref),
235             ..
236         }) = &item.kind
237         {
238             let trait_name = trait_ref
239                 .path
240                 .segments
241                 .iter()
242                 .last()
243                 .expect("path has at least one segment")
244                 .ident
245                 .name;
246             if trait_name == sym::Debug {
247                 self.in_debug_impl = true;
248             }
249         }
250     }
251
252     fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) {
253         self.in_debug_impl = false;
254     }
255
256     fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
257         fn is_build_script(cx: &EarlyContext<'_>) -> bool {
258             // Cargo sets the crate name for build scripts to `build_script_build`
259             cx.sess
260                 .opts
261                 .crate_name
262                 .as_ref()
263                 .map_or(false, |crate_name| crate_name == "build_script_build")
264         }
265
266         if mac.path == sym!(print) {
267             if !is_build_script(cx) {
268                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`");
269             }
270             self.lint_print_with_newline(cx, mac);
271         } else if mac.path == sym!(println) {
272             if !is_build_script(cx) {
273                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`");
274             }
275             self.lint_println_empty_string(cx, mac);
276         } else if mac.path == sym!(eprint) {
277             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprint!`");
278             self.lint_print_with_newline(cx, mac);
279         } else if mac.path == sym!(eprintln) {
280             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprintln!`");
281             self.lint_println_empty_string(cx, mac);
282         } else if mac.path == sym!(write) {
283             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), true) {
284                 if check_newlines(&fmt_str) {
285                     span_lint_and_then(
286                         cx,
287                         WRITE_WITH_NEWLINE,
288                         mac.span(),
289                         "using `write!()` with a format string that ends in a single newline",
290                         |err| {
291                             err.multipart_suggestion(
292                                 "use `writeln!()` instead",
293                                 vec![
294                                     (mac.path.span, String::from("writeln")),
295                                     (newline_span(&fmt_str), String::new()),
296                                 ],
297                                 Applicability::MachineApplicable,
298                             );
299                         },
300                     )
301                 }
302             }
303         } else if mac.path == sym!(writeln) {
304             if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) {
305                 if fmt_str.symbol == kw::Empty {
306                     let mut applicability = Applicability::MachineApplicable;
307                     // FIXME: remove this `#[allow(...)]` once the issue #5822 gets fixed
308                     #[allow(clippy::option_if_let_else)]
309                     let suggestion = if let Some(e) = expr {
310                         snippet_with_applicability(cx, e.span, "v", &mut applicability)
311                     } else {
312                         applicability = Applicability::HasPlaceholders;
313                         Cow::Borrowed("v")
314                     };
315
316                     span_lint_and_sugg(
317                         cx,
318                         WRITELN_EMPTY_STRING,
319                         mac.span(),
320                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
321                         "replace it with",
322                         format!("writeln!({})", suggestion),
323                         applicability,
324                     );
325                 }
326             }
327         }
328     }
329 }
330
331 /// Given a format string that ends in a newline and its span, calculates the span of the
332 /// newline, or the format string itself if the format string consists solely of a newline.
333 fn newline_span(fmtstr: &StrLit) -> Span {
334     let sp = fmtstr.span;
335     let contents = &fmtstr.symbol.as_str();
336
337     if *contents == r"\n" {
338         return sp;
339     }
340
341     let newline_sp_hi = sp.hi()
342         - match fmtstr.style {
343             StrStyle::Cooked => BytePos(1),
344             StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
345         };
346
347     let newline_sp_len = if contents.ends_with('\n') {
348         BytePos(1)
349     } else if contents.ends_with(r"\n") {
350         BytePos(2)
351     } else {
352         panic!("expected format string to contain a newline");
353     };
354
355     sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
356 }
357
358 impl Write {
359     /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
360     /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
361     /// the contents of the string, whether it's a raw string, and the span of the literal in the
362     /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
363     /// `format_str` should be written to.
364     ///
365     /// Example:
366     ///
367     /// Calling this function on
368     /// ```rust
369     /// # use std::fmt::Write;
370     /// # let mut buf = String::new();
371     /// # let something = "something";
372     /// writeln!(buf, "string to write: {}", something);
373     /// ```
374     /// will return
375     /// ```rust,ignore
376     /// (Some("string to write: {}"), Some(buf))
377     /// ```
378     #[allow(clippy::too_many_lines)]
379     fn check_tts<'a>(
380         &self,
381         cx: &EarlyContext<'a>,
382         tts: TokenStream,
383         is_write: bool,
384     ) -> (Option<StrLit>, Option<Expr>) {
385         use rustc_parse_format::{
386             AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied,
387             FormatSpec, ParseMode, Parser, Piece,
388         };
389
390         let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
391         let mut expr: Option<Expr> = None;
392         if is_write {
393             expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
394                 Ok(p) => Some(p.into_inner()),
395                 Err(_) => return (None, None),
396             };
397             // might be `writeln!(foo)`
398             if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
399                 return (None, expr);
400             }
401         }
402
403         let fmtstr = match parser.parse_str_lit() {
404             Ok(fmtstr) => fmtstr,
405             Err(_) => return (None, expr),
406         };
407         let tmp = fmtstr.symbol.as_str();
408         let mut args = vec![];
409         let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
410         while let Some(piece) = fmt_parser.next() {
411             if !fmt_parser.errors.is_empty() {
412                 return (None, expr);
413             }
414             if let Piece::NextArgument(arg) = piece {
415                 if !self.in_debug_impl && arg.format.ty == "?" {
416                     // FIXME: modify rustc's fmt string parser to give us the current span
417                     span_lint(
418                         cx,
419                         USE_DEBUG,
420                         parser.prev_token.span,
421                         "use of `Debug`-based formatting",
422                     );
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(lit)
451                     if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..)) =>
452                 {
453                     let mut all_simple = true;
454                     let mut seen = false;
455                     for arg in &args {
456                         match arg.position {
457                             ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
458                                 if n == idx {
459                                     all_simple &= arg.format == SIMPLE;
460                                     seen = true;
461                                 }
462                             }
463                             ArgumentNamed(_) => {}
464                         }
465                     }
466                     if all_simple && seen {
467                         span_lint(cx, lint, token_expr.span, "literal with an empty format string");
468                     }
469                     idx += 1;
470                 }
471                 ExprKind::Assign(lhs, rhs, _) => {
472                     if_chain! {
473                         if let ExprKind::Lit(ref lit) = rhs.kind;
474                         if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..));
475                         if let ExprKind::Path(_, p) = &lhs.kind;
476                         then {
477                             let mut all_simple = true;
478                             let mut seen = false;
479                             for arg in &args {
480                                 match arg.position {
481                                     ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
482                                     ArgumentNamed(name) => {
483                                         if *p == name {
484                                             seen = true;
485                                             all_simple &= arg.format == SIMPLE;
486                                         }
487                                     },
488                                 }
489                             }
490                             if all_simple && seen {
491                                 span_lint(cx, lint, rhs.span, "literal with an empty format string");
492                             }
493                         }
494                     }
495                 }
496                 _ => idx += 1,
497             }
498         }
499     }
500
501     fn lint_println_empty_string(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
502         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
503             if fmt_str.symbol == kw::Empty {
504                 let name = mac.path.segments[0].ident.name;
505                 span_lint_and_sugg(
506                     cx,
507                     PRINTLN_EMPTY_STRING,
508                     mac.span(),
509                     &format!("using `{}!(\"\")`", name),
510                     "replace it with",
511                     format!("{}!()", name),
512                     Applicability::MachineApplicable,
513                 );
514             }
515         }
516     }
517
518     fn lint_print_with_newline(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
519         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
520             if check_newlines(&fmt_str) {
521                 let name = mac.path.segments[0].ident.name;
522                 let suggested = format!("{}ln", name);
523                 span_lint_and_then(
524                     cx,
525                     PRINT_WITH_NEWLINE,
526                     mac.span(),
527                     &format!(
528                         "using `{}!()` with a format string that ends in a single newline",
529                         name
530                     ),
531                     |err| {
532                         err.multipart_suggestion(
533                             &format!("use `{}!` instead", suggested),
534                             vec![
535                                 (mac.path.span, suggested),
536                                 (newline_span(&fmt_str), String::new()),
537                             ],
538                             Applicability::MachineApplicable,
539                         );
540                     },
541                 );
542             }
543         }
544     }
545 }
546
547 /// Checks if the format string contains a single newline that terminates it.
548 ///
549 /// Literal and escaped newlines are both checked (only literal for raw strings).
550 fn check_newlines(fmtstr: &StrLit) -> bool {
551     let mut has_internal_newline = false;
552     let mut last_was_cr = false;
553     let mut should_lint = false;
554
555     let contents = &fmtstr.symbol.as_str();
556
557     let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| {
558         let c = c.unwrap();
559
560         if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline {
561             should_lint = true;
562         } else {
563             last_was_cr = c == '\r';
564             if c == '\n' {
565                 has_internal_newline = true;
566             }
567         }
568     };
569
570     match fmtstr.style {
571         StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb),
572         StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb),
573     }
574
575     should_lint
576 }