]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/strings.rs
Auto merge of #71780 - jcotton42:string_remove_matches, r=joshtriplett
[rust.git] / src / tools / clippy / clippy_lints / src / strings.rs
1 use rustc_errors::Applicability;
2 use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, LangItem, QPath};
3 use rustc_lint::{LateContext, LateLintPass, LintContext};
4 use rustc_middle::lint::in_external_macro;
5 use rustc_middle::ty;
6 use rustc_session::{declare_lint_pass, declare_tool_lint};
7 use rustc_span::source_map::Spanned;
8 use rustc_span::sym;
9
10 use if_chain::if_chain;
11
12 use crate::utils::SpanlessEq;
13 use crate::utils::{
14     get_parent_expr, is_allowed, is_type_diagnostic_item, match_function_call, method_calls, paths, span_lint,
15     span_lint_and_help, span_lint_and_sugg,
16 };
17
18 declare_clippy_lint! {
19     /// **What it does:** Checks for string appends of the form `x = x + y` (without
20     /// `let`!).
21     ///
22     /// **Why is this bad?** It's not really bad, but some people think that the
23     /// `.push_str(_)` method is more readable.
24     ///
25     /// **Known problems:** None.
26     ///
27     /// **Example:**
28     ///
29     /// ```rust
30     /// let mut x = "Hello".to_owned();
31     /// x = x + ", World";
32     ///
33     /// // More readable
34     /// x += ", World";
35     /// x.push_str(", World");
36     /// ```
37     pub STRING_ADD_ASSIGN,
38     pedantic,
39     "using `x = x + ..` where x is a `String` instead of `push_str()`"
40 }
41
42 declare_clippy_lint! {
43     /// **What it does:** 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?** It's not bad in and of itself. However, this particular
48     /// `Add` implementation is asymmetric (the other operand need not be `String`,
49     /// but `x` does), while addition as mathematically defined is symmetric, also
50     /// the `String::push_str(_)` function is a perfectly good replacement.
51     /// Therefore, some dislike it and wish not to have it in their code.
52     ///
53     /// That said, other people think that string addition, having a long tradition
54     /// in other languages is actually fine, which is why we decided to make this
55     /// particular lint `allow` by default.
56     ///
57     /// **Known problems:** None.
58     ///
59     /// **Example:**
60     ///
61     /// ```rust
62     /// let x = "Hello".to_owned();
63     /// x + ", World";
64     /// ```
65     pub STRING_ADD,
66     restriction,
67     "using `x + ..` where x is a `String` instead of `push_str()`"
68 }
69
70 declare_clippy_lint! {
71     /// **What it does:** Checks for the `as_bytes` method called on string literals
72     /// that contain only ASCII characters.
73     ///
74     /// **Why is this bad?** Byte string literals (e.g., `b"foo"`) can be used
75     /// instead. They are shorter but less discoverable than `as_bytes()`.
76     ///
77     /// **Known Problems:**
78     /// `"str".as_bytes()` and the suggested replacement of `b"str"` are not
79     /// equivalent because they have different types. The former is `&[u8]`
80     /// while the latter is `&[u8; 3]`. That means in general they will have a
81     /// different set of methods and different trait implementations.
82     ///
83     /// ```compile_fail
84     /// fn f(v: Vec<u8>) {}
85     ///
86     /// f("...".as_bytes().to_owned()); // works
87     /// f(b"...".to_owned()); // does not work, because arg is [u8; 3] not Vec<u8>
88     ///
89     /// fn g(r: impl std::io::Read) {}
90     ///
91     /// g("...".as_bytes()); // works
92     /// g(b"..."); // does not work
93     /// ```
94     ///
95     /// The actual equivalent of `"str".as_bytes()` with the same type is not
96     /// `b"str"` but `&b"str"[..]`, which is a great deal of punctuation and not
97     /// more readable than a function call.
98     ///
99     /// **Example:**
100     /// ```rust
101     /// // Bad
102     /// let bs = "a byte string".as_bytes();
103     ///
104     /// // Good
105     /// let bs = b"a byte string";
106     /// ```
107     pub STRING_LIT_AS_BYTES,
108     nursery,
109     "calling `as_bytes` on a string literal instead of using a byte string literal"
110 }
111
112 declare_lint_pass!(StringAdd => [STRING_ADD, STRING_ADD_ASSIGN]);
113
114 impl<'tcx> LateLintPass<'tcx> for StringAdd {
115     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
116         if in_external_macro(cx.sess(), e.span) {
117             return;
118         }
119
120         if let ExprKind::Binary(
121             Spanned {
122                 node: BinOpKind::Add, ..
123             },
124             ref left,
125             _,
126         ) = e.kind
127         {
128             if is_string(cx, left) {
129                 if !is_allowed(cx, STRING_ADD_ASSIGN, e.hir_id) {
130                     let parent = get_parent_expr(cx, e);
131                     if let Some(p) = parent {
132                         if let ExprKind::Assign(ref target, _, _) = p.kind {
133                             // avoid duplicate matches
134                             if SpanlessEq::new(cx).eq_expr(target, left) {
135                                 return;
136                             }
137                         }
138                     }
139                 }
140                 span_lint(
141                     cx,
142                     STRING_ADD,
143                     e.span,
144                     "you added something to a string. Consider using `String::push_str()` instead",
145                 );
146             }
147         } else if let ExprKind::Assign(ref target, ref src, _) = e.kind {
148             if is_string(cx, target) && is_add(cx, src, target) {
149                 span_lint(
150                     cx,
151                     STRING_ADD_ASSIGN,
152                     e.span,
153                     "you assigned the result of adding something to this string. Consider using \
154                      `String::push_str()` instead",
155                 );
156             }
157         }
158     }
159 }
160
161 fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
162     is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), sym::string_type)
163 }
164
165 fn is_add(cx: &LateContext<'_>, src: &Expr<'_>, target: &Expr<'_>) -> bool {
166     match src.kind {
167         ExprKind::Binary(
168             Spanned {
169                 node: BinOpKind::Add, ..
170             },
171             ref left,
172             _,
173         ) => SpanlessEq::new(cx).eq_expr(target, left),
174         ExprKind::Block(ref block, _) => {
175             block.stmts.is_empty() && block.expr.as_ref().map_or(false, |expr| is_add(cx, expr, target))
176         },
177         _ => false,
178     }
179 }
180
181 declare_clippy_lint! {
182     /// **What it does:** Check if the string is transformed to byte array and casted back to string.
183     ///
184     /// **Why is this bad?** It's unnecessary, the string can be used directly.
185     ///
186     /// **Known problems:** None
187     ///
188     /// **Example:**
189     /// ```rust
190     /// let _ = std::str::from_utf8(&"Hello World!".as_bytes()[6..11]).unwrap();
191     /// ```
192     /// could be written as
193     /// ```rust
194     /// let _ = &"Hello World!"[6..11];
195     /// ```
196     pub STRING_FROM_UTF8_AS_BYTES,
197     complexity,
198     "casting string slices to byte slices and back"
199 }
200
201 // Max length a b"foo" string can take
202 const MAX_LENGTH_BYTE_STRING_LIT: usize = 32;
203
204 declare_lint_pass!(StringLitAsBytes => [STRING_LIT_AS_BYTES, STRING_FROM_UTF8_AS_BYTES]);
205
206 impl<'tcx> LateLintPass<'tcx> for StringLitAsBytes {
207     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
208         use crate::utils::{snippet, snippet_with_applicability};
209         use rustc_ast::LitKind;
210
211         if_chain! {
212             // Find std::str::converts::from_utf8
213             if let Some(args) = match_function_call(cx, e, &paths::STR_FROM_UTF8);
214
215             // Find string::as_bytes
216             if let ExprKind::AddrOf(BorrowKind::Ref, _, ref args) = args[0].kind;
217             if let ExprKind::Index(ref left, ref right) = args.kind;
218             let (method_names, expressions, _) = method_calls(left, 1);
219             if method_names.len() == 1;
220             if expressions.len() == 1;
221             if expressions[0].len() == 1;
222             if method_names[0] == sym!(as_bytes);
223
224             // Check for slicer
225             if let ExprKind::Struct(QPath::LangItem(LangItem::Range, _), _, _) = right.kind;
226
227             then {
228                 let mut applicability = Applicability::MachineApplicable;
229                 let string_expression = &expressions[0][0];
230
231                 let snippet_app = snippet_with_applicability(
232                     cx,
233                     string_expression.span, "..",
234                     &mut applicability,
235                 );
236
237                 span_lint_and_sugg(
238                     cx,
239                     STRING_FROM_UTF8_AS_BYTES,
240                     e.span,
241                     "calling a slice of `as_bytes()` with `from_utf8` should be not necessary",
242                     "try",
243                     format!("Some(&{}[{}])", snippet_app, snippet(cx, right.span, "..")),
244                     applicability
245                 )
246             }
247         }
248
249         if_chain! {
250             if let ExprKind::MethodCall(path, _, args, _) = &e.kind;
251             if path.ident.name == sym!(as_bytes);
252             if let ExprKind::Lit(lit) = &args[0].kind;
253             if let LitKind::Str(lit_content, _) = &lit.node;
254             then {
255                 let callsite = snippet(cx, args[0].span.source_callsite(), r#""foo""#);
256                 let mut applicability = Applicability::MachineApplicable;
257                 if callsite.starts_with("include_str!") {
258                     span_lint_and_sugg(
259                         cx,
260                         STRING_LIT_AS_BYTES,
261                         e.span,
262                         "calling `as_bytes()` on `include_str!(..)`",
263                         "consider using `include_bytes!(..)` instead",
264                         snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability).replacen(
265                             "include_str",
266                             "include_bytes",
267                             1,
268                         ),
269                         applicability,
270                     );
271                 } else if lit_content.as_str().is_ascii()
272                     && lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT
273                     && !args[0].span.from_expansion()
274                 {
275                     span_lint_and_sugg(
276                         cx,
277                         STRING_LIT_AS_BYTES,
278                         e.span,
279                         "calling `as_bytes()` on a string literal",
280                         "consider using a byte string literal instead",
281                         format!(
282                             "b{}",
283                             snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability)
284                         ),
285                         applicability,
286                     );
287                 }
288             }
289         }
290     }
291 }
292
293 declare_clippy_lint! {
294     /// **What it does:** This lint checks for `.to_string()` method calls on values of type `&str`.
295     ///
296     /// **Why is this bad?** The `to_string` method is also used on other types to convert them to a string.
297     /// When called on a `&str` it turns the `&str` into the owned variant `String`, which can be better
298     /// expressed with `.to_owned()`.
299     ///
300     /// **Known problems:** None.
301     ///
302     /// **Example:**
303     ///
304     /// ```rust
305     /// // example code where clippy issues a warning
306     /// let _ = "str".to_string();
307     /// ```
308     /// Use instead:
309     /// ```rust
310     /// // example code which does not raise clippy warning
311     /// let _ = "str".to_owned();
312     /// ```
313     pub STR_TO_STRING,
314     restriction,
315     "using `to_string()` on a `&str`, which should be `to_owned()`"
316 }
317
318 declare_lint_pass!(StrToString => [STR_TO_STRING]);
319
320 impl LateLintPass<'_> for StrToString {
321     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
322         if_chain! {
323             if let ExprKind::MethodCall(path, _, args, _) = &expr.kind;
324             if path.ident.name == sym!(to_string);
325             let ty = cx.typeck_results().expr_ty(&args[0]);
326             if let ty::Ref(_, ty, ..) = ty.kind();
327             if *ty.kind() == ty::Str;
328             then {
329                 span_lint_and_help(
330                     cx,
331                     STR_TO_STRING,
332                     expr.span,
333                     "`to_string()` called on a `&str`",
334                     None,
335                     "consider using `.to_owned()`",
336                 );
337             }
338         }
339     }
340 }
341
342 declare_clippy_lint! {
343     /// **What it does:** This lint checks for `.to_string()` method calls on values of type `String`.
344     ///
345     /// **Why is this bad?** The `to_string` method is also used on other types to convert them to a string.
346     /// When called on a `String` it only clones the `String`, which can be better expressed with `.clone()`.
347     /// **Known problems:** None.
348     ///
349     /// **Example:**
350     ///
351     /// ```rust
352     /// // example code where clippy issues a warning
353     /// let msg = String::from("Hello World");
354     /// let _ = msg.to_string();
355     /// ```
356     /// Use instead:
357     /// ```rust
358     /// // example code which does not raise clippy warning
359     /// let msg = String::from("Hello World");
360     /// let _ = msg.clone();
361     /// ```
362     pub STRING_TO_STRING,
363     restriction,
364     "using `to_string()` on a `String`, which should be `clone()`"
365 }
366
367 declare_lint_pass!(StringToString => [STRING_TO_STRING]);
368
369 impl LateLintPass<'_> for StringToString {
370     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
371         if_chain! {
372             if let ExprKind::MethodCall(path, _, args, _) = &expr.kind;
373             if path.ident.name == sym!(to_string);
374             let ty = cx.typeck_results().expr_ty(&args[0]);
375             if is_type_diagnostic_item(cx, ty, sym::string_type);
376             then {
377                 span_lint_and_help(
378                     cx,
379                     STRING_TO_STRING,
380                     expr.span,
381                     "`to_string()` called on a `String`",
382                     None,
383                     "consider using `.clone()`",
384                 );
385             }
386         }
387     }
388 }