]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/strings.rs
0dd2da949c4c33e5d8105148abda3f98430465b0
[rust.git] / src / tools / clippy / clippy_lints / src / strings.rs
1 use rustc_errors::Applicability;
2 use rustc_hir::{BinOpKind, Expr, ExprKind};
3 use rustc_lint::{LateContext, LateLintPass, LintContext};
4 use rustc_middle::lint::in_external_macro;
5 use rustc_session::{declare_lint_pass, declare_tool_lint};
6 use rustc_span::source_map::Spanned;
7 use rustc_span::sym;
8
9 use if_chain::if_chain;
10
11 use crate::utils::SpanlessEq;
12 use crate::utils::{get_parent_expr, is_allowed, is_type_diagnostic_item, span_lint, span_lint_and_sugg};
13
14 declare_clippy_lint! {
15     /// **What it does:** Checks for string appends of the form `x = x + y` (without
16     /// `let`!).
17     ///
18     /// **Why is this bad?** It's not really bad, but some people think that the
19     /// `.push_str(_)` method is more readable.
20     ///
21     /// **Known problems:** None.
22     ///
23     /// **Example:**
24     ///
25     /// ```rust
26     /// let mut x = "Hello".to_owned();
27     /// x = x + ", World";
28     ///
29     /// // More readable
30     /// x += ", World";
31     /// x.push_str(", World");
32     /// ```
33     pub STRING_ADD_ASSIGN,
34     pedantic,
35     "using `x = x + ..` where x is a `String` instead of `push_str()`"
36 }
37
38 declare_clippy_lint! {
39     /// **What it does:** Checks for all instances of `x + _` where `x` is of type
40     /// `String`, but only if [`string_add_assign`](#string_add_assign) does *not*
41     /// match.
42     ///
43     /// **Why is this bad?** It's not bad in and of itself. However, this particular
44     /// `Add` implementation is asymmetric (the other operand need not be `String`,
45     /// but `x` does), while addition as mathematically defined is symmetric, also
46     /// the `String::push_str(_)` function is a perfectly good replacement.
47     /// Therefore, some dislike it and wish not to have it in their code.
48     ///
49     /// That said, other people think that string addition, having a long tradition
50     /// in other languages is actually fine, which is why we decided to make this
51     /// particular lint `allow` by default.
52     ///
53     /// **Known problems:** None.
54     ///
55     /// **Example:**
56     ///
57     /// ```rust
58     /// let x = "Hello".to_owned();
59     /// x + ", World";
60     /// ```
61     pub STRING_ADD,
62     restriction,
63     "using `x + ..` where x is a `String` instead of `push_str()`"
64 }
65
66 declare_clippy_lint! {
67     /// **What it does:** Checks for the `as_bytes` method called on string literals
68     /// that contain only ASCII characters.
69     ///
70     /// **Why is this bad?** Byte string literals (e.g., `b"foo"`) can be used
71     /// instead. They are shorter but less discoverable than `as_bytes()`.
72     ///
73     /// **Known Problems:**
74     /// `"str".as_bytes()` and the suggested replacement of `b"str"` are not
75     /// equivalent because they have different types. The former is `&[u8]`
76     /// while the latter is `&[u8; 3]`. That means in general they will have a
77     /// different set of methods and different trait implementations.
78     ///
79     /// ```compile_fail
80     /// fn f(v: Vec<u8>) {}
81     ///
82     /// f("...".as_bytes().to_owned()); // works
83     /// f(b"...".to_owned()); // does not work, because arg is [u8; 3] not Vec<u8>
84     ///
85     /// fn g(r: impl std::io::Read) {}
86     ///
87     /// g("...".as_bytes()); // works
88     /// g(b"..."); // does not work
89     /// ```
90     ///
91     /// The actual equivalent of `"str".as_bytes()` with the same type is not
92     /// `b"str"` but `&b"str"[..]`, which is a great deal of punctuation and not
93     /// more readable than a function call.
94     ///
95     /// **Example:**
96     /// ```rust
97     /// // Bad
98     /// let bs = "a byte string".as_bytes();
99     ///
100     /// // Good
101     /// let bs = b"a byte string";
102     /// ```
103     pub STRING_LIT_AS_BYTES,
104     nursery,
105     "calling `as_bytes` on a string literal instead of using a byte string literal"
106 }
107
108 declare_lint_pass!(StringAdd => [STRING_ADD, STRING_ADD_ASSIGN]);
109
110 impl<'tcx> LateLintPass<'tcx> for StringAdd {
111     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
112         if in_external_macro(cx.sess(), e.span) {
113             return;
114         }
115
116         if let ExprKind::Binary(
117             Spanned {
118                 node: BinOpKind::Add, ..
119             },
120             ref left,
121             _,
122         ) = e.kind
123         {
124             if is_string(cx, left) {
125                 if !is_allowed(cx, STRING_ADD_ASSIGN, e.hir_id) {
126                     let parent = get_parent_expr(cx, e);
127                     if let Some(p) = parent {
128                         if let ExprKind::Assign(ref target, _, _) = p.kind {
129                             // avoid duplicate matches
130                             if SpanlessEq::new(cx).eq_expr(target, left) {
131                                 return;
132                             }
133                         }
134                     }
135                 }
136                 span_lint(
137                     cx,
138                     STRING_ADD,
139                     e.span,
140                     "you added something to a string. Consider using `String::push_str()` instead",
141                 );
142             }
143         } else if let ExprKind::Assign(ref target, ref src, _) = e.kind {
144             if is_string(cx, target) && is_add(cx, src, target) {
145                 span_lint(
146                     cx,
147                     STRING_ADD_ASSIGN,
148                     e.span,
149                     "you assigned the result of adding something to this string. Consider using \
150                      `String::push_str()` instead",
151                 );
152             }
153         }
154     }
155 }
156
157 fn is_string(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
158     is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(e).peel_refs(), sym::string_type)
159 }
160
161 fn is_add(cx: &LateContext<'_>, src: &Expr<'_>, target: &Expr<'_>) -> bool {
162     match src.kind {
163         ExprKind::Binary(
164             Spanned {
165                 node: BinOpKind::Add, ..
166             },
167             ref left,
168             _,
169         ) => SpanlessEq::new(cx).eq_expr(target, left),
170         ExprKind::Block(ref block, _) => {
171             block.stmts.is_empty() && block.expr.as_ref().map_or(false, |expr| is_add(cx, expr, target))
172         },
173         _ => false,
174     }
175 }
176
177 // Max length a b"foo" string can take
178 const MAX_LENGTH_BYTE_STRING_LIT: usize = 32;
179
180 declare_lint_pass!(StringLitAsBytes => [STRING_LIT_AS_BYTES]);
181
182 impl<'tcx> LateLintPass<'tcx> for StringLitAsBytes {
183     fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
184         use crate::utils::{snippet, snippet_with_applicability};
185         use rustc_ast::LitKind;
186
187         if_chain! {
188             if let ExprKind::MethodCall(path, _, args, _) = &e.kind;
189             if path.ident.name == sym!(as_bytes);
190             if let ExprKind::Lit(lit) = &args[0].kind;
191             if let LitKind::Str(lit_content, _) = &lit.node;
192             then {
193                 let callsite = snippet(cx, args[0].span.source_callsite(), r#""foo""#);
194                 let mut applicability = Applicability::MachineApplicable;
195                 if callsite.starts_with("include_str!") {
196                     span_lint_and_sugg(
197                         cx,
198                         STRING_LIT_AS_BYTES,
199                         e.span,
200                         "calling `as_bytes()` on `include_str!(..)`",
201                         "consider using `include_bytes!(..)` instead",
202                         snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability).replacen(
203                             "include_str",
204                             "include_bytes",
205                             1,
206                         ),
207                         applicability,
208                     );
209                 } else if lit_content.as_str().is_ascii()
210                     && lit_content.as_str().len() <= MAX_LENGTH_BYTE_STRING_LIT
211                     && !args[0].span.from_expansion()
212                 {
213                     span_lint_and_sugg(
214                         cx,
215                         STRING_LIT_AS_BYTES,
216                         e.span,
217                         "calling `as_bytes()` on a string literal",
218                         "consider using a byte string literal instead",
219                         format!(
220                             "b{}",
221                             snippet_with_applicability(cx, args[0].span, r#""foo""#, &mut applicability)
222                         ),
223                         applicability,
224                     );
225                 }
226             }
227         }
228     }
229 }