Skip to content

Commit 63c6dac

Browse files
committed
Implement lint for regex::Regex compilation inside a loop
1 parent 903293b commit 63c6dac

File tree

5 files changed

+172
-7
lines changed

5 files changed

+172
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5873,6 +5873,7 @@ Released 2018-09-13
58735873
[`ref_in_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_in_deref
58745874
[`ref_option_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_option_ref
58755875
[`ref_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_patterns
5876+
[`regex_compile_in_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#regex_compile_in_loop
58765877
[`regex_macro`]: https://rust-lang.github.io/rust-clippy/master/index.html#regex_macro
58775878
[`renamed_function_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#renamed_function_params
58785879
[`repeat_once`]: https://rust-lang.github.io/rust-clippy/master/index.html#repeat_once

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
637637
crate::ref_patterns::REF_PATTERNS_INFO,
638638
crate::reference::DEREF_ADDROF_INFO,
639639
crate::regex::INVALID_REGEX_INFO,
640+
crate::regex::REGEX_COMPILE_IN_LOOP_INFO,
640641
crate::regex::TRIVIAL_REGEX_INFO,
641642
crate::repeat_vec_with_capacity::REPEAT_VEC_WITH_CAPACITY_INFO,
642643
crate::reserve_after_initialization::RESERVE_AFTER_INITIALIZATION_INFO,

clippy_lints/src/regex.rs

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use clippy_utils::source::SpanRangeExt;
66
use clippy_utils::{def_path_def_ids, path_def_id, paths};
77
use rustc_ast::ast::{LitKind, StrStyle};
88
use rustc_hir::def_id::DefIdMap;
9+
use rustc_hir::intravisit::{self, Visitor};
910
use rustc_hir::{BorrowKind, Expr, ExprKind};
1011
use rustc_lint::{LateContext, LateLintPass};
1112
use rustc_session::impl_lint_pass;
@@ -55,6 +56,42 @@ declare_clippy_lint! {
5556
"trivial regular expressions"
5657
}
5758

59+
declare_clippy_lint! {
60+
/// ### What it does
61+
///
62+
/// Checks for [regex](https://crates.io/crates/regex) compilation inside a loop with a literal.
63+
///
64+
/// ### Why is this bad?
65+
///
66+
/// Compiling a regex is a much more expensive operation than using one, and a compiled regex can be used multiple times.
67+
///
68+
/// ### Example
69+
/// ```no_run
70+
/// # let haystacks = [""];
71+
/// for haystack in haystacks {
72+
/// let regex = regex::Regex::new(MY_REGEX);
73+
/// if regex.is_match(heystack) {
74+
/// // Perform operation
75+
/// }
76+
/// }
77+
/// ```
78+
/// should be replaced with
79+
/// ```no_run
80+
/// # let haystacks = [""];
81+
/// let regex = regex::Regex::new(MY_REGEX);
82+
/// for haystack in haystacks {
83+
/// if regex.is_match(heystack) {
84+
/// // Perform operation
85+
/// }
86+
/// }
87+
/// ```
88+
///
89+
#[clippy::version = "1.83.0"]
90+
pub REGEX_COMPILE_IN_LOOP,
91+
perf,
92+
"regular expression compilation performed in a loop"
93+
}
94+
5895
#[derive(Copy, Clone)]
5996
enum RegexKind {
6097
Unicode,
@@ -68,7 +105,7 @@ pub struct Regex {
68105
definitions: DefIdMap<RegexKind>,
69106
}
70107

71-
impl_lint_pass!(Regex => [INVALID_REGEX, TRIVIAL_REGEX]);
108+
impl_lint_pass!(Regex => [INVALID_REGEX, TRIVIAL_REGEX, REGEX_COMPILE_IN_LOOP]);
72109

73110
impl<'tcx> LateLintPass<'tcx> for Regex {
74111
fn check_crate(&mut self, cx: &LateContext<'tcx>) {
@@ -92,17 +129,69 @@ impl<'tcx> LateLintPass<'tcx> for Regex {
92129
}
93130

94131
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
95-
if let ExprKind::Call(fun, [arg]) = expr.kind
96-
&& let Some(def_id) = path_def_id(cx, fun)
97-
&& let Some(regex_kind) = self.definitions.get(&def_id)
98-
{
132+
if let Some((regex_kind, _, arg)) = extract_regex_call(&self.definitions, cx, expr) {
99133
match regex_kind {
100134
RegexKind::Unicode => check_regex(cx, arg, true),
101135
RegexKind::UnicodeSet => check_set(cx, arg, true),
102136
RegexKind::Bytes => check_regex(cx, arg, false),
103137
RegexKind::BytesSet => check_set(cx, arg, false),
104138
}
105139
}
140+
141+
if let ExprKind::Loop(block, _, _, span) = expr.kind {
142+
let mut visitor = RegexCompVisitor {
143+
cx,
144+
loop_span: span,
145+
definitions: &self.definitions,
146+
};
147+
148+
visitor.visit_block(block);
149+
}
150+
}
151+
}
152+
153+
struct RegexCompVisitor<'pass, 'tcx> {
154+
definitions: &'pass DefIdMap<RegexKind>,
155+
cx: &'pass LateContext<'tcx>,
156+
loop_span: Span,
157+
}
158+
159+
impl<'pass, 'tcx> Visitor<'tcx> for RegexCompVisitor<'pass, 'tcx> {
160+
type NestedFilter = intravisit::nested_filter::None;
161+
162+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
163+
if let Some((_, fun, arg)) = extract_regex_call(self.definitions, self.cx, expr)
164+
&& (matches!(arg.kind, ExprKind::Lit(_)) || const_str(self.cx, arg).is_some())
165+
{
166+
span_lint_and_help(
167+
self.cx,
168+
REGEX_COMPILE_IN_LOOP,
169+
fun.span,
170+
"compiling a regex in a loop",
171+
Some(self.loop_span),
172+
"move the regex construction outside this loop",
173+
);
174+
}
175+
176+
// Avoid recursing into loops, as the LateLintPass::visit_expr will do this already.
177+
if !matches!(expr.kind, ExprKind::Loop(..)) {
178+
intravisit::walk_expr(self, expr);
179+
}
180+
}
181+
}
182+
183+
fn extract_regex_call<'tcx>(
184+
definitions: &DefIdMap<RegexKind>,
185+
cx: &LateContext<'tcx>,
186+
expr: &'tcx Expr<'tcx>,
187+
) -> Option<(RegexKind, &'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> {
188+
if let ExprKind::Call(fun, [arg]) = expr.kind
189+
&& let Some(def_id) = path_def_id(cx, fun)
190+
&& let Some(regex_kind) = definitions.get(&def_id)
191+
{
192+
Some((*regex_kind, fun, arg))
193+
} else {
194+
None
106195
}
107196
}
108197

tests/ui/regex.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
clippy::needless_borrow,
66
clippy::needless_borrows_for_generic_args
77
)]
8-
#![warn(clippy::invalid_regex, clippy::trivial_regex)]
8+
#![warn(clippy::invalid_regex, clippy::trivial_regex, clippy::regex_compile_in_loop)]
99

1010
extern crate regex;
1111

@@ -118,7 +118,31 @@ fn trivial_regex() {
118118
let _ = BRegex::new(r"\b{start}word\b{end}");
119119
}
120120

121+
fn regex_compile_in_loop() {
122+
loop {
123+
let regex = Regex::new("a.b");
124+
//~^ ERROR: compiling a regex in a loop
125+
let regex = BRegex::new("a.b");
126+
//~^ ERROR: compiling a regex in a loop
127+
128+
if true {
129+
let regex = Regex::new("a.b");
130+
//~^ ERROR: compiling a regex in a loop
131+
}
132+
133+
for _ in 0..10 {
134+
let nested_regex = Regex::new("a.b");
135+
//~^ ERROR: compiling a regex in a loop
136+
}
137+
}
138+
139+
for i in 0..10 {
140+
let dependant_regex = Regex::new(&format!("{i}"));
141+
}
142+
}
143+
121144
fn main() {
122145
syntax_error();
123146
trivial_regex();
147+
regex_compile_in_loop();
124148
}

tests/ui/regex.stderr

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,5 +195,55 @@ LL | let binary_trivial_empty = BRegex::new("^$");
195195
|
196196
= help: consider using `str::is_empty`
197197

198-
error: aborting due to 24 previous errors
198+
error: compiling a regex in a loop
199+
--> tests/ui/regex.rs:123:21
200+
|
201+
LL | let regex = Regex::new("a.b");
202+
| ^^^^^^^^^^
203+
|
204+
help: move the regex construction outside this loop
205+
--> tests/ui/regex.rs:122:5
206+
|
207+
LL | loop {
208+
| ^^^^
209+
= note: `-D clippy::regex-compile-in-loop` implied by `-D warnings`
210+
= help: to override `-D warnings` add `#[allow(clippy::regex_compile_in_loop)]`
211+
212+
error: compiling a regex in a loop
213+
--> tests/ui/regex.rs:125:21
214+
|
215+
LL | let regex = BRegex::new("a.b");
216+
| ^^^^^^^^^^^
217+
|
218+
help: move the regex construction outside this loop
219+
--> tests/ui/regex.rs:122:5
220+
|
221+
LL | loop {
222+
| ^^^^
223+
224+
error: compiling a regex in a loop
225+
--> tests/ui/regex.rs:129:25
226+
|
227+
LL | let regex = Regex::new("a.b");
228+
| ^^^^^^^^^^
229+
|
230+
help: move the regex construction outside this loop
231+
--> tests/ui/regex.rs:122:5
232+
|
233+
LL | loop {
234+
| ^^^^
235+
236+
error: compiling a regex in a loop
237+
--> tests/ui/regex.rs:134:32
238+
|
239+
LL | let nested_regex = Regex::new("a.b");
240+
| ^^^^^^^^^^
241+
|
242+
help: move the regex construction outside this loop
243+
--> tests/ui/regex.rs:133:9
244+
|
245+
LL | for _ in 0..10 {
246+
| ^^^^^^^^^^^^^^
247+
248+
error: aborting due to 28 previous errors
199249

0 commit comments

Comments
 (0)