]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/write.rs
Rollup merge of #81260 - vn971:restore-editorconfig, r=Mark-Simulacrum
[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 { of_trait: Some(trait_ref), .. }) = &item.kind {
237             let trait_name = trait_ref
238                 .path
239                 .segments
240                 .iter()
241                 .last()
242                 .expect("path has at least one segment")
243                 .ident
244                 .name;
245             if trait_name == sym::Debug {
246                 self.in_debug_impl = true;
247             }
248         }
249     }
250
251     fn check_item_post(&mut self, _: &EarlyContext<'_>, _: &Item) {
252         self.in_debug_impl = false;
253     }
254
255     fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
256         fn is_build_script(cx: &EarlyContext<'_>) -> bool {
257             // Cargo sets the crate name for build scripts to `build_script_build`
258             cx.sess
259                 .opts
260                 .crate_name
261                 .as_ref()
262                 .map_or(false, |crate_name| crate_name == "build_script_build")
263         }
264
265         if mac.path == sym!(print) {
266             if !is_build_script(cx) {
267                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `print!`");
268             }
269             self.lint_print_with_newline(cx, mac);
270         } else if mac.path == sym!(println) {
271             if !is_build_script(cx) {
272                 span_lint(cx, PRINT_STDOUT, mac.span(), "use of `println!`");
273             }
274             self.lint_println_empty_string(cx, mac);
275         } else if mac.path == sym!(eprint) {
276             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprint!`");
277             self.lint_print_with_newline(cx, mac);
278         } else if mac.path == sym!(eprintln) {
279             span_lint(cx, PRINT_STDERR, mac.span(), "use of `eprintln!`");
280             self.lint_println_empty_string(cx, mac);
281         } else if mac.path == sym!(write) {
282             if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), true) {
283                 if check_newlines(&fmt_str) {
284                     span_lint_and_then(
285                         cx,
286                         WRITE_WITH_NEWLINE,
287                         mac.span(),
288                         "using `write!()` with a format string that ends in a single newline",
289                         |err| {
290                             err.multipart_suggestion(
291                                 "use `writeln!()` instead",
292                                 vec![
293                                     (mac.path.span, String::from("writeln")),
294                                     (newline_span(&fmt_str), String::new()),
295                                 ],
296                                 Applicability::MachineApplicable,
297                             );
298                         },
299                     )
300                 }
301             }
302         } else if mac.path == sym!(writeln) {
303             if let (Some(fmt_str), expr) = self.check_tts(cx, mac.args.inner_tokens(), true) {
304                 if fmt_str.symbol == kw::Empty {
305                     let mut applicability = Applicability::MachineApplicable;
306                     // FIXME: remove this `#[allow(...)]` once the issue #5822 gets fixed
307                     #[allow(clippy::option_if_let_else)]
308                     let suggestion = if let Some(e) = expr {
309                         snippet_with_applicability(cx, e.span, "v", &mut applicability)
310                     } else {
311                         applicability = Applicability::HasPlaceholders;
312                         Cow::Borrowed("v")
313                     };
314
315                     span_lint_and_sugg(
316                         cx,
317                         WRITELN_EMPTY_STRING,
318                         mac.span(),
319                         format!("using `writeln!({}, \"\")`", suggestion).as_str(),
320                         "replace it with",
321                         format!("writeln!({})", suggestion),
322                         applicability,
323                     );
324                 }
325             }
326         }
327     }
328 }
329
330 /// Given a format string that ends in a newline and its span, calculates the span of the
331 /// newline, or the format string itself if the format string consists solely of a newline.
332 fn newline_span(fmtstr: &StrLit) -> Span {
333     let sp = fmtstr.span;
334     let contents = &fmtstr.symbol.as_str();
335
336     if *contents == r"\n" {
337         return sp;
338     }
339
340     let newline_sp_hi = sp.hi()
341         - match fmtstr.style {
342             StrStyle::Cooked => BytePos(1),
343             StrStyle::Raw(hashes) => BytePos((1 + hashes).into()),
344         };
345
346     let newline_sp_len = if contents.ends_with('\n') {
347         BytePos(1)
348     } else if contents.ends_with(r"\n") {
349         BytePos(2)
350     } else {
351         panic!("expected format string to contain a newline");
352     };
353
354     sp.with_lo(newline_sp_hi - newline_sp_len).with_hi(newline_sp_hi)
355 }
356
357 impl Write {
358     /// Checks the arguments of `print[ln]!` and `write[ln]!` calls. It will return a tuple of two
359     /// `Option`s. The first `Option` of the tuple is the macro's format string. It includes
360     /// the contents of the string, whether it's a raw string, and the span of the literal in the
361     /// source. The second `Option` in the tuple is, in the `write[ln]!` case, the expression the
362     /// `format_str` should be written to.
363     ///
364     /// Example:
365     ///
366     /// Calling this function on
367     /// ```rust
368     /// # use std::fmt::Write;
369     /// # let mut buf = String::new();
370     /// # let something = "something";
371     /// writeln!(buf, "string to write: {}", something);
372     /// ```
373     /// will return
374     /// ```rust,ignore
375     /// (Some("string to write: {}"), Some(buf))
376     /// ```
377     #[allow(clippy::too_many_lines)]
378     fn check_tts<'a>(
379         &self,
380         cx: &EarlyContext<'a>,
381         tts: TokenStream,
382         is_write: bool,
383     ) -> (Option<StrLit>, Option<Expr>) {
384         use rustc_parse_format::{
385             AlignUnknown, ArgumentImplicitlyIs, ArgumentIs, ArgumentNamed, CountImplied,
386             FormatSpec, ParseMode, Parser, Piece,
387         };
388
389         let mut parser = parser::Parser::new(&cx.sess.parse_sess, tts, false, None);
390         let mut expr: Option<Expr> = None;
391         if is_write {
392             expr = match parser.parse_expr().map_err(|mut err| err.cancel()) {
393                 Ok(p) => Some(p.into_inner()),
394                 Err(_) => return (None, None),
395             };
396             // might be `writeln!(foo)`
397             if parser.expect(&token::Comma).map_err(|mut err| err.cancel()).is_err() {
398                 return (None, expr);
399             }
400         }
401
402         let fmtstr = match parser.parse_str_lit() {
403             Ok(fmtstr) => fmtstr,
404             Err(_) => return (None, expr),
405         };
406         let tmp = fmtstr.symbol.as_str();
407         let mut args = vec![];
408         let mut fmt_parser = Parser::new(&tmp, None, None, false, ParseMode::Format);
409         while let Some(piece) = fmt_parser.next() {
410             if !fmt_parser.errors.is_empty() {
411                 return (None, expr);
412             }
413             if let Piece::NextArgument(arg) = piece {
414                 if !self.in_debug_impl && arg.format.ty == "?" {
415                     // FIXME: modify rustc's fmt string parser to give us the current span
416                     span_lint(
417                         cx,
418                         USE_DEBUG,
419                         parser.prev_token.span,
420                         "use of `Debug`-based formatting",
421                     );
422                 }
423                 args.push(arg);
424             }
425         }
426         let lint = if is_write { WRITE_LITERAL } else { PRINT_LITERAL };
427         let mut idx = 0;
428         loop {
429             const SIMPLE: FormatSpec<'_> = FormatSpec {
430                 fill: None,
431                 align: AlignUnknown,
432                 flags: 0,
433                 precision: CountImplied,
434                 precision_span: None,
435                 width: CountImplied,
436                 width_span: None,
437                 ty: "",
438                 ty_span: None,
439             };
440             if !parser.eat(&token::Comma) {
441                 return (Some(fmtstr), expr);
442             }
443             let token_expr = if let Ok(expr) = parser.parse_expr().map_err(|mut err| err.cancel()) {
444                 expr
445             } else {
446                 return (Some(fmtstr), None);
447             };
448             match &token_expr.kind {
449                 ExprKind::Lit(lit)
450                     if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..)) =>
451                 {
452                     let mut all_simple = true;
453                     let mut seen = false;
454                     for arg in &args {
455                         match arg.position {
456                             ArgumentImplicitlyIs(n) | ArgumentIs(n) => {
457                                 if n == idx {
458                                     all_simple &= arg.format == SIMPLE;
459                                     seen = true;
460                                 }
461                             }
462                             ArgumentNamed(_) => {}
463                         }
464                     }
465                     if all_simple && seen {
466                         span_lint(cx, lint, token_expr.span, "literal with an empty format string");
467                     }
468                     idx += 1;
469                 }
470                 ExprKind::Assign(lhs, rhs, _) => {
471                     if_chain! {
472                         if let ExprKind::Lit(ref lit) = rhs.kind;
473                         if !matches!(lit.kind, LitKind::Int(..) | LitKind::Float(..));
474                         if let ExprKind::Path(_, p) = &lhs.kind;
475                         then {
476                             let mut all_simple = true;
477                             let mut seen = false;
478                             for arg in &args {
479                                 match arg.position {
480                                     ArgumentImplicitlyIs(_) | ArgumentIs(_) => {},
481                                     ArgumentNamed(name) => {
482                                         if *p == name {
483                                             seen = true;
484                                             all_simple &= arg.format == SIMPLE;
485                                         }
486                                     },
487                                 }
488                             }
489                             if all_simple && seen {
490                                 span_lint(cx, lint, rhs.span, "literal with an empty format string");
491                             }
492                         }
493                     }
494                 }
495                 _ => idx += 1,
496             }
497         }
498     }
499
500     fn lint_println_empty_string(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
501         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
502             if fmt_str.symbol == kw::Empty {
503                 let name = mac.path.segments[0].ident.name;
504                 span_lint_and_sugg(
505                     cx,
506                     PRINTLN_EMPTY_STRING,
507                     mac.span(),
508                     &format!("using `{}!(\"\")`", name),
509                     "replace it with",
510                     format!("{}!()", name),
511                     Applicability::MachineApplicable,
512                 );
513             }
514         }
515     }
516
517     fn lint_print_with_newline(&self, cx: &EarlyContext<'_>, mac: &MacCall) {
518         if let (Some(fmt_str), _) = self.check_tts(cx, mac.args.inner_tokens(), false) {
519             if check_newlines(&fmt_str) {
520                 let name = mac.path.segments[0].ident.name;
521                 let suggested = format!("{}ln", name);
522                 span_lint_and_then(
523                     cx,
524                     PRINT_WITH_NEWLINE,
525                     mac.span(),
526                     &format!(
527                         "using `{}!()` with a format string that ends in a single newline",
528                         name
529                     ),
530                     |err| {
531                         err.multipart_suggestion(
532                             &format!("use `{}!` instead", suggested),
533                             vec![
534                                 (mac.path.span, suggested),
535                                 (newline_span(&fmt_str), String::new()),
536                             ],
537                             Applicability::MachineApplicable,
538                         );
539                     },
540                 );
541             }
542         }
543     }
544 }
545
546 /// Checks if the format string contains a single newline that terminates it.
547 ///
548 /// Literal and escaped newlines are both checked (only literal for raw strings).
549 fn check_newlines(fmtstr: &StrLit) -> bool {
550     let mut has_internal_newline = false;
551     let mut last_was_cr = false;
552     let mut should_lint = false;
553
554     let contents = &fmtstr.symbol.as_str();
555
556     let mut cb = |r: Range<usize>, c: Result<char, EscapeError>| {
557         let c = c.unwrap();
558
559         if r.end == contents.len() && c == '\n' && !last_was_cr && !has_internal_newline {
560             should_lint = true;
561         } else {
562             last_was_cr = c == '\r';
563             if c == '\n' {
564                 has_internal_newline = true;
565             }
566         }
567     };
568
569     match fmtstr.style {
570         StrStyle::Cooked => unescape::unescape_literal(contents, unescape::Mode::Str, &mut cb),
571         StrStyle::Raw(_) => unescape::unescape_literal(contents, unescape::Mode::RawStr, &mut cb),
572     }
573
574     should_lint
575 }