Skip to main content

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