bindgen/
regex_set.rs

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