compose_idents/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod core;
4mod eval;
5mod funcs;
6
7use crate::core::{Expr, State};
8use crate::eval::Eval;
9use proc_macro::TokenStream;
10use quote::{format_ident, quote};
11use std::collections::HashMap;
12use syn::{
13    bracketed,
14    parse::{Parse, ParseStream},
15    parse_macro_input,
16    visit_mut::VisitMut,
17    Block, Ident, LitStr, Token,
18};
19
20struct IdentSpecItem {
21    alias: Ident,
22    exprs: Vec<Expr>,
23}
24
25impl IdentSpecItem {
26    fn replacement(&self, state: &State) -> Ident {
27        let ident = self.exprs.iter().fold("".to_string(), |acc, item| {
28            format!("{}{}", acc, item.eval(state))
29        });
30        format_ident!("{}", ident)
31    }
32}
33
34struct IdentSpec {
35    items: Vec<IdentSpecItem>,
36}
37
38impl Parse for IdentSpec {
39    fn parse(input: ParseStream) -> syn::Result<Self> {
40        let mut items = Vec::new();
41        loop {
42            let alias: Ident = input.parse()?;
43            input.parse::<Token![=]>()?;
44            let content;
45            bracketed!(content in input);
46            let mut exprs = Vec::new();
47            loop {
48                match content.parse::<Expr>() {
49                    Ok(expr) => exprs.push(expr),
50                    Err(err) => return Err(err),
51                }
52                if content.is_empty() {
53                    break;
54                }
55                content.parse::<Token![,]>()?;
56            }
57            items.push(IdentSpecItem { alias, exprs });
58            input.parse::<Token![;]>()?;
59            if !input.peek(Ident) {
60                break;
61            }
62        }
63        Ok(IdentSpec { items })
64    }
65}
66
67impl IdentSpec {
68    fn replacements(&self, state: &State) -> HashMap<Ident, Ident> {
69        self.items
70            .iter()
71            .map(|item| (item.alias.clone(), item.replacement(state)))
72            .collect()
73    }
74}
75
76struct ComposeIdentsArgs {
77    spec: IdentSpec,
78    block: Block,
79}
80
81impl Parse for ComposeIdentsArgs {
82    fn parse(input: ParseStream) -> syn::Result<Self> {
83        let spec: IdentSpec = input.parse()?;
84        let block: Block = input.parse()?;
85        let _ = input.parse::<Token![;]>();
86        Ok(ComposeIdentsArgs { spec, block })
87    }
88}
89
90struct ComposeIdentsVisitor {
91    replacements: HashMap<Ident, Ident>,
92}
93
94impl VisitMut for ComposeIdentsVisitor {
95    fn visit_ident_mut(&mut self, ident: &mut Ident) {
96        if let Some(replacement) = self.replacements.get(ident) {
97            *ident = replacement.clone();
98        }
99    }
100
101    fn visit_lit_str_mut(&mut self, i: &mut LitStr) {
102        let value = i.value();
103        let mut formatted = i.value().clone();
104
105        for (alias, repl) in self.replacements.iter() {
106            formatted = formatted.replace(&format!("%{}%", alias), &repl.to_string());
107        }
108
109        if formatted != value {
110            *i = LitStr::new(&formatted, i.span());
111        }
112    }
113}
114
115static mut COUNTER: u64 = 0;
116
117/// Compose identifiers from the provided parts and replace their aliases in the code block.
118///
119/// In addition to replacing identifier aliases it replaces tokens like `%alias%` in string
120/// literals (including in doc-attributes).
121///
122/// # Example
123///
124/// ```rust
125#[doc = include_str!("../snippets/usage.rs")]
126/// ```
127#[proc_macro]
128pub fn compose_idents(input: TokenStream) -> TokenStream {
129    let state = State {
130        seed: unsafe {
131            COUNTER += 1;
132            COUNTER
133        },
134    };
135    let args = parse_macro_input!(input as ComposeIdentsArgs);
136    let mut visitor = ComposeIdentsVisitor {
137        replacements: args.spec.replacements(&state),
138    };
139    let mut block = args.block;
140    visitor.visit_block_mut(&mut block);
141
142    let block_content = block.stmts;
143
144    let expanded = quote! { #(#block_content)* };
145    TokenStream::from(expanded)
146}