]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/regex.rs
Merge pull request #3294 from mikerite/fix-3276
[rust.git] / clippy_lints / src / regex.rs
1 // Copyright 2014-2018 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution.
3 //
4 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7 // option. This file may not be copied, modified, or distributed
8 // except according to those terms.
9
10
11 use regex_syntax;
12 use crate::rustc::hir::*;
13 use crate::rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
14 use crate::rustc::{declare_tool_lint, lint_array};
15 use crate::rustc_data_structures::fx::FxHashSet;
16 use if_chain::if_chain;
17 use crate::syntax::ast::{LitKind, NodeId, StrStyle};
18 use crate::syntax::source_map::{BytePos, Span};
19 use crate::utils::{is_expn_of, match_def_path, match_type, opt_def_id, paths, span_help_and_lint, span_lint};
20 use crate::consts::{constant, Constant};
21 use std::convert::TryFrom;
22
23 /// **What it does:** Checks [regex](https://crates.io/crates/regex) creation
24 /// (with `Regex::new`,`RegexBuilder::new` or `RegexSet::new`) for correct
25 /// regex syntax.
26 ///
27 /// **Why is this bad?** This will lead to a runtime panic.
28 ///
29 /// **Known problems:** None.
30 ///
31 /// **Example:**
32 /// ```rust
33 /// Regex::new("|")
34 /// ```
35 declare_clippy_lint! {
36     pub INVALID_REGEX,
37     correctness,
38     "invalid regular expressions"
39 }
40
41 /// **What it does:** Checks for trivial [regex](https://crates.io/crates/regex)
42 /// creation (with `Regex::new`, `RegexBuilder::new` or `RegexSet::new`).
43 ///
44 /// **Why is this bad?** Matching the regex can likely be replaced by `==` or
45 /// `str::starts_with`, `str::ends_with` or `std::contains` or other `str`
46 /// methods.
47 ///
48 /// **Known problems:** None.
49 ///
50 /// **Example:**
51 /// ```rust
52 /// Regex::new("^foobar")
53 /// ```
54 declare_clippy_lint! {
55     pub TRIVIAL_REGEX,
56     style,
57     "trivial regular expressions"
58 }
59
60 /// **What it does:** Checks for usage of `regex!(_)` which (as of now) is
61 /// usually slower than `Regex::new(_)` unless called in a loop (which is a bad
62 /// idea anyway).
63 ///
64 /// **Why is this bad?** Performance, at least for now. The macro version is
65 /// likely to catch up long-term, but for now the dynamic version is faster.
66 ///
67 /// **Known problems:** None.
68 ///
69 /// **Example:**
70 /// ```rust
71 /// regex!("foo|bar")
72 /// ```
73 declare_clippy_lint! {
74     pub REGEX_MACRO,
75     style,
76     "use of `regex!(_)` instead of `Regex::new(_)`"
77 }
78
79 #[derive(Clone, Default)]
80 pub struct Pass {
81     spans: FxHashSet<Span>,
82     last: Option<NodeId>,
83 }
84
85 impl LintPass for Pass {
86     fn get_lints(&self) -> LintArray {
87         lint_array!(INVALID_REGEX, REGEX_MACRO, TRIVIAL_REGEX)
88     }
89 }
90
91 impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
92     fn check_crate(&mut self, _: &LateContext<'a, 'tcx>, _: &'tcx Crate) {
93         self.spans.clear();
94     }
95
96     fn check_block(&mut self, cx: &LateContext<'a, 'tcx>, block: &'tcx Block) {
97         if_chain! {
98             if self.last.is_none();
99             if let Some(ref expr) = block.expr;
100             if match_type(cx, cx.tables.expr_ty(expr), &paths::REGEX);
101             if let Some(span) = is_expn_of(expr.span, "regex");
102             then {
103                 if !self.spans.contains(&span) {
104                     span_lint(cx,
105                               REGEX_MACRO,
106                               span,
107                               "`regex!(_)` found. \
108                               Please use `Regex::new(_)`, which is faster for now.");
109                     self.spans.insert(span);
110                 }
111                 self.last = Some(block.id);
112             }
113         }
114     }
115
116     fn check_block_post(&mut self, _: &LateContext<'a, 'tcx>, block: &'tcx Block) {
117         if self.last.map_or(false, |id| block.id == id) {
118             self.last = None;
119         }
120     }
121
122     fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
123         if_chain! {
124             if let ExprKind::Call(ref fun, ref args) = expr.node;
125             if let ExprKind::Path(ref qpath) = fun.node;
126             if args.len() == 1;
127             if let Some(def_id) = opt_def_id(cx.tables.qpath_def(qpath, fun.hir_id));
128             then {
129                 if match_def_path(cx.tcx, def_id, &paths::REGEX_NEW) ||
130                    match_def_path(cx.tcx, def_id, &paths::REGEX_BUILDER_NEW) {
131                     check_regex(cx, &args[0], true);
132                 } else if match_def_path(cx.tcx, def_id, &paths::REGEX_BYTES_NEW) ||
133                    match_def_path(cx.tcx, def_id, &paths::REGEX_BYTES_BUILDER_NEW) {
134                     check_regex(cx, &args[0], false);
135                 } else if match_def_path(cx.tcx, def_id, &paths::REGEX_SET_NEW) {
136                     check_set(cx, &args[0], true);
137                 } else if match_def_path(cx.tcx, def_id, &paths::REGEX_BYTES_SET_NEW) {
138                     check_set(cx, &args[0], false);
139                 }
140             }
141         }
142     }
143 }
144
145 #[allow(clippy::cast_possible_truncation)] // truncation very unlikely here
146 fn str_span(base: Span, c: regex_syntax::ast::Span, offset: u16) -> Span {
147     let offset = u32::from(offset);
148     let end = base.lo() + BytePos(u32::try_from(c.end.offset).expect("offset too large") + offset);
149     let start = base.lo() + BytePos(u32::try_from(c.start.offset).expect("offset too large") + offset);
150     assert!(start <= end);
151     Span::new(start, end, base.ctxt())
152 }
153
154 fn const_str<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, e: &'tcx Expr) -> Option<String> {
155     constant(cx, cx.tables, e).and_then(|(c, _)| match c {
156         Constant::Str(s) => Some(s),
157         _ => None,
158     })
159 }
160
161 fn is_trivial_regex(s: &regex_syntax::hir::Hir) -> Option<&'static str> {
162     use regex_syntax::hir::HirKind::*;
163     use regex_syntax::hir::Anchor::*;
164
165     let is_literal = |e: &[regex_syntax::hir::Hir]| e.iter().all(|e| match *e.kind() {
166         Literal(_) => true,
167         _ => false,
168     });
169
170     match *s.kind() {
171         Empty |
172         Anchor(_) => Some("the regex is unlikely to be useful as it is"),
173         Literal(_) => Some("consider using `str::contains`"),
174         Alternation(ref exprs) => if exprs.iter().all(|e| e.kind().is_empty()) {
175             Some("the regex is unlikely to be useful as it is")
176         } else {
177             None
178         },
179         Concat(ref exprs) => match (exprs[0].kind(), exprs[exprs.len() - 1].kind()) {
180             (&Anchor(StartText), &Anchor(EndText)) if exprs[1..(exprs.len() - 1)].is_empty() => Some("consider using `str::is_empty`"),
181             (&Anchor(StartText), &Anchor(EndText)) if is_literal(&exprs[1..(exprs.len() - 1)]) => Some("consider using `==` on `str`s"),
182             (&Anchor(StartText), &Literal(_)) if is_literal(&exprs[1..]) => Some("consider using `str::starts_with`"),
183             (&Literal(_), &Anchor(EndText)) if is_literal(&exprs[1..(exprs.len() - 1)]) => Some("consider using `str::ends_with`"),
184             _ if is_literal(exprs) => Some("consider using `str::contains`"),
185             _ => None,
186         },
187         _ => None,
188     }
189 }
190
191 fn check_set<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr, utf8: bool) {
192     if_chain! {
193         if let ExprKind::AddrOf(_, ref expr) = expr.node;
194         if let ExprKind::Array(ref exprs) = expr.node;
195         then {
196             for expr in exprs {
197                 check_regex(cx, expr, utf8);
198             }
199         }
200     }
201 }
202
203 fn check_regex<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr, utf8: bool) {
204     let mut parser = regex_syntax::ParserBuilder::new()
205         .unicode(utf8)
206         .allow_invalid_utf8(!utf8)
207         .build();
208
209     if let ExprKind::Lit(ref lit) = expr.node {
210         if let LitKind::Str(ref r, style) = lit.node {
211             let r = &r.as_str();
212             let offset = if let StrStyle::Raw(n) = style { 2 + n } else { 1 };
213             match parser.parse(r) {
214                 Ok(r) => if let Some(repl) = is_trivial_regex(&r) {
215                     span_help_and_lint(
216                         cx,
217                         TRIVIAL_REGEX,
218                         expr.span,
219                         "trivial regex",
220                         repl,
221                     );
222                 },
223                 Err(regex_syntax::Error::Parse(e)) => {
224                     span_lint(
225                         cx,
226                         INVALID_REGEX,
227                         str_span(expr.span, *e.span(), offset),
228                         &format!("regex syntax error: {}", e.kind()),
229                     );
230                 },
231                 Err(regex_syntax::Error::Translate(e)) => {
232                     span_lint(
233                         cx,
234                         INVALID_REGEX,
235                         str_span(expr.span, *e.span(), offset),
236                         &format!("regex syntax error: {}", e.kind()),
237                     );
238                 },
239                 Err(e) => {
240                     span_lint(
241                         cx,
242                         INVALID_REGEX,
243                         expr.span,
244                         &format!("regex syntax error: {}", e),
245                     );
246                 },
247             }
248         }
249     } else if let Some(r) = const_str(cx, expr) {
250         match parser.parse(&r) {
251             Ok(r) => if let Some(repl) = is_trivial_regex(&r) {
252                 span_help_and_lint(
253                     cx,
254                     TRIVIAL_REGEX,
255                     expr.span,
256                     "trivial regex",
257                     repl,
258                 );
259             },
260             Err(regex_syntax::Error::Parse(e)) => {
261                 span_lint(
262                     cx,
263                     INVALID_REGEX,
264                     expr.span,
265                     &format!("regex syntax error on position {}: {}", e.span().start.offset, e.kind()),
266                 );
267             },
268             Err(regex_syntax::Error::Translate(e)) => {
269                 span_lint(
270                     cx,
271                     INVALID_REGEX,
272                     expr.span,
273                     &format!("regex syntax error on position {}: {}", e.span().start.offset, e.kind()),
274                 );
275             },
276             Err(e) => {
277                 span_lint(
278                     cx,
279                     INVALID_REGEX,
280                     expr.span,
281                     &format!("regex syntax error: {}", e),
282                 );
283             },
284         }
285     }
286 }