1 | //! A type that represents the union of a set of regular expressions. |
2 | #![deny (clippy::missing_docs_in_private_items)] |
3 | |
4 | use regex::RegexSet as RxSet; |
5 | use std::cell::Cell; |
6 | |
7 | /// A dynamic set of regular expressions. |
8 | #[derive (Clone, Debug, Default)] |
9 | pub 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 | |
19 | impl 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" )] |
144 | fn 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 | |