]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/write.rs
Auto merge of #105650 - cassaundra:float-literal-suggestion, r=pnkfelix
[rust.git] / src / tools / clippy / clippy_lints / src / write.rs
1 use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2 use clippy_utils::macros::{root_macro_call_first_node, FormatArgsExpn, MacroCall};
3 use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
4 use clippy_utils::{is_in_cfg_test, is_in_test_function};
5 use rustc_ast::LitKind;
6 use rustc_errors::Applicability;
7 use rustc_hir::{Expr, ExprKind, HirIdMap, Impl, Item, ItemKind};
8 use rustc_lint::{LateContext, LateLintPass, LintContext};
9 use rustc_session::{declare_tool_lint, impl_lint_pass};
10 use rustc_span::{sym, BytePos};
11
12 declare_clippy_lint! {
13     /// ### What it does
14     /// This lint warns when you use `println!("")` to
15     /// print a newline.
16     ///
17     /// ### Why is this bad?
18     /// You should use `println!()`, which is simpler.
19     ///
20     /// ### Example
21     /// ```rust
22     /// println!("");
23     /// ```
24     ///
25     /// Use instead:
26     /// ```rust
27     /// println!();
28     /// ```
29     #[clippy::version = "pre 1.29.0"]
30     pub PRINTLN_EMPTY_STRING,
31     style,
32     "using `println!(\"\")` with an empty string"
33 }
34
35 declare_clippy_lint! {
36     /// ### What it does
37     /// This lint warns when you use `print!()` with a format
38     /// string that ends in a newline.
39     ///
40     /// ### Why is this bad?
41     /// You should use `println!()` instead, which appends the
42     /// newline.
43     ///
44     /// ### Example
45     /// ```rust
46     /// # let name = "World";
47     /// print!("Hello {}!\n", name);
48     /// ```
49     /// use println!() instead
50     /// ```rust
51     /// # let name = "World";
52     /// println!("Hello {}!", name);
53     /// ```
54     #[clippy::version = "pre 1.29.0"]
55     pub PRINT_WITH_NEWLINE,
56     style,
57     "using `print!()` with a format string that ends in a single newline"
58 }
59
60 declare_clippy_lint! {
61     /// ### What it does
62     /// Checks for printing on *stdout*. The purpose of this lint
63     /// is to catch debugging remnants.
64     ///
65     /// ### Why is this bad?
66     /// People often print on *stdout* while debugging an
67     /// application and might forget to remove those prints afterward.
68     ///
69     /// ### Known problems
70     /// Only catches `print!` and `println!` calls.
71     ///
72     /// ### Example
73     /// ```rust
74     /// println!("Hello world!");
75     /// ```
76     #[clippy::version = "pre 1.29.0"]
77     pub PRINT_STDOUT,
78     restriction,
79     "printing on stdout"
80 }
81
82 declare_clippy_lint! {
83     /// ### What it does
84     /// Checks for printing on *stderr*. The purpose of this lint
85     /// is to catch debugging remnants.
86     ///
87     /// ### Why is this bad?
88     /// People often print on *stderr* while debugging an
89     /// application and might forget to remove those prints afterward.
90     ///
91     /// ### Known problems
92     /// Only catches `eprint!` and `eprintln!` calls.
93     ///
94     /// ### Example
95     /// ```rust
96     /// eprintln!("Hello world!");
97     /// ```
98     #[clippy::version = "1.50.0"]
99     pub PRINT_STDERR,
100     restriction,
101     "printing on stderr"
102 }
103
104 declare_clippy_lint! {
105     /// ### What it does
106     /// Checks for use of `Debug` formatting. The purpose of this
107     /// lint is to catch debugging remnants.
108     ///
109     /// ### Why is this bad?
110     /// The purpose of the `Debug` trait is to facilitate
111     /// debugging Rust code. It should not be used in user-facing output.
112     ///
113     /// ### Example
114     /// ```rust
115     /// # let foo = "bar";
116     /// println!("{:?}", foo);
117     /// ```
118     #[clippy::version = "pre 1.29.0"]
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     /// ### Example
134     /// ```rust
135     /// println!("{}", "foo");
136     /// ```
137     /// use the literal without formatting:
138     /// ```rust
139     /// println!("foo");
140     /// ```
141     #[clippy::version = "pre 1.29.0"]
142     pub PRINT_LITERAL,
143     style,
144     "printing a literal with a format string"
145 }
146
147 declare_clippy_lint! {
148     /// ### What it does
149     /// This lint warns when you use `writeln!(buf, "")` to
150     /// print a newline.
151     ///
152     /// ### Why is this bad?
153     /// You should use `writeln!(buf)`, which is simpler.
154     ///
155     /// ### Example
156     /// ```rust
157     /// # use std::fmt::Write;
158     /// # let mut buf = String::new();
159     /// writeln!(buf, "");
160     /// ```
161     ///
162     /// Use instead:
163     /// ```rust
164     /// # use std::fmt::Write;
165     /// # let mut buf = String::new();
166     /// writeln!(buf);
167     /// ```
168     #[clippy::version = "pre 1.29.0"]
169     pub WRITELN_EMPTY_STRING,
170     style,
171     "using `writeln!(buf, \"\")` with an empty string"
172 }
173
174 declare_clippy_lint! {
175     /// ### What it does
176     /// This lint warns when you use `write!()` with a format
177     /// string that
178     /// ends in a newline.
179     ///
180     /// ### Why is this bad?
181     /// You should use `writeln!()` instead, which appends the
182     /// newline.
183     ///
184     /// ### Example
185     /// ```rust
186     /// # use std::fmt::Write;
187     /// # let mut buf = String::new();
188     /// # let name = "World";
189     /// write!(buf, "Hello {}!\n", name);
190     /// ```
191     ///
192     /// Use instead:
193     /// ```rust
194     /// # use std::fmt::Write;
195     /// # let mut buf = String::new();
196     /// # let name = "World";
197     /// writeln!(buf, "Hello {}!", name);
198     /// ```
199     #[clippy::version = "pre 1.29.0"]
200     pub WRITE_WITH_NEWLINE,
201     style,
202     "using `write!()` with a format string that ends in a single newline"
203 }
204
205 declare_clippy_lint! {
206     /// ### What it does
207     /// This lint warns about the use of literals as `write!`/`writeln!` args.
208     ///
209     /// ### Why is this bad?
210     /// Using literals as `writeln!` args is inefficient
211     /// (c.f., https://github.com/matthiaskrgr/rust-str-bench) and unnecessary
212     /// (i.e., just put the literal in the format string)
213     ///
214     /// ### Example
215     /// ```rust
216     /// # use std::fmt::Write;
217     /// # let mut buf = String::new();
218     /// writeln!(buf, "{}", "foo");
219     /// ```
220     ///
221     /// Use instead:
222     /// ```rust
223     /// # use std::fmt::Write;
224     /// # let mut buf = String::new();
225     /// writeln!(buf, "foo");
226     /// ```
227     #[clippy::version = "pre 1.29.0"]
228     pub WRITE_LITERAL,
229     style,
230     "writing a literal with a format string"
231 }
232
233 #[derive(Default)]
234 pub struct Write {
235     in_debug_impl: bool,
236     allow_print_in_tests: bool,
237 }
238
239 impl Write {
240     pub fn new(allow_print_in_tests: bool) -> Self {
241         Self {
242             allow_print_in_tests,
243             ..Default::default()
244         }
245     }
246 }
247
248 impl_lint_pass!(Write => [
249     PRINT_WITH_NEWLINE,
250     PRINTLN_EMPTY_STRING,
251     PRINT_STDOUT,
252     PRINT_STDERR,
253     USE_DEBUG,
254     PRINT_LITERAL,
255     WRITE_WITH_NEWLINE,
256     WRITELN_EMPTY_STRING,
257     WRITE_LITERAL,
258 ]);
259
260 impl<'tcx> LateLintPass<'tcx> for Write {
261     fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
262         if is_debug_impl(cx, item) {
263             self.in_debug_impl = true;
264         }
265     }
266
267     fn check_item_post(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
268         if is_debug_impl(cx, item) {
269             self.in_debug_impl = false;
270         }
271     }
272
273     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
274         let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
275         let Some(diag_name) = cx.tcx.get_diagnostic_name(macro_call.def_id) else { return };
276         let Some(name) = diag_name.as_str().strip_suffix("_macro") else { return };
277
278         let is_build_script = cx
279             .sess()
280             .opts
281             .crate_name
282             .as_ref()
283             .map_or(false, |crate_name| crate_name == "build_script_build");
284
285         let allowed_in_tests = self.allow_print_in_tests
286             && (is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id));
287         match diag_name {
288             sym::print_macro | sym::println_macro if !allowed_in_tests => {
289                 if !is_build_script {
290                     span_lint(cx, PRINT_STDOUT, macro_call.span, &format!("use of `{name}!`"));
291                 }
292             },
293             sym::eprint_macro | sym::eprintln_macro if !allowed_in_tests => {
294                 span_lint(cx, PRINT_STDERR, macro_call.span, &format!("use of `{name}!`"));
295             },
296             sym::write_macro | sym::writeln_macro => {},
297             _ => return,
298         }
299
300         let Some(format_args) = FormatArgsExpn::find_nested(cx, expr, macro_call.expn) else { return };
301
302         // ignore `writeln!(w)` and `write!(v, some_macro!())`
303         if format_args.format_string.span.from_expansion() {
304             return;
305         }
306
307         match diag_name {
308             sym::print_macro | sym::eprint_macro | sym::write_macro => {
309                 check_newline(cx, &format_args, &macro_call, name);
310             },
311             sym::println_macro | sym::eprintln_macro | sym::writeln_macro => {
312                 check_empty_string(cx, &format_args, &macro_call, name);
313             },
314             _ => {},
315         }
316
317         check_literal(cx, &format_args, name);
318
319         if !self.in_debug_impl {
320             for arg in &format_args.args {
321                 if arg.format.r#trait == sym::Debug {
322                     span_lint(cx, USE_DEBUG, arg.span, "use of `Debug`-based formatting");
323                 }
324             }
325         }
326     }
327 }
328 fn is_debug_impl(cx: &LateContext<'_>, item: &Item<'_>) -> bool {
329     if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), .. }) = &item.kind
330         && let Some(trait_id) = trait_ref.trait_def_id()
331     {
332         cx.tcx.is_diagnostic_item(sym::Debug, trait_id)
333     } else {
334         false
335     }
336 }
337
338 fn check_newline(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
339     let format_string_parts = &format_args.format_string.parts;
340     let mut format_string_span = format_args.format_string.span;
341
342     let Some(last) = format_string_parts.last() else { return };
343
344     let count_vertical_whitespace = || {
345         format_string_parts
346             .iter()
347             .flat_map(|part| part.as_str().chars())
348             .filter(|ch| matches!(ch, '\r' | '\n'))
349             .count()
350     };
351
352     if last.as_str().ends_with('\n')
353         // ignore format strings with other internal vertical whitespace
354         && count_vertical_whitespace() == 1
355
356         // ignore trailing arguments: `print!("Issue\n{}", 1265);`
357         && format_string_parts.len() > format_args.args.len()
358     {
359         let lint = if name == "write" {
360             format_string_span = expand_past_previous_comma(cx, format_string_span);
361
362             WRITE_WITH_NEWLINE
363         } else {
364             PRINT_WITH_NEWLINE
365         };
366
367         span_lint_and_then(
368             cx,
369             lint,
370             macro_call.span,
371             &format!("using `{name}!()` with a format string that ends in a single newline"),
372             |diag| {
373                 let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
374                 let Some(format_snippet) = snippet_opt(cx, format_string_span) else { return };
375
376                 if format_string_parts.len() == 1 && last.as_str() == "\n" {
377                     // print!("\n"), write!(f, "\n")
378
379                     diag.multipart_suggestion(
380                         format!("use `{name}ln!` instead"),
381                         vec![(name_span, format!("{name}ln")), (format_string_span, String::new())],
382                         Applicability::MachineApplicable,
383                     );
384                 } else if format_snippet.ends_with("\\n\"") {
385                     // print!("...\n"), write!(f, "...\n")
386
387                     let hi = format_string_span.hi();
388                     let newline_span = format_string_span.with_lo(hi - BytePos(3)).with_hi(hi - BytePos(1));
389
390                     diag.multipart_suggestion(
391                         format!("use `{name}ln!` instead"),
392                         vec![(name_span, format!("{name}ln")), (newline_span, String::new())],
393                         Applicability::MachineApplicable,
394                     );
395                 }
396             },
397         );
398     }
399 }
400
401 fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, macro_call: &MacroCall, name: &str) {
402     if let [part] = &format_args.format_string.parts[..]
403         && let mut span = format_args.format_string.span
404         && part.as_str() == "\n"
405     {
406         let lint = if name == "writeln" {
407             span = expand_past_previous_comma(cx, span);
408
409             WRITELN_EMPTY_STRING
410         } else {
411             PRINTLN_EMPTY_STRING
412         };
413
414         span_lint_and_then(
415             cx,
416             lint,
417             macro_call.span,
418             &format!("empty string literal in `{name}!`"),
419             |diag| {
420                 diag.span_suggestion(
421                     span,
422                     "remove the empty string",
423                     String::new(),
424                     Applicability::MachineApplicable,
425                 );
426             },
427         );
428     }
429 }
430
431 fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &str) {
432     let mut counts = HirIdMap::<usize>::default();
433     for param in format_args.params() {
434         *counts.entry(param.value.hir_id).or_default() += 1;
435     }
436
437     for arg in &format_args.args {
438         let value = arg.param.value;
439
440         if counts[&value.hir_id] == 1
441             && arg.format.is_default()
442             && let ExprKind::Lit(lit) = &value.kind
443             && !value.span.from_expansion()
444             && let Some(value_string) = snippet_opt(cx, value.span)
445         {
446             let (replacement, replace_raw) = match lit.node {
447                 LitKind::Str(..) => extract_str_literal(&value_string),
448                 LitKind::Char(ch) => (
449                     match ch {
450                         '"' => "\\\"",
451                         '\'' => "'",
452                         _ => &value_string[1..value_string.len() - 1],
453                     }
454                     .to_string(),
455                     false,
456                 ),
457                 LitKind::Bool(b) => (b.to_string(), false),
458                 _ => continue,
459             };
460
461             let lint = if name.starts_with("write") {
462                 WRITE_LITERAL
463             } else {
464                 PRINT_LITERAL
465             };
466
467             let format_string_is_raw = format_args.format_string.style.is_some();
468             let replacement = match (format_string_is_raw, replace_raw) {
469                 (false, false) => Some(replacement),
470                 (false, true) => Some(replacement.replace('"', "\\\"").replace('\\', "\\\\")),
471                 (true, false) => match conservative_unescape(&replacement) {
472                     Ok(unescaped) => Some(unescaped),
473                     Err(UnescapeErr::Lint) => None,
474                     Err(UnescapeErr::Ignore) => continue,
475                 },
476                 (true, true) => {
477                     if replacement.contains(['#', '"']) {
478                         None
479                     } else {
480                         Some(replacement)
481                     }
482                 },
483             };
484
485             span_lint_and_then(
486                 cx,
487                 lint,
488                 value.span,
489                 "literal with an empty format string",
490                 |diag| {
491                     if let Some(replacement) = replacement
492                         // `format!("{}", "a")`, `format!("{named}", named = "b")
493                         //              ~~~~~                      ~~~~~~~~~~~~~
494                         && let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
495                     {
496                         let replacement = replacement.replace('{', "{{").replace('}', "}}");
497                         diag.multipart_suggestion(
498                             "try this",
499                             vec![(arg.span, replacement), (value_span, String::new())],
500                             Applicability::MachineApplicable,
501                         );
502                     }
503                 },
504             );
505         }
506     }
507 }
508
509 /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
510 ///
511 /// `r#"a"#` -> (`a`, true)
512 ///
513 /// `"b"` -> (`b`, false)
514 fn extract_str_literal(literal: &str) -> (String, bool) {
515     let (literal, raw) = match literal.strip_prefix('r') {
516         Some(stripped) => (stripped.trim_matches('#'), true),
517         None => (literal, false),
518     };
519
520     (literal[1..literal.len() - 1].to_string(), raw)
521 }
522
523 enum UnescapeErr {
524     /// Should still be linted, can be manually resolved by author, e.g.
525     ///
526     /// ```ignore
527     /// print!(r"{}", '"');
528     /// ```
529     Lint,
530     /// Should not be linted, e.g.
531     ///
532     /// ```ignore
533     /// print!(r"{}", '\r');
534     /// ```
535     Ignore,
536 }
537
538 /// Unescape a normal string into a raw string
539 fn conservative_unescape(literal: &str) -> Result<String, UnescapeErr> {
540     let mut unescaped = String::with_capacity(literal.len());
541     let mut chars = literal.chars();
542     let mut err = false;
543
544     while let Some(ch) = chars.next() {
545         match ch {
546             '#' => err = true,
547             '\\' => match chars.next() {
548                 Some('\\') => unescaped.push('\\'),
549                 Some('"') => err = true,
550                 _ => return Err(UnescapeErr::Ignore),
551             },
552             _ => unescaped.push(ch),
553         }
554     }
555
556     if err { Err(UnescapeErr::Lint) } else { Ok(unescaped) }
557 }