Skip to main content

tramlines_core/grammar/
regex.rs

1//! Compiles each pattern once per run.
2
3use std::{cell::RefCell, collections::HashMap};
4
5use regex::Regex;
6
7/// A per-run cache of compiled patterns. A pattern that does not compile is
8/// remembered too, so its message is stable.
9#[derive(Debug, Default)]
10pub struct RegexCache {
11    map: RefCell<HashMap<String, Result<Regex, String>>>,
12}
13
14impl RegexCache {
15    /// A fresh cache.
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    /// The compiled pattern, or the compiler's message on one line: the
21    /// last line of its report, which says what is wrong, since a finding
22    /// is one line.
23    // sokf-implements: TL_16
24    pub fn get(&self, pattern: &str) -> Result<Regex, String> {
25        if let Some(hit) = self.map.borrow().get(pattern) {
26            return hit.clone();
27        }
28        let compiled = Regex::new(pattern).map_err(|e| reason(&e));
29        self.map
30            .borrow_mut()
31            .insert(pattern.to_string(), compiled.clone());
32        compiled
33    }
34}
35
36/// The compiler's message for `e` on one line: the last line of its
37/// report, which says what is wrong, since a finding is one line.
38pub(crate) fn reason(e: &regex::Error) -> String {
39    let report = e.to_string();
40    let last = report.lines().rev().find(|l| !l.trim().is_empty());
41    last.unwrap_or(&report)
42        .trim()
43        .trim_start_matches("error: ")
44        .to_string()
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn compiles_once_and_remembers_failures() {
53        let cache = RegexCache::new();
54        let a = cache.get("^a+$").unwrap();
55        assert!(a.is_match("aaa"));
56        assert!(cache.get("^a+$").is_ok());
57        let e = cache.get("(").unwrap_err();
58        assert_eq!(cache.get("(").unwrap_err(), e);
59        assert!(!e.contains('\n'), "{e}");
60        assert_eq!(cache.map.borrow().len(), 2);
61    }
62}