ident_str/
lib.rs

1//! A macro to convert string literals into identifiers.  The primary use-case is to allow
2//! declarative macros to produce identifiers that are unique to each call.  The alternative is to
3//! accept each identifier as an argument to the macro call, which gets unweildy with many
4//! identifiers.
5//!
6//! # Usage
7//!
8//! ```
9//! macro_rules! my_macro {
10//!     ($name: ident) => {
11//!         ident_str::ident_str! {
12//!             #name_a = concat!(stringify!($name), "_a"),
13//!             #name_b = concat!(stringify!($name), "_b")
14//!             => {
15//!                 fn #name_a() {}
16//!                 fn #name_b() {}
17//!             }
18//!         }
19//!     };
20//! }
21//!
22//! my_macro!(foo);
23//! ```
24//!
25//! expands to
26//!
27//! ```
28//! fn foo_a() {}
29//! fn foo_b() {}
30//! ```
31//!
32//! # Supported Macros
33//!
34//! This crate takes advantage of [`MacroString`](https://github.com/dtolnay/macro-string/) and
35//! supports all macros supported by it.  Those macros are:
36//! - `concat!`
37//! - `stringify!`
38//! - `env!`
39//! - `include!`
40//! - `include_str!`
41//!
42//! # Ignore Variable
43//!
44//! When any unknown variables are encountered, `ident_str!` will error, if that behaviour is not
45//! desired, you can add `#<var> = None` to the declarations:
46//!
47//! ```
48//! # // Weird stringify! magic is to make this example compile
49//! ident_str::ident_str! {
50//!     #ignore = None
51//! #   => const _: &str = stringify!(
52//!     => #ignore
53//! #   );
54//! }
55//! ```
56//!
57//! This exapands into
58//!
59//! ```ignore
60//! #ignore
61//! ```
62
63use std::collections::HashMap;
64
65use macro_string::MacroString;
66use proc_macro::TokenStream;
67use proc_macro2::{Group, Ident, TokenStream as TokenStream2, TokenTree};
68use quote::{ToTokens, TokenStreamExt};
69use syn::{
70    Token,
71    parse::{Parse, ParseStream},
72    parse_macro_input,
73    punctuated::Punctuated,
74};
75
76enum Value {
77    MacroString(MacroString),
78    None,
79}
80
81impl Value {
82    pub fn to_string(&self) -> Option<String> {
83        match self {
84            Value::MacroString(MacroString(n)) => Some(n.clone()),
85            Value::None => None,
86        }
87    }
88}
89
90impl Parse for Value {
91    fn parse(input: ParseStream) -> syn::Result<Self> {
92        if input
93            .step(|x| {
94                if let Some((ident, cursor)) = x.ident()
95                    && ident == "None"
96                {
97                    Ok((ident, cursor))
98                } else {
99                    Err(x.error("Expected string or None"))
100                }
101            })
102            .is_ok()
103        {
104            Ok(Value::None)
105        } else {
106            Ok(Value::MacroString(input.parse()?))
107        }
108    }
109}
110
111struct Decl {
112    _hash: Token![#],
113    ident: Ident,
114    _eq_token: Token![=],
115    value: Value,
116}
117
118impl Decl {
119    fn name_to_tokens(&self) -> TokenStream2 {
120        let mut tokens = TokenStream2::new();
121        self._hash.to_tokens(&mut tokens);
122        self.ident.to_tokens(&mut tokens);
123        tokens
124    }
125}
126
127impl Parse for Decl {
128    fn parse(input: ParseStream) -> syn::Result<Self> {
129        let hash = input.parse()?;
130        let ident = input.parse()?;
131        let eq_token = input.parse()?;
132        let value = input.parse()?;
133        Ok(Self {
134            _hash: hash,
135            ident,
136            _eq_token: eq_token,
137            value,
138        })
139    }
140}
141
142struct Decls {
143    decls: Punctuated<Decl, Token![,]>,
144    _arrow: Token![=>],
145    body: TokenStream2,
146}
147
148impl Parse for Decls {
149    fn parse(input: ParseStream) -> syn::Result<Self> {
150        Ok(Self {
151            decls: Punctuated::parse_separated_nonempty(input)?,
152            _arrow: input.parse()?,
153            // TODO: emit warning when stream does not use a variable (proc_macro::Diagnostic to be
154            // stabilised)
155            body: {
156                if input.peek(syn::token::Brace) {
157                    let g: Group = input.parse()?;
158                    g.stream()
159                } else {
160                    input.parse()?
161                }
162            },
163        })
164    }
165}
166
167fn append_error(errors: &mut Option<syn::Error>, new: syn::Error) {
168    if let Some(errors) = errors {
169        errors.combine(new);
170    } else {
171        *errors = Some(new);
172    }
173}
174
175fn translate_stream(
176    stream: TokenStream2,
177    map: &HashMap<String, Decl>,
178    errors: &mut Option<syn::Error>,
179) -> TokenStream2 {
180    let mut out = TokenStream2::new();
181    let mut iter = stream.into_iter().peekable();
182    while let Some(tok) = iter.next() {
183        match tok {
184            TokenTree::Ident(_) => out.append(tok),
185            TokenTree::Group(group) => {
186                let mut group = Group::new(
187                    group.delimiter(),
188                    translate_stream(group.stream(), map, errors),
189                );
190                group.set_span(group.span());
191                out.append(TokenTree::Group(group));
192            }
193            TokenTree::Punct(ref p) if p.as_char() == '#' => {
194                if let Some(TokenTree::Ident(_)) = iter.peek() {
195                    let Some(TokenTree::Ident(ident)) = iter.next() else {
196                        unreachable!();
197                    };
198                    let strident = ident.to_string();
199                    if let Some(decl) = map.get(&strident) {
200                        let ident = if let Some(value) = &decl.value.to_string() {
201                            Ident::new(value, ident.span()) // this won't panic, as we checked the string in main
202                        } else {
203                            out.append(tok);
204                            ident
205                        };
206                        out.append(TokenTree::Ident(ident));
207                    } else {
208                        let mut tokens = TokenStream2::new();
209                        tokens.append(tok);
210                        tokens.append(ident);
211                        append_error(
212                            errors,
213                            // TODO: Replace this with Diagnostic to get better error message
214                            syn::Error::new_spanned(
215                                tokens,
216                                format!(
217                                    "Unknown ident variable.  If you intended to literally use '#{0}', add `#{0} = None` to the declarations",
218                                    strident
219                                ),
220                            ),
221                        );
222                        continue;
223                    }
224                } else {
225                    out.append(tok);
226                }
227            }
228            TokenTree::Punct(_) | TokenTree::Literal(_) => out.append(tok),
229        }
230    }
231
232    out
233}
234
235/// The main macro.
236///
237/// Accepts any number of `name = <string literal>` pairs (or using macros like `concat!` or
238/// `stringify!`), followed by `=>` and then the body that will be expanded (optionally surrounded
239/// by `{}`).
240///
241/// ```
242/// ident_str::ident_str! {
243///     #name = "hello_world" =>
244///     fn #name() -> &'static str {
245///         stringify!(#name)
246///     }
247/// }
248///
249/// # fn main() {
250/// #     assert_eq!(hello_world(), "hello_world");
251/// # }
252/// ```
253///
254#[proc_macro]
255pub fn ident_str(input: TokenStream) -> TokenStream {
256    let decls = parse_macro_input!(input as Decls);
257    let mut map = HashMap::<String, Decl>::with_capacity(decls.decls.len());
258    let mut errors: Option<syn::Error> = None;
259    let mut can_continue = true;
260    for d in decls.decls.into_iter() {
261        let strident = d.ident.to_string();
262        let existing = map.get(&strident);
263        if existing.is_some() {
264            append_error(
265                &mut errors,
266                syn::Error::new_spanned(
267                    d.name_to_tokens(),
268                    format!("Redefinition of #{}", d.ident),
269                ),
270            );
271        }
272
273        if let Some(valstring) = d.value.to_string()
274            && syn::parse_str::<Ident>(&valstring).is_err()
275        {
276            append_error(
277                &mut errors,
278                syn::Error::new_spanned(
279                    d.name_to_tokens(),
280                    format!("Invalid identifier: {:?}", valstring),
281                ),
282            );
283            can_continue = false;
284        }
285        map.insert(strident, d);
286    }
287
288    let mut tokens = if can_continue {
289        translate_stream(decls.body, &map, &mut errors)
290    } else {
291        debug_assert!(errors.is_some());
292        TokenStream2::new()
293    };
294
295    if let Some(errors) = errors {
296        errors.to_compile_error().to_tokens(&mut tokens);
297    }
298
299    tokens.into()
300}