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