1//! A type that represents the union of a set of regular expressions.
2#![deny(clippy::missing_docs_in_private_items)]
3
4use regex::RegexSet as RxSet;
5use std::cell::Cell;
6
7/// A dynamic set of regular expressions.
8#[derive(Clone, Debug, Default)]
9pub struct RegexSet {
10 items: Vec<Box<str>>,
11 /// Whether any of the items in the set was ever matched. The length of this
12 /// vector is exactly the length of `items`.
13 matched: Vec<Cell<bool>>,
14 set: Option<RxSet>,
15 /// Whether we should record matching items in the `matched` vector or not.
16 record_matches: bool,
17}
18
19impl RegexSet {
20 /// Create a new RegexSet
21 pub fn new() -> RegexSet {
22 RegexSet::default()
23 }
24
25 /// Is this set empty?
26 pub fn is_empty(&self) -> bool {
27 self.items.is_empty()
28 }
29
30 /// Insert a new regex into this set.
31 pub fn insert<S>(&mut self, string: S)
32 where
33 S: AsRef<str>,
34 {
35 self.items.push(string.as_ref().to_owned().into_boxed_str());
36 self.matched.push(Cell::new(false));
37 self.set = None;
38 }
39
40 /// Returns slice of String from its field 'items'
41 pub fn get_items(&self) -> &[Box<str>] {
42 &self.items
43 }
44
45 /// Returns an iterator over regexes in the set which didn't match any
46 /// strings yet.
47 pub fn unmatched_items(&self) -> impl Iterator<Item = &str> {
48 self.items.iter().enumerate().filter_map(move |(i, item)| {
49 if !self.record_matches || self.matched[i].get() {
50 return None;
51 }
52
53 Some(item.as_ref())
54 })
55 }
56
57 /// Construct a RegexSet from the set of entries we've accumulated.
58 ///
59 /// Must be called before calling `matches()`, or it will always return
60 /// false.
61 #[inline]
62 pub fn build(&mut self, record_matches: bool) {
63 self.build_inner(record_matches, None)
64 }
65
66 #[cfg(all(feature = "__cli", feature = "experimental"))]
67 /// Construct a RegexSet from the set of entries we've accumulated and emit diagnostics if the
68 /// name of the regex set is passed to it.
69 ///
70 /// Must be called before calling `matches()`, or it will always return
71 /// false.
72 #[inline]
73 pub fn build_with_diagnostics(
74 &mut self,
75 record_matches: bool,
76 name: Option<&'static str>,
77 ) {
78 self.build_inner(record_matches, name)
79 }
80
81 #[cfg(all(not(feature = "__cli"), feature = "experimental"))]
82 /// Construct a RegexSet from the set of entries we've accumulated and emit diagnostics if the
83 /// name of the regex set is passed to it.
84 ///
85 /// Must be called before calling `matches()`, or it will always return
86 /// false.
87 #[inline]
88 pub(crate) fn build_with_diagnostics(
89 &mut self,
90 record_matches: bool,
91 name: Option<&'static str>,
92 ) {
93 self.build_inner(record_matches, name)
94 }
95
96 fn build_inner(
97 &mut self,
98 record_matches: bool,
99 _name: Option<&'static str>,
100 ) {
101 let items = self.items.iter().map(|item| format!("^({})$", item));
102 self.record_matches = record_matches;
103 self.set = match RxSet::new(items) {
104 Ok(x) => Some(x),
105 Err(e) => {
106 warn!("Invalid regex in {:?}: {:?}", self.items, e);
107 #[cfg(feature = "experimental")]
108 if let Some(name) = _name {
109 invalid_regex_warning(self, e, name);
110 }
111 None
112 }
113 }
114 }
115
116 /// Does the given `string` match any of the regexes in this set?
117 pub fn matches<S>(&self, string: S) -> bool
118 where
119 S: AsRef<str>,
120 {
121 let s = string.as_ref();
122 let set = match self.set {
123 Some(ref set) => set,
124 None => return false,
125 };
126
127 if !self.record_matches {
128 return set.is_match(s);
129 }
130
131 let matches = set.matches(s);
132 if !matches.matched_any() {
133 return false;
134 }
135 for i in matches.iter() {
136 self.matched[i].set(true);
137 }
138
139 true
140 }
141}
142
143#[cfg(feature = "experimental")]
144fn invalid_regex_warning(
145 set: &RegexSet,
146 err: regex::Error,
147 name: &'static str,
148) {
149 use crate::diagnostics::{Diagnostic, Level, Slice};
150
151 let mut diagnostic = Diagnostic::default();
152
153 match err {
154 regex::Error::Syntax(string) => {
155 if string.starts_with("regex parse error:\n") {
156 let mut source = String::new();
157
158 let mut parsing_source = true;
159
160 for line in string.lines().skip(1) {
161 if parsing_source {
162 if line.starts_with(' ') {
163 source.push_str(line);
164 source.push('\n');
165 continue;
166 }
167 parsing_source = false;
168 }
169 let error = "error: ";
170 if line.starts_with(error) {
171 let (_, msg) = line.split_at(error.len());
172 diagnostic.add_annotation(msg.to_owned(), Level::Error);
173 } else {
174 diagnostic.add_annotation(line.to_owned(), Level::Info);
175 }
176 }
177 let mut slice = Slice::default();
178 slice.with_source(source);
179 diagnostic.add_slice(slice);
180
181 diagnostic.with_title(
182 "Error while parsing a regular expression.",
183 Level::Warn,
184 );
185 } else {
186 diagnostic.with_title(string, Level::Warn);
187 }
188 }
189 err => {
190 let err = err.to_string();
191 diagnostic.with_title(err, Level::Warn);
192 }
193 }
194
195 diagnostic.add_annotation(
196 format!("This regular expression was passed via `{}`.", name),
197 Level::Note,
198 );
199
200 if set.items.iter().any(|item| item.as_ref() == "*") {
201 diagnostic.add_annotation("Wildcard patterns \"*\" are no longer considered valid. Use \".*\" instead.", Level::Help);
202 }
203 diagnostic.display();
204}
205