Skip to main content

sscanf_macro/
utils.rs

1#![allow(
2    unused,
3    reason = "These are general utilities that I copy-paste between projects"
4)]
5
6use crate::*;
7
8mod visitors {
9    pub mod lifetime;
10}
11pub use visitors::lifetime::*;
12
13/// A workaround for Spans on stable Rust.
14///
15/// Span manipulation doesn't work on stable Rust, which also means that spans cannot be joined
16/// together. This means that any compiler errors that occur would only point at the first token
17/// of the spanned expression, which is not very helpful.
18///
19/// The workaround, as demonstrated by `Error::new_spanned`, is to have the first part of the
20/// spanned expression be spanned with the first part of the source span, and the second part of the
21/// spanned expression be spanned with the second part of the source span. The compiler only looks
22/// at the start and end of the span and underlines everything in between, so this works.
23#[derive(Copy, Clone)]
24pub struct FullSpan(Span, Span);
25
26impl FullSpan {
27    pub fn from_span(span: Span) -> Self {
28        Self(span, span)
29    }
30    pub fn from_spanned<T: ToTokens>(span: &T) -> Self {
31        let mut tokens = span.to_token_stream().into_iter().map(|t| t.span());
32        let start = tokens.next().unwrap_or(Span::call_site());
33        let end = tokens.last().unwrap_or(start);
34        Self(start, end)
35    }
36    pub fn apply(self, a: TokenStream, b: TokenStream) -> TokenStream {
37        let mut ret = self.apply_start(a);
38        ret.extend(self.apply_end(b));
39        ret
40    }
41    pub fn apply_start(self, a: TokenStream) -> TokenStream {
42        a.with_span(self.0)
43    }
44    pub fn apply_end(self, b: TokenStream) -> TokenStream {
45        b.with_span(self.1)
46    }
47}
48
49/// Find the closest match to a string in a list of strings.
50pub fn find_closest<'a>(s: &str, compare: &[&'a str]) -> Option<&'a str> {
51    let mut best_confidence = 0.8; // minimum confidence
52    let mut best_match = None;
53    for valid in compare {
54        let confidence = strsim::jaro_winkler(s, valid);
55        if confidence > best_confidence {
56            best_confidence = confidence;
57            best_match = Some(*valid);
58        }
59    }
60    best_match
61}
62/// Find the closest match to a string in a list of elements, removing it.
63pub fn take_closest<T: Display>(s: &str, compare: &mut Vec<T>) -> Option<T> {
64    let mut best_confidence = 0.8; // minimum confidence
65    let mut best_index = None;
66    for (i, valid) in compare.iter().enumerate() {
67        let confidence = strsim::jaro_winkler(s, &valid.to_string());
68        if confidence > best_confidence {
69            best_confidence = confidence;
70            best_index = Some(i);
71        }
72    }
73    best_index.map(|index| compare.remove(index))
74}
75
76/// Format a list of items as a comma-separated list, with "or" before the last item.
77pub fn list_items<T: Display>(items: &[T]) -> String {
78    list_items_with(items, |x| x)
79}
80
81/// Format a list of items as a comma-separated list, with "or" before the last item.
82pub fn list_items_quoted<T: Display>(items: &[T], quote: char) -> String {
83    list_items_with(items, |x| format!("{quote}{x}{quote}"))
84}
85
86/// Format a list of items as a comma-separated list, with "or" before the last item.
87pub fn list_items_with<'a, T, D: Display + 'a>(
88    items: &'a [T],
89    mut display: impl FnMut(&'a T) -> D,
90) -> String {
91    match items {
92        [] => String::new(),
93        [x] => display(x).to_string(),
94        [a, b] => format!("{} or {}", display(a), display(b)),
95        [start @ .., last] => {
96            use std::fmt::Write;
97            let mut s = String::new();
98            for item in start {
99                write!(s, "{}, ", display(item)).unwrap();
100            }
101            write!(s, "or {}", display(last)).unwrap();
102            s
103        }
104    }
105}
106
107/// Extension trait for [`TokenStream`] that allows setting the span of all tokens in the stream.
108pub trait TokenStreamExt {
109    fn set_span(&mut self, span: Span);
110    fn with_span(self, span: Span) -> Self;
111}
112impl TokenStreamExt for TokenStream {
113    fn set_span(&mut self, span: Span) {
114        let old = std::mem::replace(self, TokenStream::new());
115        *self = old.with_span(span);
116    }
117    fn with_span(self, span: Span) -> Self {
118        self.into_iter()
119            .map(|mut t| {
120                if let proc_macro2::TokenTree::Group(ref mut g) = t {
121                    *g = proc_macro2::Group::new(g.delimiter(), g.stream().with_span(span));
122                }
123                t.set_span(span);
124                t
125            })
126            .collect()
127    }
128}
129
130// WORKAROUNDS: proc_macro(1) has stabilized several methods on Span, but proc_macro2 has not.
131// Will be removed once proc_macro2 stabilizes these methods.
132
133pub trait SpanExt {
134    fn stable_start(&self) -> Span;
135    fn stable_end(&self) -> Span;
136    fn stable_line(&self) -> usize;
137    fn stable_column(&self) -> usize;
138    fn stable_file(&self) -> String;
139}
140#[cfg(not(test))]
141impl SpanExt for Span {
142    fn stable_start(&self) -> Span {
143        self.unwrap().start().into() // Span2 -> Span1 -> call start() -> Span2
144    }
145    fn stable_end(&self) -> Span {
146        self.unwrap().end().into() // Span2 -> Span1 -> call end() -> Span2
147    }
148    fn stable_line(&self) -> usize {
149        self.unwrap().line() // Span2 -> Span1 -> line()
150    }
151    fn stable_column(&self) -> usize {
152        self.unwrap().column() // Span2 -> Span1 -> column()
153    }
154    fn stable_file(&self) -> String {
155        self.unwrap().file()
156    }
157}
158#[cfg(test)]
159impl SpanExt for Span {
160    fn stable_start(&self) -> Span {
161        // Note: Span::unwrap() returns a proc_macro(1) Span, which is not available outside of procedural macros, aka when testing.
162        *self
163    }
164    fn stable_end(&self) -> Span {
165        *self
166    }
167    fn stable_line(&self) -> usize {
168        999
169    }
170    fn stable_column(&self) -> usize {
171        123
172    }
173    fn stable_file(&self) -> String {
174        "/inside/a/test.rs".to_string()
175    }
176}
177
178pub trait ToTokensExt {
179    fn start_span(&self) -> Span;
180    fn end_span(&self) -> Span;
181}
182
183impl<T: ToTokens> ToTokensExt for T {
184    fn start_span(&self) -> Span {
185        self.to_token_stream()
186            .into_iter()
187            .next()
188            .map_or_else(Span::call_site, |t| t.span().stable_start())
189    }
190    fn end_span(&self) -> Span {
191        self.to_token_stream()
192            .into_iter()
193            .last()
194            .map_or_else(Span::call_site, |t| t.span().stable_end())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    /// Utility macro for other tests: Asserts that the given block or statement throws a panic with the given message.
203    #[macro_export]
204    macro_rules! assert_panic_message_eq {
205        ( $block:block, $message:literal $(,)? ) => {
206            let Err(error) = std::panic::catch_unwind(move || $block) else {
207                panic!("code {} did not panic", stringify!($block));
208            };
209            if let Some(s) = error.downcast_ref::<&'static str>() {
210                assert_eq!(*s, $message);
211            } else if let Some(s) = error.downcast_ref::<String>() {
212                assert_eq!(s, $message);
213            } else {
214                panic!("unexpected panic payload: {:?}", error);
215            }
216        };
217        ( $expression:expr, $message:literal $(,)? ) => {
218            assert_panic_message_eq!(
219                {
220                    $expression; // avoid problems with lifetimes by not returning the value
221                },
222                $message
223            );
224        };
225        ( $statement:stmt, $message:literal $(,)? ) => {
226            assert_panic_message_eq!({ $statement }, $message);
227        };
228    }
229
230    #[test]
231    fn find_closest_basic() {
232        let options = ["apple", "banana", "cherry", "date"];
233
234        assert_eq!(find_closest("appl", &options), Some("apple"));
235        assert_eq!(find_closest("bannana", &options), Some("banana"));
236        assert_eq!(find_closest("cheri", &options), Some("cherry"));
237        assert_eq!(find_closest("dat", &options), Some("date"));
238
239        assert_eq!(find_closest("xyz", &options), None);
240    }
241
242    #[test]
243    fn take_closest_basic() {
244        let mut options = vec![
245            "apple".to_string(),
246            "banana".to_string(),
247            "cherry".to_string(),
248            "date".to_string(),
249        ];
250
251        assert_eq!(
252            take_closest("appl", &mut options),
253            Some("apple".to_string())
254        );
255        assert_eq!(take_closest("appl", &mut options), None); // already taken
256
257        assert_eq!(
258            take_closest("bannana", &mut options),
259            Some("banana".to_string())
260        );
261        assert_eq!(
262            take_closest("cheri", &mut options),
263            Some("cherry".to_string())
264        );
265        assert_eq!(take_closest("dat", &mut options), Some("date".to_string()));
266
267        assert_eq!(take_closest("xyz", &mut options), None);
268    }
269
270    #[test]
271    fn list_items_empty() {
272        let items: [&str; 0] = [];
273        assert_eq!(list_items(&items), "");
274        assert_eq!(list_items_quoted(&items, '"'), "");
275        assert_eq!(list_items_with(&items, |x| format!("{x} ^ {x}")), "");
276    }
277
278    #[test]
279    fn list_items_single() {
280        let items = ["apple"];
281        assert_eq!(list_items(&items), "apple");
282        assert_eq!(list_items_quoted(&items, '`'), "`apple`");
283        assert_eq!(
284            list_items_with(&items, |x| format!("{x} ^ {x}")),
285            "apple ^ apple"
286        );
287    }
288
289    #[test]
290    fn list_items_two() {
291        let items = ["apple", "banana"];
292        assert_eq!(list_items(&items), "apple or banana");
293        assert_eq!(list_items_quoted(&items, '\''), "'apple' or 'banana'");
294        assert_eq!(
295            list_items_with(&items, |x| format!("{x} ^ {x}")),
296            "apple ^ apple or banana ^ banana"
297        );
298    }
299
300    #[test]
301    fn list_items_many() {
302        let items = ["apple", "banana", "cherry"];
303        assert_eq!(list_items(&items), "apple, banana, or cherry");
304        assert_eq!(
305            list_items_quoted(&items, '"'),
306            "\"apple\", \"banana\", or \"cherry\""
307        );
308        assert_eq!(
309            list_items_with(&items, |x| format!("{x} ^ {x}")),
310            "apple ^ apple, banana ^ banana, or cherry ^ cherry"
311        );
312
313        let items = ["apple", "banana", "cherry", "date"];
314        assert_eq!(list_items(&items), "apple, banana, cherry, or date");
315        assert_eq!(
316            list_items_quoted(&items, '`'),
317            "`apple`, `banana`, `cherry`, or `date`"
318        );
319        assert_eq!(
320            list_items_with(&items, |x| format!("{x} ^ {x}")),
321            "apple ^ apple, banana ^ banana, cherry ^ cherry, or date ^ date"
322        );
323    }
324}