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
new() -> RegexSet21     pub fn new() -> RegexSet {
22         RegexSet::default()
23     }
24 
25     /// Is this set empty?
is_empty(&self) -> bool26     pub fn is_empty(&self) -> bool {
27         self.items.is_empty()
28     }
29 
30     /// Insert a new regex into this set.
insert<S>(&mut self, string: S) where S: AsRef<str>,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'
get_items(&self) -> &[Box<str>]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.
unmatched_items(&self) -> impl Iterator<Item = &str>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]
build(&mut self, record_matches: bool)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]
build_with_diagnostics( &mut self, record_matches: bool, name: Option<&'static str>, )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]
build_with_diagnostics( &mut self, record_matches: bool, name: Option<&'static str>, )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 
build_inner( &mut self, record_matches: bool, _name: Option<&'static str>, )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?
matches<S>(&self, string: S) -> bool where S: AsRef<str>,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")]
invalid_regex_warning( set: &RegexSet, err: regex::Error, name: &'static str, )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