sqruff_lib_core/
helpers.rs

1use std::cell::RefCell;
2use std::hash::BuildHasherDefault;
3use std::panic;
4use std::path::{Component, Path, PathBuf};
5use std::sync::Once;
6
7use crate::parser::matchable::{Matchable, MatchableTraitImpl};
8
9pub type IndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<ahash::AHasher>>;
10pub type IndexSet<V> = indexmap::IndexSet<V, BuildHasherDefault<ahash::AHasher>>;
11
12pub trait ToMatchable: Sized {
13    fn to_matchable(self) -> Matchable;
14}
15
16impl<T: Into<MatchableTraitImpl>> ToMatchable for T {
17    fn to_matchable(self) -> Matchable {
18        Matchable::new(self.into())
19    }
20}
21
22pub fn capitalize(s: &str) -> String {
23    assert!(s.is_ascii());
24
25    let mut chars = s.chars();
26    let Some(first_char) = chars.next() else {
27        return String::new();
28    };
29
30    first_char
31        .to_uppercase()
32        .chain(chars.map(|ch| ch.to_ascii_lowercase()))
33        .collect()
34}
35
36pub trait Config: Sized {
37    fn config(mut self, f: impl FnOnce(&mut Self)) -> Self {
38        f(&mut self);
39        self
40    }
41}
42
43impl<T> Config for T {}
44
45pub fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
46    let last = iter.next();
47    iter.scan(last, |state, item| std::mem::replace(state, Some(item)))
48}
49
50// https://github.com/rust-lang/rfcs/issues/2208#issuecomment-342679694
51pub fn normalize(p: &Path) -> PathBuf {
52    let mut stack: Vec<Component> = vec![];
53
54    // We assume .components() removes redundant consecutive path separators.
55    // Note that .components() also does some normalization of '.' on its own
56    // anyways. This '.' normalization happens to be compatible with the
57    // approach below.
58    for component in p.components() {
59        match component {
60            // Drop CurDir components, do not even push onto the stack.
61            Component::CurDir => {}
62
63            // For ParentDir components, we need to use the contents of the stack.
64            Component::ParentDir => {
65                // Look at the top element of stack, if any.
66                let top = stack.last().copied();
67
68                match top {
69                    // A component is on the stack, need more pattern matching.
70                    Some(c) => {
71                        match c {
72                            // Push the ParentDir on the stack.
73                            Component::Prefix(_) => {
74                                stack.push(component);
75                            }
76
77                            // The parent of a RootDir is itself, so drop the ParentDir (no-op).
78                            Component::RootDir => {}
79
80                            // A CurDir should never be found on the stack, since they are dropped
81                            // when seen.
82                            Component::CurDir => {
83                                unreachable!();
84                            }
85
86                            // If a ParentDir is found, it must be due to it piling up at the start
87                            // of a path. Push the new ParentDir onto
88                            // the stack.
89                            Component::ParentDir => {
90                                stack.push(component);
91                            }
92
93                            // If a Normal is found, pop it off.
94                            Component::Normal(_) => {
95                                let _ = stack.pop();
96                            }
97                        }
98                    }
99
100                    // Stack is empty, so path is empty, just push.
101                    None => {
102                        stack.push(component);
103                    }
104                }
105            }
106
107            // All others, simply push onto the stack.
108            _ => {
109                stack.push(component);
110            }
111        }
112    }
113
114    // If an empty PathBuf would be return, instead return CurDir ('.').
115    if stack.is_empty() {
116        return PathBuf::from(".");
117    }
118
119    let mut norm_path = PathBuf::new();
120
121    for item in &stack {
122        norm_path.push(item);
123    }
124
125    norm_path
126}
127
128pub fn enter_panic(context: String) -> PanicContext {
129    static ONCE: Once = Once::new();
130    ONCE.call_once(PanicContext::init);
131
132    with_ctx(|ctx| ctx.push(context));
133    PanicContext { _priv: () }
134}
135
136#[must_use]
137pub struct PanicContext {
138    _priv: (),
139}
140
141impl PanicContext {
142    #[allow(clippy::print_stderr)]
143    fn init() {
144        let default_hook = panic::take_hook();
145        let hook = move |panic_info: &panic::PanicHookInfo<'_>| {
146            with_ctx(|ctx| {
147                if !ctx.is_empty() {
148                    eprintln!("Panic context:");
149                    for frame in ctx.iter() {
150                        eprintln!("> {frame}\n");
151                    }
152                }
153                default_hook(panic_info);
154            });
155        };
156        panic::set_hook(Box::new(hook));
157    }
158}
159
160impl Drop for PanicContext {
161    fn drop(&mut self) {
162        with_ctx(|ctx| assert!(ctx.pop().is_some()));
163    }
164}
165
166fn with_ctx(f: impl FnOnce(&mut Vec<String>)) {
167    thread_local! {
168        static CTX: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
169    }
170    CTX.with(|ctx| f(&mut ctx.borrow_mut()));
171}