litcheck_lit/config/
substitutions.rs

1use std::{borrow::Borrow, borrow::Cow, hash::Hash};
2
3use litcheck::{
4    diagnostics::{Diagnostic, SourceSpan, Span},
5    StaticCow,
6};
7use serde::Deserialize;
8
9use crate::FxIndexMap;
10
11#[derive(Diagnostic, Debug, thiserror::Error)]
12#[error("invalid substitution pattern: '{pattern}': {error}")]
13#[diagnostic()]
14pub struct InvalidSubstitutionPatternError {
15    #[label("{error}")]
16    span: SourceSpan,
17    #[source]
18    error: regex::Error,
19    pattern: String,
20}
21
22pub struct ScopedSubstitutionSet<'a> {
23    parent: &'a SubstitutionSet,
24    set: SubstitutionSet,
25}
26impl<'scope> ScopedSubstitutionSet<'scope> {
27    pub fn new(parent: &'scope SubstitutionSet) -> Self {
28        Self {
29            parent,
30            set: SubstitutionSet::default(),
31        }
32    }
33
34    #[allow(unused)]
35    pub fn is_empty(&self) -> bool {
36        self.set.is_empty() && self.parent.is_empty()
37    }
38
39    #[allow(unused)]
40    pub fn get<Q>(&self, pattern: &Q) -> Option<&StaticCow<str>>
41    where
42        StaticCow<str>: Borrow<Q>,
43        Q: Hash + Eq + ?Sized,
44    {
45        self.set.get(pattern).or_else(|| self.parent.get(pattern))
46    }
47
48    pub fn contains<Q>(&self, pattern: &Q) -> bool
49    where
50        StaticCow<str>: Borrow<Q>,
51        Q: Hash + Eq + ?Sized,
52    {
53        self.set.contains(pattern) || self.parent.contains(pattern)
54    }
55
56    #[inline]
57    pub fn iter(&self) -> impl Iterator<Item = (&StaticCow<str>, &StaticCow<str>)> + '_ {
58        SubstitutionsIter {
59            set: &self.set,
60            iter: self.parent.iter(),
61            filter: true,
62        }
63    }
64
65    // Identify previously defined substitutions that may conflict with this one
66    pub fn find_matching<'a>(&'a self, pattern: &'a str) -> Matches<'a> {
67        let has_exact = self.contains(pattern);
68        let has_fuzzy = self
69            .iter()
70            .any(|(k, _)| k != pattern && k.contains(pattern));
71        match (has_exact, has_fuzzy) {
72            (false, _) => Matches::Empty,
73            (true, false) => Matches::Exact(pattern),
74            (true, true) => Matches::Fuzzy {
75                exact: pattern,
76                keys: SubstitutionsIter {
77                    set: &self.set,
78                    iter: self.parent.iter(),
79                    filter: true,
80                },
81            },
82        }
83    }
84
85    pub fn extend<I, K, V>(&mut self, substitutions: I)
86    where
87        StaticCow<str>: From<K>,
88        StaticCow<str>: From<V>,
89        I: IntoIterator<Item = (K, V)>,
90    {
91        let substitutions = substitutions
92            .into_iter()
93            .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)));
94        self.set.extend(substitutions);
95    }
96
97    pub fn insert<K, V>(&mut self, pattern: K, replacement: V)
98    where
99        StaticCow<str>: From<K>,
100        StaticCow<str>: From<V>,
101    {
102        self.set
103            .insert(StaticCow::from(pattern), StaticCow::from(replacement));
104    }
105
106    pub fn apply<'a>(
107        &self,
108        input: Span<&'a str>,
109    ) -> Result<Cow<'a, str>, InvalidSubstitutionPatternError> {
110        let escape_re = regex::Regex::new("%%").unwrap();
111
112        let (span, input) = input.into_parts();
113        let mut buffer = Cow::Borrowed(input);
114        let mut needs_unescape = false;
115        let mut needs_escaping = true;
116        for (pattern, replacement) in self.iter() {
117            if needs_escaping {
118                if let Cow::Owned(escaped) =
119                    escape_re.replace_all(&buffer, regex::NoExpand("#_MARKER_#"))
120                {
121                    buffer = Cow::Owned(escaped);
122                    needs_unescape = true;
123                } else {
124                    needs_escaping = false;
125                }
126            }
127            let re =
128                regex::Regex::new(pattern).map_err(|error| InvalidSubstitutionPatternError {
129                    span,
130                    error,
131                    pattern: pattern.clone().into_owned(),
132                })?;
133            if let Cow::Owned(replaced) = re.replace_all(&buffer, replacement) {
134                buffer = Cow::Owned(replaced);
135                needs_escaping = true;
136            }
137        }
138
139        if needs_unescape {
140            let unescape_re = regex::Regex::new("#_MARKER_#").unwrap();
141            if let Cow::Owned(unescaped) = unescape_re.replace_all(&buffer, regex::NoExpand("%")) {
142                buffer = Cow::Owned(unescaped);
143            }
144        }
145
146        Ok(buffer)
147    }
148}
149
150pub struct SubstitutionsIter<'a> {
151    set: &'a SubstitutionSet,
152    iter: indexmap::map::Iter<'a, StaticCow<str>, StaticCow<str>>,
153    filter: bool,
154}
155impl<'a> Iterator for SubstitutionsIter<'a> {
156    type Item = (&'a StaticCow<str>, &'a StaticCow<str>);
157
158    fn next(&mut self) -> Option<Self::Item> {
159        if self.filter {
160            let result = self
161                .iter
162                .next()
163                .and_then(|item @ (k, _)| self.set.get(k).map(|v| (k, v)).or(Some(item)));
164            if result.is_none() {
165                self.iter = self.set.iter();
166                self.filter = false;
167            } else {
168                return result;
169            }
170        }
171        self.iter.next()
172    }
173}
174
175#[derive(Default, Clone, Debug, Deserialize)]
176#[serde(transparent)]
177pub struct SubstitutionSet {
178    set: FxIndexMap<StaticCow<str>, StaticCow<str>>,
179}
180impl IntoIterator for SubstitutionSet {
181    type Item = (StaticCow<str>, StaticCow<str>);
182    type IntoIter = indexmap::map::IntoIter<StaticCow<str>, StaticCow<str>>;
183
184    #[inline]
185    fn into_iter(self) -> Self::IntoIter {
186        self.set.into_iter()
187    }
188}
189impl SubstitutionSet {
190    #[allow(unused)]
191    pub fn new<I, K, V>(substitutions: I) -> Self
192    where
193        StaticCow<str>: From<K>,
194        StaticCow<str>: From<V>,
195        I: IntoIterator<Item = (K, V)>,
196    {
197        let set = substitutions
198            .into_iter()
199            .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)))
200            .collect();
201        Self { set }
202    }
203
204    #[inline]
205    pub fn is_empty(&self) -> bool {
206        self.set.is_empty()
207    }
208
209    pub fn contains<Q>(&self, pattern: &Q) -> bool
210    where
211        StaticCow<str>: Borrow<Q>,
212        Q: Hash + Eq + ?Sized,
213    {
214        self.set.contains_key(pattern)
215    }
216
217    pub fn get<Q>(&self, pattern: &Q) -> Option<&StaticCow<str>>
218    where
219        StaticCow<str>: Borrow<Q>,
220        Q: Hash + Eq + ?Sized,
221    {
222        self.set.get(pattern)
223    }
224
225    #[inline]
226    pub fn iter(&self) -> indexmap::map::Iter<'_, StaticCow<str>, StaticCow<str>> {
227        self.set.iter()
228    }
229
230    #[inline]
231    pub fn keys(&self) -> indexmap::map::Keys<'_, StaticCow<str>, StaticCow<str>> {
232        self.set.keys()
233    }
234
235    pub fn extend<I, K, V>(&mut self, substitutions: I)
236    where
237        StaticCow<str>: From<K>,
238        StaticCow<str>: From<V>,
239        I: IntoIterator<Item = (K, V)>,
240    {
241        let substitutions = substitutions
242            .into_iter()
243            .map(|(k, v)| (StaticCow::from(k), StaticCow::from(v)));
244        self.set.extend(substitutions);
245    }
246
247    pub fn insert<K, V>(&mut self, pattern: K, replacement: V)
248    where
249        StaticCow<str>: From<K>,
250        StaticCow<str>: From<V>,
251    {
252        self.set
253            .insert(StaticCow::from(pattern), StaticCow::from(replacement));
254    }
255}
256
257pub enum Matches<'a> {
258    Empty,
259    Exact(&'a str),
260    Fuzzy {
261        exact: &'a str,
262        keys: SubstitutionsIter<'a>,
263    },
264}
265impl<'a> Iterator for Matches<'a> {
266    type Item = &'a str;
267
268    fn next(&mut self) -> Option<Self::Item> {
269        match self {
270            Self::Empty => None,
271            Self::Exact(s) => {
272                let s = *s;
273                *self = Self::Empty;
274                Some(s)
275            }
276            Self::Fuzzy { exact, keys } => loop {
277                if let Some((key, _)) = keys.next() {
278                    if key.contains(*exact) {
279                        break Some(key.as_ref());
280                    }
281                } else {
282                    *self = Self::Empty;
283                    return None;
284                }
285            },
286        }
287    }
288}