forge_fmt/
string.rs

1//! Helpers for dealing with quoted strings
2
3/// The state of a character in a string with quotable components
4/// This is a simplified version of the
5/// [actual parser](https://docs.soliditylang.org/en/v0.8.15/grammar.html#a4.SolidityLexer.EscapeSequence)
6/// as we don't care about hex or other character meanings
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum QuoteState {
9    /// Not currently in quoted string
10    #[default]
11    None,
12    /// The opening character of a quoted string
13    Opening(char),
14    /// A character in a quoted string
15    String(char),
16    /// The `\` in an escape sequence `"\n"`
17    Escaping(char),
18    /// The escaped character e.g. `n` in `"\n"`
19    Escaped(char),
20    /// The closing character
21    Closing(char),
22}
23
24/// An iterator over characters and indices in a string slice with information about quoted string
25/// states
26pub struct QuoteStateCharIndices<'a> {
27    iter: std::str::CharIndices<'a>,
28    state: QuoteState,
29}
30
31impl<'a> QuoteStateCharIndices<'a> {
32    fn new(string: &'a str) -> Self {
33        Self { iter: string.char_indices(), state: QuoteState::None }
34    }
35    pub fn with_state(mut self, state: QuoteState) -> Self {
36        self.state = state;
37        self
38    }
39}
40
41impl Iterator for QuoteStateCharIndices<'_> {
42    type Item = (QuoteState, usize, char);
43    fn next(&mut self) -> Option<Self::Item> {
44        let (idx, ch) = self.iter.next()?;
45        match self.state {
46            QuoteState::None | QuoteState::Closing(_) => {
47                if ch == '\'' || ch == '"' {
48                    self.state = QuoteState::Opening(ch);
49                } else {
50                    self.state = QuoteState::None
51                }
52            }
53            QuoteState::String(quote) | QuoteState::Opening(quote) | QuoteState::Escaped(quote) => {
54                if ch == quote {
55                    self.state = QuoteState::Closing(quote)
56                } else if ch == '\\' {
57                    self.state = QuoteState::Escaping(quote)
58                } else {
59                    self.state = QuoteState::String(quote)
60                }
61            }
62            QuoteState::Escaping(quote) => self.state = QuoteState::Escaped(quote),
63        }
64        Some((self.state, idx, ch))
65    }
66}
67
68/// An iterator over the indices of quoted string locations
69pub struct QuotedRanges<'a>(QuoteStateCharIndices<'a>);
70
71impl QuotedRanges<'_> {
72    pub fn with_state(mut self, state: QuoteState) -> Self {
73        self.0 = self.0.with_state(state);
74        self
75    }
76}
77
78impl Iterator for QuotedRanges<'_> {
79    type Item = (char, usize, usize);
80    fn next(&mut self) -> Option<Self::Item> {
81        let (quote, start) = loop {
82            let (state, idx, _) = self.0.next()?;
83            match state {
84                QuoteState::Opening(quote) |
85                QuoteState::Escaping(quote) |
86                QuoteState::Escaped(quote) |
87                QuoteState::String(quote) => break (quote, idx),
88                QuoteState::Closing(quote) => return Some((quote, idx, idx)),
89                QuoteState::None => {}
90            }
91        };
92        for (state, idx, _) in self.0.by_ref() {
93            if matches!(state, QuoteState::Closing(_)) {
94                return Some((quote, start, idx))
95            }
96        }
97        None
98    }
99}
100
101/// Helpers for iterating over quoted strings
102pub trait QuotedStringExt {
103    /// Returns an iterator of characters, indices and their quoted string state.
104    fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_>;
105
106    /// Returns an iterator of quoted string ranges.
107    fn quoted_ranges(&self) -> QuotedRanges<'_> {
108        QuotedRanges(self.quote_state_char_indices())
109    }
110
111    /// Check to see if a string is quoted. This will return true if the first character
112    /// is a quote and the last character is a quote with no non-quoted sections in between.
113    fn is_quoted(&self) -> bool {
114        let mut iter = self.quote_state_char_indices();
115        if !matches!(iter.next(), Some((QuoteState::Opening(_), _, _))) {
116            return false
117        }
118        while let Some((state, _, _)) = iter.next() {
119            if matches!(state, QuoteState::Closing(_)) {
120                return iter.next().is_none()
121            }
122        }
123        false
124    }
125}
126
127impl<T> QuotedStringExt for T
128where
129    T: AsRef<str>,
130{
131    fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> {
132        QuoteStateCharIndices::new(self.as_ref())
133    }
134}
135
136impl QuotedStringExt for str {
137    fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> {
138        QuoteStateCharIndices::new(self)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use similar_asserts::assert_eq;
146
147    #[test]
148    fn quote_state_char_indices() {
149        assert_eq!(
150            r#"a'a"\'\"\n\\'a"#.quote_state_char_indices().collect::<Vec<_>>(),
151            vec![
152                (QuoteState::None, 0, 'a'),
153                (QuoteState::Opening('\''), 1, '\''),
154                (QuoteState::String('\''), 2, 'a'),
155                (QuoteState::String('\''), 3, '"'),
156                (QuoteState::Escaping('\''), 4, '\\'),
157                (QuoteState::Escaped('\''), 5, '\''),
158                (QuoteState::Escaping('\''), 6, '\\'),
159                (QuoteState::Escaped('\''), 7, '"'),
160                (QuoteState::Escaping('\''), 8, '\\'),
161                (QuoteState::Escaped('\''), 9, 'n'),
162                (QuoteState::Escaping('\''), 10, '\\'),
163                (QuoteState::Escaped('\''), 11, '\\'),
164                (QuoteState::Closing('\''), 12, '\''),
165                (QuoteState::None, 13, 'a'),
166            ]
167        );
168    }
169
170    #[test]
171    fn quoted_ranges() {
172        let string = r#"testing "double quoted" and 'single quoted' strings"#;
173        assert_eq!(
174            string
175                .quoted_ranges()
176                .map(|(quote, start, end)| (quote, &string[start..=end]))
177                .collect::<Vec<_>>(),
178            vec![('"', r#""double quoted""#), ('\'', "'single quoted'")]
179        );
180    }
181}