Skip to main content

moxy_token/
ident.rs

1use super::{ToTokens, TokenStream};
2use crate::lex::{Cursor, LexError, Scan};
3use crate::{Span, TokenTree};
4
5/// Builds an [`Ident`] from one or more segments, lexing the result and panicking
6/// on invalid identifier syntax.
7///
8/// Accepts a bare identifier, a string/expression that evaluates to a name, or a
9/// comma-separated list of segments that are concatenated into a single identifier.
10/// Bare identifiers are captured by name via `stringify!`; expressions are converted
11/// with [`ToString`].
12///
13/// # Examples
14///
15/// ```ignore
16/// use moxy::token::ident;
17///
18/// // Bare identifier
19/// let counter = ident!(counter);
20/// assert_eq!(counter, "counter");
21///
22/// // Raw identifier
23/// let kw = ident!(r#struct);
24/// assert_eq!(kw.text(), "struct");
25/// assert!(kw.is_raw());
26///
27/// // From a string or expression
28/// let name = ident!("buffer");
29/// assert_eq!(name, "buffer");
30///
31/// // Concatenate segments into one identifier
32/// let field = ident!(get, "_", value);
33/// assert_eq!(field, "get_value");
34///
35/// // Bare identifier segments are taken by name, not evaluated
36/// let slot = ident!(slot, "_", n);
37/// assert_eq!(slot, "slot_n");
38/// ```
39///
40/// # Panics
41///
42/// Panics if the concatenated text is not a valid identifier.
43#[macro_export]
44macro_rules! ident {
45    ($x:ident) => { $crate::Ident::lex(stringify!($x).to_string()).expect("invalid syntax") };
46    ($x:expr)  => { $crate::Ident::lex($x).expect("invalid syntax") };
47    ($head:ident, $($tail:tt)+) => {{
48        let mut __ident = stringify!($head).to_string();
49        ident!(@accum __ident, $($tail)+)
50    }};
51    ($head:expr, $($tail:tt)+) => {{
52        let mut __ident = $head.to_string();
53        ident!(@accum __ident, $($tail)+)
54    }};
55    (@accum $acc:ident, $next:ident, $($tail:tt)+) => {{
56        $acc += stringify!($next);
57        ident!(@accum $acc, $($tail)+)
58    }};
59    (@accum $acc:ident, $next:expr, $($tail:tt)+) => {{
60        $acc += &$next.to_string();
61        ident!(@accum $acc, $($tail)+)
62    }};
63    (@accum $acc:ident, $last:ident) => {{
64        $acc += stringify!($last);
65        $crate::Ident::lex($acc).expect("invalid syntax")
66    }};
67    (@accum $acc:ident, $last:expr) => {{
68        $acc += &$last.to_string();
69        $crate::Ident::lex($acc).expect("invalid syntax")
70    }};
71}
72
73/// A Rust identifier token, including its source span and raw-identifier form.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Ident {
76    text: Box<str>,
77    span: Span,
78}
79
80impl Ident {
81    pub fn new(text: impl std::fmt::Display) -> Self {
82        Self {
83            text: text.to_string().into_boxed_str(),
84            span: Span::default(),
85        }
86    }
87
88    pub fn lex(input: impl std::fmt::Display) -> Result<Self, LexError> {
89        std::str::FromStr::from_str(&input.to_string())
90    }
91
92    #[inline]
93    pub fn with_span(mut self, span: Span) -> Self {
94        self.span = span;
95        self
96    }
97
98    #[inline]
99    pub fn text(&self) -> &str {
100        match self.text.strip_prefix("r#") {
101            Some(rest) => rest,
102            None => &self.text,
103        }
104    }
105
106    #[inline]
107    pub fn is_raw(&self) -> bool {
108        self.text.starts_with("r#")
109    }
110
111    #[inline]
112    pub fn span(&self) -> Span {
113        self.span
114    }
115
116    #[inline]
117    pub fn set_span(&mut self, span: Span) {
118        self.span = span;
119    }
120
121    pub fn to_lowercase(mut self) -> Self {
122        self.text = self.text.to_lowercase().into_boxed_str();
123        self
124    }
125
126    pub fn to_uppercase(mut self) -> Self {
127        self.text = self.text.to_uppercase().into_boxed_str();
128        self
129    }
130
131    pub fn to_pascal_case(&self) -> Self {
132        let text: String = self
133            .text
134            .split(|c: char| !c.is_alphanumeric())
135            .filter(|s| !s.is_empty())
136            .map(|word| {
137                let mut chars = word.chars();
138
139                match chars.next() {
140                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
141                    None => String::new(),
142                }
143            })
144            .collect();
145
146        Self::new(text).with_span(self.span)
147    }
148
149    pub fn to_snake_case(&self) -> Self {
150        let mut output = String::with_capacity(self.text().len());
151
152        for (i, c) in self.text().chars().enumerate() {
153            if c.is_ascii_uppercase() {
154                if i > 0 && !output.ends_with('_') {
155                    output.push('_');
156                }
157
158                output.push(c.to_ascii_lowercase());
159            } else if c.is_alphanumeric() {
160                output.push(c);
161            } else if !output.ends_with('_') {
162                output.push('_');
163            }
164        }
165
166        Self::new(output.trim_matches('_')).with_span(self.span)
167    }
168
169    #[inline]
170    pub fn to_token_tree(&self) -> TokenTree {
171        TokenTree::Ident(self.clone())
172    }
173
174    #[inline]
175    pub fn into_token_tree(self) -> TokenTree {
176        TokenTree::Ident(self)
177    }
178}
179
180impl Scan for Ident {
181    fn scan(cursor: Cursor<'_>) -> Result<(Cursor<'_>, Self), LexError> {
182        // Raw ident: r#ident
183        if cursor.starts_with("r#") {
184            let after = cursor.advance_by(2);
185            let end = after.skip_while(unicode_ident::is_xid_continue);
186
187            if end.offset() == after.offset() {
188                return cursor.error().into();
189            }
190
191            let span = cursor.span_to(&end);
192            let name = &cursor.rest()[..end.offset() as usize - cursor.offset() as usize];
193            return Ok((end, Self::new(name).with_span(span)));
194        }
195
196        let first = cursor.first().ok_or(cursor.error())?;
197
198        if first != '_' && !unicode_ident::is_xid_start(first) {
199            return cursor.error().into();
200        }
201
202        let end = cursor.advance_by(first.len_utf8()).skip_while(unicode_ident::is_xid_continue);
203        let span = cursor.span_to(&end);
204        let text = &cursor.rest()[..end.offset() as usize - cursor.offset() as usize];
205        Ok((end, Self::new(text).with_span(span)))
206    }
207}
208
209impl std::fmt::Display for Ident {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(f, "{}", self.text)
212    }
213}
214
215impl ToTokens for Ident {
216    fn to_tokens(&self, tokens: &mut TokenStream) {
217        tokens.extend_one(TokenTree::Ident(self.clone()));
218    }
219}
220
221impl crate::Spanner for Ident {
222    fn span(&self) -> Span {
223        self.span
224    }
225}
226
227impl std::str::FromStr for Ident {
228    type Err = LexError;
229
230    fn from_str(s: &str) -> Result<Self, Self::Err> {
231        let cursor = Cursor::new(s, 0);
232        let (_, ident) = Self::scan(cursor)?;
233        Ok(ident)
234    }
235}
236
237#[cfg(feature = "serde")]
238impl serde::Serialize for Ident {
239    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
240    where
241        S: serde::Serializer,
242    {
243        self.text.serialize(s)
244    }
245}
246
247impl PartialEq<str> for Ident {
248    fn eq(&self, other: &str) -> bool {
249        self.text.as_ref() == other
250    }
251}
252
253impl PartialEq<&str> for Ident {
254    fn eq(&self, other: &&str) -> bool {
255        self.text.as_ref() == *other
256    }
257}
258
259impl std::hash::Hash for Ident {
260    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
261        self.text.hash(state);
262    }
263}