]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
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 | } |