Skip to main content

syntect/parsing/
regex.rs

1use once_cell::sync::OnceCell;
2use serde::de::{Deserialize, Deserializer};
3use serde::ser::{Serialize, Serializer};
4use std::error::Error;
5use std::{
6    collections::HashMap,
7    sync::{Arc, Weak},
8};
9
10/// An abstraction for regex patterns.
11///
12/// * Allows swapping out the regex implementation because it's only in this module.
13/// * Makes regexes serializable and deserializable using just the pattern string.
14/// * Lazily compiles regexes on first use to improve initialization time.
15#[derive(Debug)]
16pub struct Regex {
17    regex_str: String,
18    regex: Arc<OnceCell<regex_impl::Regex>>,
19    #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
20    engines: Option<fancy_regex::EngineCache>,
21}
22
23/// Exact-pattern interning without retaining compiled values.
24#[derive(Debug, Default)]
25pub(crate) struct RegexCache {
26    patterns: HashMap<String, Weak<OnceCell<regex_impl::Regex>>>,
27    #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
28    engines: fancy_regex::EngineCache,
29}
30
31impl RegexCache {
32    pub(crate) fn share(&mut self, regex: &mut Regex) {
33        #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
34        {
35            regex.engines = Some(self.engines.clone());
36        }
37        if let Some(cached) = self.patterns.get(&regex.regex_str).and_then(Weak::upgrade) {
38            regex.regex = cached;
39        } else {
40            self.patterns
41                .insert(regex.regex_str.clone(), Arc::downgrade(&regex.regex));
42        }
43    }
44}
45
46/// A region contains text positions for capture groups in a match result.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct Region {
49    region: regex_impl::Region,
50}
51
52pub(crate) type SearchScratch = regex_impl::SearchScratch;
53
54impl Regex {
55    #[cfg(all(feature = "owner-census", not(feature = "regex-onig")))]
56    pub(crate) fn visit_engines<'a>(
57        &'a self,
58        visit: impl FnMut(fancy_regex::owner_census::Engine<'a>),
59    ) {
60        if let Some(compiled) = self.regex.get() {
61            compiled.visit_engines(visit);
62        }
63    }
64
65    #[cfg(feature = "regex-census")]
66    pub(crate) fn compiled_owner(&self) -> Option<usize> {
67        self.regex.get().map(|_| Arc::as_ptr(&self.regex) as usize)
68    }
69
70    /// Create a new regex from the pattern string.
71    ///
72    /// Note that the regex compilation happens on first use, which is why this method does not
73    /// return a result.
74    pub fn new(regex_str: String) -> Self {
75        Self {
76            regex_str,
77            regex: Arc::new(OnceCell::new()),
78            #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
79            engines: None,
80        }
81    }
82
83    /// Check whether the pattern compiles as a valid regex or not.
84    pub fn try_compile(regex_str: &str) -> Option<Box<dyn Error + Send + Sync + 'static>> {
85        regex_impl::Regex::new(regex_str).err()
86    }
87
88    /// Return the regex pattern.
89    pub fn regex_str(&self) -> &str {
90        &self.regex_str
91    }
92
93    /// Check if the regex matches the given text.
94    pub fn is_match(&self, text: &str) -> bool {
95        self.regex().is_match(text)
96    }
97
98    /// Search for the pattern in the given text from begin/end positions.
99    ///
100    /// If a region is passed, it is used for storing match group positions. The argument allows
101    /// the [`Region`] to be reused between searches, which makes a significant performance
102    /// difference.
103    ///
104    /// [`Region`]: struct.Region.html
105    pub fn search(
106        &self,
107        text: &str,
108        begin: usize,
109        end: usize,
110        region: Option<&mut Region>,
111    ) -> bool {
112        self.regex()
113            .search(text, begin, end, region.map(|r| &mut r.region))
114    }
115
116    pub(crate) fn search_with_scratch(
117        &self,
118        text: &str,
119        begin: usize,
120        end: usize,
121        region: &mut Region,
122        scratch: &mut SearchScratch,
123    ) -> bool {
124        self.regex()
125            .search_with_scratch(text, begin, end, Some(&mut region.region), scratch)
126    }
127
128    pub(crate) fn possible_with_ascii_bytes(&self, present: u128) -> bool {
129        #[cfg(not(feature = "regex-onig"))]
130        {
131            self.regex().required_ascii_bytes() & !present == 0
132        }
133        #[cfg(feature = "regex-onig")]
134        {
135            let _ = present;
136            true
137        }
138    }
139
140    fn regex(&self) -> &regex_impl::Regex {
141        self.regex.get_or_init(|| {
142            #[cfg(feature = "syntax-timing")]
143            let _timing =
144                super::syntax_timing::Initializer::new(super::syntax_timing::Phase::OuterRegex);
145            #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
146            if let Some(cache) = &self.engines {
147                return regex_impl::Regex::new_cached(&self.regex_str, cache)
148                    .expect("regex string should be pre-tested");
149            }
150            regex_impl::Regex::new(&self.regex_str).expect("regex string should be pre-tested")
151        })
152    }
153}
154
155impl Clone for Regex {
156    fn clone(&self) -> Self {
157        Regex {
158            regex_str: self.regex_str.clone(),
159            regex: Arc::clone(&self.regex),
160            #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
161            engines: self.engines.clone(),
162        }
163    }
164}
165
166impl PartialEq for Regex {
167    fn eq(&self, other: &Regex) -> bool {
168        self.regex_str == other.regex_str
169    }
170}
171
172impl Eq for Regex {}
173
174impl Serialize for Regex {
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: Serializer,
178    {
179        serializer.serialize_str(&self.regex_str)
180    }
181}
182
183impl<'de> Deserialize<'de> for Regex {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        let regex_str = String::deserialize(deserializer)?;
189        Ok(Regex::new(regex_str))
190    }
191}
192
193impl Region {
194    pub(crate) fn owned_heap_bytes(&self) -> Option<usize> {
195        regex_impl::region_owned_heap_bytes(&self.region)
196    }
197
198    pub fn new() -> Self {
199        Self {
200            region: regex_impl::new_region(),
201        }
202    }
203
204    /// Get the start/end positions of the capture group with given index.
205    ///
206    /// If there is no match for that group or the index does not correspond to a group, `None` is
207    /// returned. The index 0 returns the whole match.
208    pub fn pos(&self, index: usize) -> Option<(usize, usize)> {
209        self.region.pos(index)
210    }
211}
212
213impl Default for Region {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219#[cfg(feature = "regex-onig")]
220mod regex_impl {
221    pub use onig::Region;
222    use onig::{MatchParam, RegexOptions, SearchOptions, Syntax};
223    use std::error::Error;
224
225    #[derive(Default)]
226    pub(crate) struct SearchScratch;
227
228    #[derive(Debug)]
229    pub struct Regex {
230        regex: onig::Regex,
231    }
232
233    pub fn new_region() -> Region {
234        Region::with_capacity(8)
235    }
236
237    pub(super) fn region_owned_heap_bytes(_: &Region) -> Option<usize> {
238        None
239    }
240
241    impl Regex {
242        pub fn new(regex_str: &str) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
243            let result = onig::Regex::with_options(
244                regex_str,
245                RegexOptions::REGEX_OPTION_CAPTURE_GROUP,
246                Syntax::default(),
247            );
248            match result {
249                Ok(regex) => Ok(Regex { regex }),
250                Err(error) => Err(Box::new(error)),
251            }
252        }
253
254        pub(super) fn search_with_scratch(
255            &self,
256            text: &str,
257            begin: usize,
258            end: usize,
259            region: Option<&mut Region>,
260            _: &mut SearchScratch,
261        ) -> bool {
262            self.search(text, begin, end, region)
263        }
264
265        pub fn is_match(&self, text: &str) -> bool {
266            self.regex
267                .match_with_options(text, 0, SearchOptions::SEARCH_OPTION_NONE, None)
268                .is_some()
269        }
270
271        pub fn search(
272            &self,
273            text: &str,
274            begin: usize,
275            end: usize,
276            region: Option<&mut Region>,
277        ) -> bool {
278            let matched = self.regex.search_with_param(
279                text,
280                begin,
281                end,
282                SearchOptions::SEARCH_OPTION_NONE,
283                region,
284                MatchParam::default(),
285            );
286
287            // If there's an error during search, treat it as non-matching.
288            // For example, in case of catastrophic backtracking, onig should
289            // fail with a "retry-limit-in-match over" error eventually.
290            matches!(matched, Ok(Some(_)))
291        }
292    }
293}
294
295// If both regex-fancy and regex-onig are requested, this condition makes regex-onig win.
296#[cfg(all(feature = "regex-fancy", not(feature = "regex-onig")))]
297mod regex_impl {
298    use std::error::Error;
299
300    #[derive(Debug)]
301    pub struct Regex {
302        regex: fancy_regex::Regex,
303    }
304
305    pub(crate) use fancy_regex::SearchScratch;
306
307    #[derive(Clone, Debug, Eq, PartialEq)]
308    pub struct Region {
309        positions: Vec<Option<(usize, usize)>>,
310    }
311
312    pub fn new_region() -> Region {
313        Region {
314            positions: Vec::with_capacity(8),
315        }
316    }
317
318    pub(super) fn region_owned_heap_bytes(region: &Region) -> Option<usize> {
319        region
320            .positions
321            .capacity()
322            .checked_mul(std::mem::size_of::<Option<(usize, usize)>>())
323    }
324
325    impl Regex {
326        pub fn new(regex_str: &str) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
327            Self::build(
328                regex_str,
329                #[cfg(feature = "shared-engines")]
330                None,
331            )
332        }
333
334        #[cfg(feature = "shared-engines")]
335        pub(super) fn new_cached(
336            regex_str: &str,
337            cache: &fancy_regex::EngineCache,
338        ) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
339            Self::build(regex_str, Some(cache))
340        }
341
342        fn build(
343            regex_str: &str,
344            #[cfg(feature = "shared-engines")] cache: Option<&fancy_regex::EngineCache>,
345        ) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
346            let mut builder = fancy_regex::RegexBuilder::new(regex_str);
347            builder.oniguruma_mode(true);
348            #[cfg(feature = "shared-engines")]
349            let result = match cache {
350                Some(cache) => builder.build_with_cache(cache),
351                None => builder.build(),
352            };
353            #[cfg(not(feature = "shared-engines"))]
354            let result = builder.build();
355            match result {
356                Ok(regex) => Ok(Regex { regex }),
357                Err(error) => Err(Box::new(error)),
358            }
359        }
360
361        #[cfg(feature = "owner-census")]
362        pub(super) fn visit_engines<'a>(
363            &'a self,
364            visit: impl FnMut(fancy_regex::owner_census::Engine<'a>),
365        ) {
366            self.regex.visit_engines(visit);
367        }
368
369        pub fn is_match(&self, text: &str) -> bool {
370            // Errors are treated as non-matches
371            self.regex.is_match(text).unwrap_or(false)
372        }
373
374        pub fn search(
375            &self,
376            text: &str,
377            begin: usize,
378            end: usize,
379            region: Option<&mut Region>,
380        ) -> bool {
381            self.search_with_scratch(text, begin, end, region, &mut SearchScratch::default())
382        }
383
384        pub(super) fn search_with_scratch(
385            &self,
386            text: &str,
387            begin: usize,
388            end: usize,
389            region: Option<&mut Region>,
390            scratch: &mut SearchScratch,
391        ) -> bool {
392            // If there's an error during search, treat it as non-matching.
393            // For example, in case of catastrophic backtracking, fancy-regex should
394            // fail with an error eventually.
395            if let Ok(Some(captures)) =
396                self.regex
397                    .captures_from_pos_with_scratch(&text[..end], begin, scratch)
398            {
399                if let Some(region) = region {
400                    region.init_from_captures(&captures);
401                }
402                true
403            } else {
404                false
405            }
406        }
407
408        pub(super) fn required_ascii_bytes(&self) -> u128 {
409            self.regex.required_ascii_bytes()
410        }
411    }
412
413    impl Region {
414        fn init_from_captures(&mut self, captures: &fancy_regex::Captures) {
415            self.positions.clear();
416            for i in 0..captures.len() {
417                let pos = captures.get(i).map(|m| (m.start(), m.end()));
418                self.positions.push(pos);
419            }
420        }
421
422        pub fn pos(&self, i: usize) -> Option<(usize, usize)> {
423            if i < self.positions.len() {
424                self.positions[i]
425            } else {
426                None
427            }
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn owned_heap_tracks_capture_growth_and_private_clones() {
438        let regex = Regex::new("(a)".repeat(16));
439        let text = "a".repeat(16);
440        let mut region = Region::new();
441        let empty_bytes = region.owned_heap_bytes();
442        assert!(regex.search(&text, 0, text.len(), Some(&mut region)));
443        let captured_bytes = region.owned_heap_bytes();
444        let cloned = region.clone();
445        for capture in 0..=16 {
446            assert_eq!(cloned.pos(capture), region.pos(capture));
447        }
448        assert_eq!(cloned.pos(16), Some((15, 16)));
449        if cfg!(feature = "regex-onig") {
450            assert_eq!(captured_bytes, None);
451            assert_eq!(cloned.owned_heap_bytes(), None);
452        } else {
453            assert!(captured_bytes.unwrap() > empty_bytes.unwrap());
454            let minimum = 17 * std::mem::size_of::<Option<(usize, usize)>>();
455            assert!(cloned.owned_heap_bytes().unwrap() >= minimum);
456        }
457        assert!(Regex::new("(b)".into()).search("b", 0, 1, Some(&mut region)));
458        assert_eq!(region.pos(1), Some((0, 1)));
459        assert_eq!(region.pos(16), None);
460        assert_eq!(cloned.pos(16), Some((15, 16)));
461        assert_eq!(region.owned_heap_bytes(), captured_bytes);
462    }
463
464    #[test]
465    fn caches_compiled_regex() {
466        let regex = Regex::new(String::from(r"\w+"));
467
468        assert!(regex.regex.get().is_none());
469        assert!(regex.is_match("test"));
470        assert!(regex.regex.get().is_some());
471    }
472
473    #[test]
474    fn serde_as_string() {
475        let pattern: Regex = serde_json::from_str("\"just a string\"").unwrap();
476        assert_eq!(pattern.regex_str(), "just a string");
477        let back_to_str = serde_json::to_string(&pattern).unwrap();
478        assert_eq!(back_to_str, "\"just a string\"");
479    }
480
481    #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
482    #[test]
483    fn only_static_rules_receive_engine_cache_admission() {
484        use crate::parsing::syntax_definition::{MatchOperation, MatchPattern};
485
486        let mut closing = MatchPattern {
487            has_captures: true,
488            regex: Regex::new(r"^\1$".into()),
489            scope: vec![],
490            captures: None,
491            operation: MatchOperation::None,
492            with_prototype: None,
493        };
494        let mut cache = RegexCache::default();
495        cache.share(&mut closing.regex);
496        assert!(closing.regex.engines.is_some());
497        assert!(closing.regex.clone().engines.is_some());
498        let restored: Regex =
499            serde_json::from_str(&serde_json::to_string(&closing.regex).unwrap()).unwrap();
500        assert!(restored.engines.is_none());
501        let opener = Regex::new(r"<<(\w+)".into());
502        let mut region = Region::new();
503        assert!(opener.search("<<END", 0, 5, Some(&mut region)));
504        let derived = closing.regex_with_refs(&region, "<<END");
505        assert!(derived.engines.is_none());
506        assert!(derived.is_match("END"));
507        assert!(!derived.is_match("OTHER"));
508        assert!(opener.engines.is_none());
509    }
510
511    #[test]
512    fn shared_compilation_keeps_captures_private_and_releases_its_owner() {
513        let text = r"(?<=prefix:)([a-z]+)([0-9]+)";
514        let mut first = Regex::new(text.into());
515        let mut second = Regex::new(text.into());
516        let independent = Regex::new(text.into());
517        let mut cache = RegexCache::default();
518        cache.share(&mut first);
519        cache.share(&mut second);
520        assert!(Arc::ptr_eq(&first.regex, &second.regex));
521        assert!(!Arc::ptr_eq(&first.regex, &independent.regex));
522        let owner = Arc::downgrade(&first.regex);
523
524        let mut a = Region::new();
525        let mut b = Region::new();
526        assert!(first.search("prefix:alpha12", 0, 14, Some(&mut a)));
527        assert!(second.search("prefix:beta345", 0, 14, Some(&mut b)));
528        assert_eq!(a.pos(1), Some((7, 12)));
529        assert_eq!(b.pos(1), Some((7, 11)));
530        assert_eq!(a.pos(2), Some((12, 14)));
531        assert_eq!(b.pos(2), Some((11, 14)));
532        assert!(second.regex.get().is_some());
533        let cloned = first.clone();
534        assert!(Arc::ptr_eq(&first.regex, &cloned.regex));
535        std::thread::scope(|threads| {
536            for regex in [&first, &second, &cloned] {
537                threads.spawn(move || {
538                    let mut region = Region::new();
539                    for _ in 0..100 {
540                        assert!(regex.search("prefix:alpha12", 0, 14, Some(&mut region)));
541                        assert_eq!(region.pos(1), Some((7, 12)));
542                        assert!(!regex.search("not a match", 0, 11, Some(&mut region)));
543                    }
544                });
545            }
546        });
547        drop((first, second, cloned));
548        assert!(owner.upgrade().is_none());
549        assert_eq!(cache.patterns.len(), 1);
550        let mut replacement = Regex::new(text.into());
551        cache.share(&mut replacement);
552        assert!(Arc::ptr_eq(
553            &cache.patterns[text].upgrade().unwrap(),
554            &replacement.regex,
555        ));
556        assert!(replacement.regex.get().is_none());
557        assert!(replacement.search("prefix:gamma67", 0, 14, Some(&mut a)));
558        assert_eq!(a.pos(1), Some((7, 12)));
559        assert_eq!(a.pos(2), Some((12, 14)));
560    }
561
562    #[cfg(feature = "yaml-load")]
563    #[test]
564    fn grammar_interning_preserves_rule_actions_clone_and_serialization() {
565        use crate::parsing::syntax_definition::Pattern;
566        use crate::parsing::{
567            ParseState, Scope, ScopeStackOp, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder,
568        };
569
570        let definition = SyntaxDefinition::load_from_str(
571            "name: Shared\nscope: source.shared\ncontexts:\n  main:\n    - match: 'a'\n      scope: keyword.first\n      push: second\n  second:\n    - match: 'a'\n      scope: string.second\n      pop: true\n",
572            true, None,
573        ).unwrap();
574        let mut builder = SyntaxSetBuilder::new();
575        builder.add(definition);
576        let set = builder.build();
577        let syntax = &set.syntaxes()[0];
578        let first = set.get_context(&syntax.context_ids()["main"]).unwrap();
579        let second = set.get_context(&syntax.context_ids()["second"]).unwrap();
580        let (Pattern::Match(a), Pattern::Match(b)) = (&first.patterns[0], &second.patterns[0])
581        else {
582            panic!("expected match rules")
583        };
584        assert!(Arc::ptr_eq(&a.regex.regex, &b.regex.regex));
585        let expected = ParseState::new(syntax).parse_line("aa\n", &set).unwrap();
586        assert!(expected.contains(&(0, ScopeStackOp::Push(Scope::new("keyword.first").unwrap()))));
587        assert!(expected.contains(&(1, ScopeStackOp::Push(Scope::new("string.second").unwrap()))));
588        let restored: SyntaxSet = crate::dumps::from_binary(&crate::dumps::dump_binary(&set));
589        for set in [set.clone(), restored] {
590            let answer = ParseState::new(&set.syntaxes()[0])
591                .parse_line("aa\n", &set)
592                .unwrap();
593            assert_eq!(answer, expected);
594        }
595    }
596}