concat_idents_bruce0203/
lib.rs

1//! This crates provides a single, easy to use macro, that allows you to actually use concatenated
2//! identifiers in Rust.
3
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7
8use quote::quote;
9use syn::parse::{self, Parse, ParseStream};
10use syn::spanned::Spanned;
11use syn::token::Underscore;
12use syn::{
13    parse_macro_input, Block, Ident, LitBool, LitByte, LitByteStr, LitChar, LitFloat, LitInt,
14    LitStr, Token,
15};
16
17#[cfg(test)]
18mod tests {
19    #[test]
20    fn test() {
21        let t = trybuild::TestCases::new();
22        t.pass("tests/pass.rs");
23        t.compile_fail("tests/fail/*.rs");
24    }
25}
26
27/// A helper struct that implements [`Parse`] and extracts the `replace_ident`, the `concatenated_ident` and
28/// the code `block` from the original macro input
29/// ```text
30/// concat_idents!(
31///     ident = ident1, _, ident2 { /* code */ }
32///     ^^^^^   ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
33///     |       |                 code block
34///     |       |
35///     |       concatenated-ident (in this case: 'ident1_ident2')
36///     |
37///     replace-ident
38/// );
39/// ```
40struct InputParser {
41    replace_ident: Ident,
42    concatenated_ident: Ident,
43    block: proc_macro2::TokenStream,
44}
45
46impl Parse for InputParser {
47    fn parse(input: ParseStream) -> parse::Result<Self> {
48        let replace_ident: Ident = input.parse()?;
49        let _: Token![=] = input.parse()?;
50        let IdentParser(concatenated_ident) = input.parse()?;
51        let block: Block = input.parse()?;
52        let stmts = block.stmts;
53        let block = quote! {#(#stmts)*};
54        Ok(InputParser {
55            replace_ident,
56            concatenated_ident,
57            block,
58        })
59    }
60}
61
62/// A helper struct that implements [`Parse`] and makes one [`Ident`] from a comma separated list
63/// of idents, literals and underscores
64/// ```text
65/// ident1, ident2, _, 3, _, true
66/// => ident1ident2_3_true
67/// ```
68struct IdentParser(Ident);
69
70impl Parse for IdentParser {
71    fn parse(input: ParseStream) -> parse::Result<Self> {
72        let mut ident_parts = vec![];
73
74        while !input.peek(syn::token::Brace) {
75            ident_parts.push(IdentPart::parse(input)?);
76
77            if input.peek(Token![,]) {
78                input.parse::<Token![,]>()?;
79            } else {
80                break;
81            }
82        }
83
84        let span = match ident_parts.first() {
85            Some(IdentPart::Ident(i)) => i.span(),
86            Some(IdentPart::Underscore(u)) => u.span(),
87            Some(IdentPart::Str(s)) => s.span(),
88            Some(IdentPart::Char(c)) => c.span(),
89            Some(IdentPart::Bool(b)) if ident_parts.len() > 1 => b.span(),
90
91            Some(IdentPart::Bool(b)) => {
92                return Err(syn::Error::new(
93                    b.span(),
94                    "Identifiers cannot consist of only one bool",
95                ))
96            }
97            Some(IdentPart::Int(i)) if ident_parts.len() > 1 => {
98                return Err(syn::Error::new(
99                    i.span(),
100                    "Identifiers cannot start with integers",
101                ))
102            }
103            Some(IdentPart::Int(i)) => {
104                return Err(syn::Error::new(
105                    i.span(),
106                    "Identifiers cannot start nor consist only of integers with integers",
107                ))
108            }
109            None => {
110                return Err(syn::Error::new(
111                    input.span(),
112                    "Expected at least one identifier",
113                ))
114            }
115        };
116
117        let mut ident = String::new();
118
119        for part in ident_parts {
120            match part {
121                IdentPart::Ident(i) => ident.push_str(i.to_string().trim_start_matches("r#")),
122                IdentPart::Underscore(_) => ident.push('_'),
123                IdentPart::Int(i) => ident.push_str(i.to_string().as_str()),
124                IdentPart::Bool(b) => ident.push_str(b.value.to_string().as_str()),
125                IdentPart::Str(s) => ident.push_str(s.value().as_str()),
126                IdentPart::Char(c) => ident.push(c.value()),
127            }
128        }
129
130        Ok(Self(Ident::new(ident.as_str(), span)))
131    }
132}
133
134/// A helper struct, that represents a valid part of an identifier. Does not guarantee, that
135/// this specific part is a fully qualified identifier.
136///
137/// ```text
138/// ident1, ident2, _, 3, _, true
139/// => ident1, ident2, _, 3, _, true
140/// ```
141enum IdentPart {
142    Underscore(Underscore),
143    Ident(Ident),
144    Int(LitInt),
145    Bool(LitBool),
146    Str(LitStr),
147    Char(LitChar),
148}
149
150impl Parse for IdentPart {
151    fn parse(input: ParseStream) -> parse::Result<Self> {
152        if input.peek(Ident) {
153            Ok(Self::Ident(input.parse()?))
154        } else if input.peek(Token![_]) {
155            Ok(Self::Underscore(input.parse()?))
156        } else if input.peek(LitInt) {
157            Ok(Self::Int(input.parse()?))
158        } else if input.peek(LitBool) {
159            Ok(Self::Bool(input.parse()?))
160        } else if input.peek(LitStr) {
161            let string = input.parse::<LitStr>()?;
162            if string
163                .value()
164                .contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
165            {
166                Err(syn::Error::new(
167                    string.span(),
168                    "Identifier parts can only contain [a-zA-Z0-9_]",
169                ))
170            } else {
171                Ok(Self::Str(string))
172            }
173        } else if input.peek(LitChar) {
174            let char = input.parse::<LitChar>()?;
175            let c = char.value();
176            if !c.is_ascii_alphanumeric() && c != '_' {
177                Err(syn::Error::new(
178                    char.span(),
179                    "Identifier parts can only contain [a-zA-Z0-9_]",
180                ))
181            } else {
182                Ok(Self::Char(char))
183            }
184        } else if input.peek(LitByteStr) {
185            Err(syn::Error::new(
186                input.span(),
187                "Identifiers cannot contain byte string",
188            ))
189        } else if input.peek(LitByte) {
190            Err(syn::Error::new(
191                input.span(),
192                "Identifiers cannot contain bytes",
193            ))
194        } else if input.peek(LitFloat) {
195            Err(syn::Error::new(
196                input.span(),
197                "Identifiers cannot contain floats",
198            ))
199        } else {
200            Err(syn::Error::new(
201                input.span(),
202                "Expected either an identifies, a `_`, an int, a bool, \
203                 a string-literal, or a character-literal.\n\
204                 Note: To create an Identifies from a reserved keywords like `struct`, or `return`, \
205                 wrap it quotes, i.e. `\"struct\"`, or escape them with `r#`, i.e. `r#struct` .",
206            ))
207        }
208    }
209}
210
211/// A helper struct that implements [`VisitMut`] and is responsible for replacing the `replace_ident`
212/// with the `concatenated_ident`.
213struct IdentReplacer {
214    replace_ident: Ident,
215    concatenated_ident: Ident,
216    code_block: proc_macro2::TokenStream,
217}
218
219impl IdentReplacer {
220    /// Creates a new Instance of IdentReplacer from an InputParser
221    fn from_input_parser(input_parser: InputParser) -> Self {
222        Self {
223            replace_ident: input_parser.replace_ident,
224            concatenated_ident: input_parser.concatenated_ident,
225            code_block: input_parser.block,
226        }
227    }
228
229    /// Replaces all `replace_idents` in the `code_block` with the `concatenated_ident`
230    fn replace_idents(mut self) -> Self {
231        let mut token_trees = self.code_block.clone().into_iter().collect::<Vec<_>>();
232        for token_tree in token_trees.iter_mut() {
233            match token_tree {
234                proc_macro2::TokenTree::Ident(ref mut ident) => {
235                    if *ident == self.replace_ident {
236                        *ident = self.concatenated_ident.clone();
237                    }
238                }
239                _ => {}
240            }
241        }
242        self.code_block = quote! { #(#token_trees)* };
243        self
244    }
245
246    /// generates a TokenStream from the code-block
247    fn produce_token_stream(self) -> TokenStream {
248        self.code_block.into()
249    }
250}
251
252/// This macros makes it possible to concatenate identifiers at compile time and use them as normal.
253/// It's an extension/replacement of `std::concat_idents`, since in comprassion to the std-solution,
254/// the idents here can be used everywhere.
255///
256/// # Usage:
257/// ### Basic usage
258/// ```
259/// use concat_idents::concat_idents;
260///
261/// concat_idents!(fn_name = foo_, _, bar {
262///        fn fn_name() {
263///            // --snip--
264///        }
265/// });
266///
267/// foo__bar();
268/// ```
269///
270/// ### Generating Tests
271/// ```
272///# use concat_idents::concat_idents;
273///# use std::ops::{Add, Sub};
274/// macro_rules! generate_test {
275///    ($method:ident($lhs:ident, $rhs:ident)) => {
276///        concat_idents!(test_name = $method, _, $lhs, _, $rhs {
277///            #[test]
278///            fn test_name() {
279///                let _ = $lhs::default().$method($rhs::default());
280///            }
281///        });
282///    };
283/// }
284///
285/// #[derive(Default)]
286/// struct S(i32);
287/// impl Add<i32> for S {
288///    // --snip--
289///#    type Output = S;
290///#    fn add(self,rhs: i32) -> Self::Output { S(self.0 + rhs) }
291/// }
292/// impl Sub<i32> for S {
293///    // --snip--
294///#    type Output = S;
295///#    fn sub(self,rhs: i32) -> Self::Output { S(self.0 - rhs) }
296/// }
297///
298/// generate_test!(add(S, i32));
299/// generate_test!(sub(S, i32));
300/// ```
301#[proc_macro]
302pub fn concat_idents(item: TokenStream) -> TokenStream {
303    let input_parser = parse_macro_input!(item as InputParser);
304
305    IdentReplacer::from_input_parser(input_parser)
306        .replace_idents()
307        .produce_token_stream()
308}