]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/strings.rs
Auto merge of #8799 - Alexendoo:lintcheck-common, r=giraffate
[rust.git] / clippy_lints / src / strings.rs
1 use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg};
2 use clippy_utils::source::{snippet, snippet_with_applicability};
3 use clippy_utils::ty::is_type_diagnostic_item;
4 use clippy_utils::{get_parent_expr, is_lint_allowed, match_function_call, method_calls, paths};
5 use clippy_utils::{peel_blocks, SpanlessEq};
6 use if_chain::if_chain;
7 use rustc_errors::Applicability;
8 use rustc_hir::def_id::DefId;
9 use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, LangItem, QPath};
10 use rustc_lint::{LateContext, LateLintPass, LintContext};
11 use rustc_middle::lint::in_external_macro;
12 use rustc_middle::ty;
13 use rustc_session::{declare_lint_pass, declare_tool_lint};
14 use rustc_span::source_map::Spanned;
15 use rustc_span::sym;
16
17 declare_clippy_lint! {
18     /// ### What it does
19     /// Checks for string appends of the form `x = x + y` (without
20     /// `let`!).
21     ///
22     /// ### Why is this bad?
23     /// It's not really bad, but some people think that the
24     /// `.push_str(_)` method is more readable.
25     ///
26     /// ### Example
27     /// ```rust
28     /// let mut x = "Hello".to_owned();
29     /// x = x + ", World";
30     ///
31     /// // More readable
32     /// x += ", World";
33     /// x.push_str(", World");
34     /// ```
35     #[clippy::version = "pre 1.29.0"]
36     pub STRING_ADD_ASSIGN,
37     pedantic,
38     "using `x = x + ..` where x is a `String` instead of `push_str()`"
39 }
40
41 declare_clippy_lint! {
42     /// ### What it does
43     /// Checks for all instances of `x + _` where `x` is of type
44     /// `String`, but only if [`string_add_assign`](#string_add_assign) does *not*
45     /// match.
46     ///
47     /// ### Why is this bad?
48     /// It's not bad in and of itself. However, this particular
49     /// `Add` implementation is asymmetric (the other operand need not be `String`,
50     /// but `x` does), while addition as mathematically defined is symmetric, also
51     /// the `String::push_str(_)` function is a perfectly good replacement.
52     /// Therefore, some dislike it and wish not to have it in their code.
53     ///
54     /// That said, other people think that string addition, having a long tradition
55     /// in other languages is actually fine, which is why we decided to make this
56     /// particular lint `allow` by default.
57     ///
58     /// ### Example
59     /// ```rust
60     /// let x = "Hello".to_owned();
61     /// x + ", World";
62     /// ```
63     #[clippy::version = "pre 1.29.0"]
64     pub STRING_ADD,
65     restriction,
66     "using `x + ..` where x is a `String` instead of `push_str()`"
67 }
68
69 declare_clippy_lint! {
70     /// ### What it does
71     /// Checks for the `as_bytes` method called on string literals
72     /// that contain only ASCII characters.
73     ///
74     /// ### Why is this bad?
75     /// Byte string literals (e.g., `b"foo"`) can be used
76     /// instead. They are shorter but less discoverable than `as_bytes()`.
77     ///
78     /// ### Known problems
79     /// `"str".as_bytes()` and the suggested replacement of `b"str"` are not
80     /// equivalent because they have different types. The former is `&[u8]`
81     /// while the latter is `&[u8; 3]`. That means in general they will have a
82     /// different set of methods and different trait implementations.
83     ///
84     /// ```compile_fail
85     /// fn f(v: Vec<u8>) {}
86     ///
87     /// f("...".as_bytes().to_owned()); // works
88     /// f(b"...".to_owned()); // does not work, because arg is [u8; 3] not Vec<u8>
89     ///
90     /// fn g(r: impl std::io::Read) {}
91     ///
92     /// g("...".as_bytes()); // works
93     /// g(b"..."); // does not work
94     /// ```
95     ///
96     /// The actual equivalent of `"str".as_bytes()` with the same type is not
97     /// `b"str"` but `&b"str"[..]`, which is a great deal of punctuation and not
98     /// more readable than a function call.
99     ///
100     /// ### Example
101     /// ```rust
102     /// // Bad
103     /// let bs = "a byte string".as_bytes();
104     ///
105     /// // Good
106     /// let bs = b"a byte string";
107     /// ```
108     #[clippy::version = "pre 1.29.0"]
109     pub STRING_LIT_AS_BYTES,
110     nursery,
111     "calling `as_bytes` on a string literal instead of using a byte string literal"
112 }
113
114 declare_clippy_lint! {
115     /// ### What it does
116     /// Checks for slice operations on strings
117     ///
118     /// ### Why is this bad?
119     /// UTF-8 characters span multiple bytes, and it is easy to inadvertently confuse character
120     /// counts and string indices. This may lead to panics, and should warrant some test cases
121     /// containing wide UTF-8 characters. This lint is most useful in code that should avoid
122     /// panics at all costs.
123     ///
124     /// ### Known problems
125     /// Probably lots of false positives. If an index comes from a known valid position (e.g.
126     /// obtained via `char_indices` over the same string), it is totally OK.
127     ///
128     /// # Example
129     /// ```rust,should_panic
130     /// &"Ölkanne"[1..];
131     /// ```
132     #[clippy::version = "1.58.0"]
133     pub STRING_SLICE,
134     restriction,
135     "slicing a string"
136 }
137
138 declare_lint_pass!(StringAdd => [STRING_ADD, STRING_ADD_ASSIGN, STRING_SLICE]);
139
140 impl<'tcx> LateLintPass<'tcx> for StringAdd {
141     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
142         if in_external_macro(cx.sess(), e.span) {
143             return;
144         }
145         match e.kind {
146             ExprKind::Binary(
147                 Spanned {
148                     node: BinOpKind::Add, ..
149                 },
150                 left,
151                 _,
152             ) => {
153                 if is_string(cx, left) {
154                     if !is_lint_allowed(cx, STRING_ADD_ASSIGN, e.hir_id) {
155                         let parent = get_parent_expr(cx, e);
156                         if let Some(p) = parent {
157                             if let ExprKind::Assign(target, _, _) = p.kind {
158                                 // avoid duplicate matches
159                                 if SpanlessEq::new(cx).eq_expr(target, left) {
160                                     return;
161                                 }
162                             }
163                         }
164                     }
165                     span_lint(
166                         cx,
167                         STRING_ADD,
168                         e.span,
169                         "you added something to a string. Consider using `String::push_str()` instead",
170                     );
171                 }
172             },
173             ExprKind::Assign(target, src, _) => {
174                 if is_string(cx, target) && is_add(cx, src, target) {
175                     span_lint(
176                         cx,
177                         STRING_ADD_ASSIGN,
178                         e.span,
179                         "you assigned the result of adding something to this string. Consider using \
180                          `String::push_str()` instead",
181                     );
182                 }
183             },
184             ExprKind::Index(target, _idx) => {
185                 let e_ty = cx.typeck_results().expr_ty(target).peel_refs();
186                 if matches!(e_ty.kind(), ty::Str) || is_type_diagnostic_item(cx, e_ty, sym::String) {
187                     span_lint(
188                         cx,
189                         STRING_SLICE,
190                         e.span,
191                         "indexing into a string may panic if the index is within a UTF-8 character",
192                     );
193                 }
194             },
195             _ => {},
196         }
197     }
198 }
199
200 fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
201     is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), sym::String)
202 }
203
204 fn is_add(cx: &LateContext<'_>, src: &Expr<'_>, target: &Expr<'_>) -> bool {
205     match peel_blocks(src).kind {
206         ExprKind::Binary(
207             Spanned {
208                 node: BinOpKind::Add, ..
209             },
210             left,
211             _,
212         ) => SpanlessEq::new(cx).eq_expr(target, left),
213         _ => false,
214     }
215 }
216
217 declare_clippy_lint! {
218     /// ### What it does
219     /// Check if the string is transformed to byte array and casted back to string.
220     ///
221     /// ### Why is this bad?
222     /// It's unnecessary, the string can be used directly.
223     ///
224     /// ### Example
225     /// ```rust
226     /// let _ = std::str::from_utf8(&"Hello World!".as_bytes()[6..11]).unwrap();
227     /// ```
228     /// could be written as
229     /// ```rust
230     /// let _ = &"Hello World!"[6..11];
231     /// ```
232     #[clippy::version = "1.50.0"]
233     pub STRING_FROM_UTF8_AS_BYTES,
234     complexity,
235     "casting string slices to byte slices and back"
236 }
237
238 // Max length a b"foo" string can take
239 const MAX_LENGTH_BYTE_STRING_LIT: usize = 32;
240
241 declare_lint_pass!(StringLitAsBytes => [STRING_LIT_AS_BYTES, STRING_FROM_UTF8_AS_BYTES]);
242
243 impl<'tcx> LateLintPass<'tcx> for StringLitAsBytes {
244     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
245         use rustc_ast::LitKind;
246
247         if_chain! {
248             // Find std::str::converts::from_utf8
249             if let Some(args) = match_function_call(cx, e, &paths::STR_FROM_UTF8);
250
251             // Find string::as_bytes
252             if let ExprKind::AddrOf(BorrowKind::Ref, _, args) = args[0].kind;
253             if let ExprKind::Index(left, right) = args.kind;
254             let (method_names, expressions, _) = method_calls(left, 1);
255             if method_names.len() == 1;
256             if expressions.len() == 1;
257             if expressions[0].len() == 1;
258             if method_names[0] == sym!(as_bytes);
259
260             // Check for slicer
261             if let ExprKind::Struct(QPath::LangItem(LangItem::Range, ..), _, _) = right.kind;
262
263             then {
264                 let mut applicability = Applicability::MachineApplicable;
265                 let string_expression = &expressions[0][0];
266
267                 let snippet_app = snippet_with_applicability(
268                     cx,
269                     string_expression.span, "..",
270                     &mut applicability,
271                 );
272
273                 span_lint_and_sugg(
274                     cx,
275                     STRING_FROM_UTF8_AS_BYTES,
276                     e.span,
277                     "calling a slice of `as_bytes()` with `from_utf8` should be not necessary",
278                     "try",
279                     format!("Some(&{}[{}])", snippet_app, snippet(cx, right.span, "..")),
280                     applicability
281                 )
282             }
283         }
284
285         if_chain! {
286             if let ExprKind::MethodCall(path, args, _) = &e.kind;
287             if path.ident.name == sym!(as_bytes);
288             if let ExprKind::Lit(lit) = &args[0].kind;
289             if let LitKind::Str(lit_content, _) = &lit.node;
290             then {
291                 let callsite = snippet(cx, args[0].span.source_callsite(), r#""foo""#);
292                 let mut applicability = Applicability::MachineApplicable;
293                 if callsite.starts_with("include_str!") {
294                     span_lint_and_sugg(
295                         cx,
296                         STRING_LIT_AS_BYTES,
297                         e.span,
298                         "calling `as_bytes()` on `include_str!(..)`",
299                         "consider using `include_bytes!(..)` instead",
300                         snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability).replacen(
301                             "include_str",
302                             "include_bytes",
303                             1,
304                         ),
305                         applicability,
306                     );
307                 } else if lit_content.as_str().is_ascii()
308                     && lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT
309                     && !args[0].span.from_expansion()
310                 {
311                     span_lint_and_sugg(
312                         cx,
313                         STRING_LIT_AS_BYTES,
314                         e.span,
315                         "calling `as_bytes()` on a string literal",
316                         "consider using a byte string literal instead",
317                         format!(
318                             "b{}",
319                             snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability)
320                         ),
321                         applicability,
322                     );
323                 }
324             }
325         }
326
327         if_chain! {
328             if let ExprKind::MethodCall(path, [recv], _) = &e.kind;
329             if path.ident.name == sym!(into_bytes);
330             if let ExprKind::MethodCall(path, [recv], _) = &recv.kind;
331             if matches!(path.ident.name.as_str(), "to_owned" | "to_string");
332             if let ExprKind::Lit(lit) = &recv.kind;
333             if let LitKind::Str(lit_content, _) = &lit.node;
334
335             if lit_content.as_str().is_ascii();
336             if lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT;
337             if !recv.span.from_expansion();
338             then {
339                 let mut applicability = Applicability::MachineApplicable;
340
341                 span_lint_and_sugg(
342                     cx,
343                     STRING_LIT_AS_BYTES,
344                     e.span,
345                     "calling `into_bytes()` on a string literal",
346                     "consider using a byte string literal instead",
347                     format!(
348                         "b{}.to_vec()",
349                         snippet_with_applicability(cx, recv.span, r#""..""#, &mut applicability)
350                     ),
351                     applicability,
352                 );
353             }
354         }
355     }
356 }
357
358 declare_clippy_lint! {
359     /// ### What it does
360     /// This lint checks for `.to_string()` method calls on values of type `&str`.
361     ///
362     /// ### Why is this bad?
363     /// The `to_string` method is also used on other types to convert them to a string.
364     /// When called on a `&str` it turns the `&str` into the owned variant `String`, which can be better
365     /// expressed with `.to_owned()`.
366     ///
367     /// ### Example
368     /// ```rust
369     /// // example code where clippy issues a warning
370     /// let _ = "str".to_string();
371     /// ```
372     /// Use instead:
373     /// ```rust
374     /// // example code which does not raise clippy warning
375     /// let _ = "str".to_owned();
376     /// ```
377     #[clippy::version = "pre 1.29.0"]
378     pub STR_TO_STRING,
379     restriction,
380     "using `to_string()` on a `&str`, which should be `to_owned()`"
381 }
382
383 declare_lint_pass!(StrToString => [STR_TO_STRING]);
384
385 impl<'tcx> LateLintPass<'tcx> for StrToString {
386     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
387         if_chain! {
388             if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind;
389             if path.ident.name == sym!(to_string);
390             let ty = cx.typeck_results().expr_ty(self_arg);
391             if let ty::Ref(_, ty, ..) = ty.kind();
392             if *ty.kind() == ty::Str;
393             then {
394                 span_lint_and_help(
395                     cx,
396                     STR_TO_STRING,
397                     expr.span,
398                     "`to_string()` called on a `&str`",
399                     None,
400                     "consider using `.to_owned()`",
401                 );
402             }
403         }
404     }
405 }
406
407 declare_clippy_lint! {
408     /// ### What it does
409     /// This lint checks for `.to_string()` method calls on values of type `String`.
410     ///
411     /// ### Why is this bad?
412     /// The `to_string` method is also used on other types to convert them to a string.
413     /// When called on a `String` it only clones the `String`, which can be better expressed with `.clone()`.
414     ///
415     /// ### Example
416     /// ```rust
417     /// // example code where clippy issues a warning
418     /// let msg = String::from("Hello World");
419     /// let _ = msg.to_string();
420     /// ```
421     /// Use instead:
422     /// ```rust
423     /// // example code which does not raise clippy warning
424     /// let msg = String::from("Hello World");
425     /// let _ = msg.clone();
426     /// ```
427     #[clippy::version = "pre 1.29.0"]
428     pub STRING_TO_STRING,
429     restriction,
430     "using `to_string()` on a `String`, which should be `clone()`"
431 }
432
433 declare_lint_pass!(StringToString => [STRING_TO_STRING]);
434
435 impl<'tcx> LateLintPass<'tcx> for StringToString {
436     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
437         if_chain! {
438             if let ExprKind::MethodCall(path, [self_arg, ..], _) = &expr.kind;
439             if path.ident.name == sym!(to_string);
440             let ty = cx.typeck_results().expr_ty(self_arg);
441             if is_type_diagnostic_item(cx, ty, sym::String);
442             then {
443                 span_lint_and_help(
444                     cx,
445                     STRING_TO_STRING,
446                     expr.span,
447                     "`to_string()` called on a `String`",
448                     None,
449                     "consider using `.clone()`",
450                 );
451             }
452         }
453     }
454 }
455
456 declare_clippy_lint! {
457     /// ### What it does
458     /// Warns about calling `str::trim` (or variants) before `str::split_whitespace`.
459     ///
460     /// ### Why is this bad?
461     /// `split_whitespace` already ignores leading and trailing whitespace.
462     ///
463     /// ### Example
464     /// ```rust
465     /// " A B C ".trim().split_whitespace();
466     /// ```
467     /// Use instead:
468     /// ```rust
469     /// " A B C ".split_whitespace();
470     /// ```
471     #[clippy::version = "1.62.0"]
472     pub TRIM_SPLIT_WHITESPACE,
473     style,
474     "using `str::trim()` or alike before `str::split_whitespace`"
475 }
476 declare_lint_pass!(TrimSplitWhitespace => [TRIM_SPLIT_WHITESPACE]);
477
478 impl<'tcx> LateLintPass<'tcx> for TrimSplitWhitespace {
479     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
480         let tyckres = cx.typeck_results();
481         if_chain! {
482             if let ExprKind::MethodCall(path, [split_recv], split_ws_span) = expr.kind;
483             if path.ident.name == sym!(split_whitespace);
484             if let Some(split_ws_def_id) = tyckres.type_dependent_def_id(expr.hir_id);
485             if cx.tcx.is_diagnostic_item(sym::str_split_whitespace, split_ws_def_id);
486             if let ExprKind::MethodCall(path, [_trim_recv], trim_span) = split_recv.kind;
487             if let trim_fn_name @ ("trim" | "trim_start" | "trim_end") = path.ident.name.as_str();
488             if let Some(trim_def_id) = tyckres.type_dependent_def_id(split_recv.hir_id);
489             if is_one_of_trim_diagnostic_items(cx, trim_def_id);
490             then {
491                 span_lint_and_sugg(
492                     cx,
493                     TRIM_SPLIT_WHITESPACE,
494                     trim_span.with_hi(split_ws_span.lo()),
495                     &format!("found call to `str::{}` before `str::split_whitespace`", trim_fn_name),
496                     &format!("remove `{}()`", trim_fn_name),
497                     String::new(),
498                     Applicability::MachineApplicable,
499                 );
500             }
501         }
502     }
503 }
504
505 fn is_one_of_trim_diagnostic_items(cx: &LateContext<'_>, trim_def_id: DefId) -> bool {
506     cx.tcx.is_diagnostic_item(sym::str_trim, trim_def_id)
507         || cx.tcx.is_diagnostic_item(sym::str_trim_start, trim_def_id)
508         || cx.tcx.is_diagnostic_item(sym::str_trim_end, trim_def_id)
509 }