//! A type that represents the union of a set of regular expressions. #![deny(clippy::missing_docs_in_private_items)] use regex::RegexSet as RxSet; use std::cell::Cell; /// A dynamic set of regular expressions. #[derive(Clone, Debug, Default)] pub struct RegexSet { items: Vec>, /// Whether any of the items in the set was ever matched. The length of this /// vector is exactly the length of `items`. matched: Vec>, set: Option, /// Whether we should record matching items in the `matched` vector or not. record_matches: bool, } impl RegexSet { /// Create a new RegexSet pub fn new() -> RegexSet { RegexSet::default() } /// Is this set empty? pub fn is_empty(&self) -> bool { self.items.is_empty() } /// Insert a new regex into this set. pub fn insert(&mut self, string: S) where S: AsRef, { self.items.push(string.as_ref().to_owned().into_boxed_str()); self.matched.push(Cell::new(false)); self.set = None; } /// Returns slice of String from its field 'items' pub fn get_items(&self) -> &[Box] { &self.items } /// Returns an iterator over regexes in the set which didn't match any /// strings yet. pub fn unmatched_items(&self) -> impl Iterator { self.items.iter().enumerate().filter_map(move |(i, item)| { if !self.record_matches || self.matched[i].get() { return None; } Some(item.as_ref()) }) } /// Construct a RegexSet from the set of entries we've accumulated. /// /// Must be called before calling `matches()`, or it will always return /// false. #[inline] pub fn build(&mut self, record_matches: bool) { self.build_inner(record_matches, None) } #[cfg(all(feature = "__cli", feature = "experimental"))] /// Construct a RegexSet from the set of entries we've accumulated and emit diagnostics if the /// name of the regex set is passed to it. /// /// Must be called before calling `matches()`, or it will always return /// false. #[inline] pub fn build_with_diagnostics( &mut self, record_matches: bool, name: Option<&'static str>, ) { self.build_inner(record_matches, name) } #[cfg(all(not(feature = "__cli"), feature = "experimental"))] /// Construct a RegexSet from the set of entries we've accumulated and emit diagnostics if the /// name of the regex set is passed to it. /// /// Must be called before calling `matches()`, or it will always return /// false. #[inline] pub(crate) fn build_with_diagnostics( &mut self, record_matches: bool, name: Option<&'static str>, ) { self.build_inner(record_matches, name) } fn build_inner( &mut self, record_matches: bool, _name: Option<&'static str>, ) { let items = self.items.iter().map(|item| format!("^({})$", item)); self.record_matches = record_matches; self.set = match RxSet::new(items) { Ok(x) => Some(x), Err(e) => { warn!("Invalid regex in {:?}: {:?}", self.items, e); #[cfg(feature = "experimental")] if let Some(name) = _name { invalid_regex_warning(self, e, name); } None } } } /// Does the given `string` match any of the regexes in this set? pub fn matches(&self, string: S) -> bool where S: AsRef, { let s = string.as_ref(); let set = match self.set { Some(ref set) => set, None => return false, }; if !self.record_matches { return set.is_match(s); } let matches = set.matches(s); if !matches.matched_any() { return false; } for i in matches.iter() { self.matched[i].set(true); } true } } #[cfg(feature = "experimental")] fn invalid_regex_warning( set: &RegexSet, err: regex::Error, name: &'static str, ) { use crate::diagnostics::{Diagnostic, Level, Slice}; let mut diagnostic = Diagnostic::default(); match err { regex::Error::Syntax(string) => { if string.starts_with("regex parse error:\n") { let mut source = String::new(); let mut parsing_source = true; for line in string.lines().skip(1) { if parsing_source { if line.starts_with(' ') { source.push_str(line); source.push('\n'); continue; } parsing_source = false; } let error = "error: "; if line.starts_with(error) { let (_, msg) = line.split_at(error.len()); diagnostic.add_annotation(msg.to_owned(), Level::Error); } else { diagnostic.add_annotation(line.to_owned(), Level::Info); } } let mut slice = Slice::default(); slice.with_source(source); diagnostic.add_slice(slice); diagnostic.with_title( "Error while parsing a regular expression.", Level::Warn, ); } else { diagnostic.with_title(string, Level::Warn); } } err => { let err = err.to_string(); diagnostic.with_title(err, Level::Warn); } } diagnostic.add_annotation( format!("This regular expression was passed via `{}`.", name), Level::Note, ); if set.items.iter().any(|item| item.as_ref() == "*") { diagnostic.add_annotation("Wildcard patterns \"*\" are no longer considered valid. Use \".*\" instead.", Level::Help); } diagnostic.display(); }