stringify_inner/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use core::{convert::identity, str::FromStr as _};
4
5use proc_macro::{
6    Delimiter, Group, Ident, Literal, Punct, Spacing::*, Span, TokenStream,
7    TokenTree,
8};
9use proc_macro_tool::{
10    rerr, stream, streams, try_pfunc, SetSpan as _, TokenTreeExt as _,
11};
12
13fn extract_str(s: &str, span: Span) -> Result<String, TokenStream> {
14    if !s.ends_with(['"', '#']) {
15        return rerr("invalid string suffix", span);
16    }
17    if s.starts_with('"') {
18        return Ok(s[1..s.len()-1].to_owned());
19    }
20    if !s.starts_with('r') {
21        return rerr("invalid string literal", span);
22    }
23    let mut s = &s[1..];
24    while s.starts_with('#') {
25        s = &s[1..s.len()-1];
26    }
27    let escaped = format!("{:?}", &s[1..s.len()-1]);
28    debug_assert!(escaped.starts_with('"') && escaped.ends_with('"'), "{escaped}");
29    Ok(escaped[1..escaped.len()-1].to_string())
30}
31
32fn merge_str(a: &Literal, b: &Literal) -> Result<Literal, TokenStream> {
33    let (sa, sb) = (a.span(), b.span());
34    let (a, b) = (a.to_string(), b.to_string());
35    let (a, b) = (extract_str(&a, sa)?, extract_str(&b, sb)?);
36    let lit = Literal::from_str(&format!("\"{a}{b}\"")).unwrap();
37    Ok(lit.set_spaned(sa))
38}
39
40fn do_operation(i: Ident, group: Group) -> Result<TokenStream, TokenStream> {
41    Ok(match &*i.to_string() {
42        "concat" => {
43            let gspan = group.span();
44            let param = expr_impl(group.stream())?;
45            let mut s = Literal::string("");
46            let mut span = None;
47            let mut iter = param.into_iter().peekable();
48
49            while let Some(tt) = iter.next() {
50                iter.next_if(|p| p.is_punch(','));
51                let TokenTree::Literal(lit) = tt else {
52                    return rerr("is not a literal", tt.span());
53                };
54                span.get_or_insert(lit.span());
55                s = merge_str(&s, &lit)?;
56            }
57
58            s.set_span(span.unwrap_or(gspan));
59            stream([TokenTree::from(s)])
60        },
61        "stringify" => {
62            let out_span = group.stream().into_iter().next()
63                .map_or(group.span(), |t| t.span());
64
65            let s = group.stream().to_string();
66            let tt = Literal::string(&s).set_spaned(out_span);
67            stream([TokenTree::from(tt)])
68        },
69        _ => unreachable!(),
70    })
71}
72
73fn expr_impl(stream: TokenStream) -> Result<TokenStream, TokenStream> {
74    try_pfunc(
75        stream,
76        false,
77        ["concat", "stringify"],
78        do_operation,
79    )
80}
81
82/// Run string expressions on expressions
83///
84/// - `#stringify(...)`: like `stringify!(...)`
85/// - `#concat(...)`: like `concat!(...)`
86///
87/// # Examples
88/// ```
89/// use stringify_inner::sexpr;
90///
91/// assert_eq!(sexpr!(#stringify(foo)), "foo");
92/// assert_eq!(sexpr!(&#stringify(foo)[1..]), "oo");
93/// assert_eq!(sexpr!(#concat(#stringify(foo), "bar")), "foobar");
94/// ```
95#[proc_macro]
96pub fn sexpr(stream: TokenStream) -> TokenStream {
97    expr_impl(stream).map_or_else(identity, identity)
98}
99
100/// Run string expressions on attribute
101///
102/// - `#stringify(...)`: like `stringify!(...)`
103/// - `#concat(...)`: like `concat!(...)`
104///
105/// # Examples
106/// ```
107/// use stringify_inner::sexpr_attr;
108///
109/// #[sexpr_attr(doc(alias = #stringify(bar)))]
110/// fn foo() {}
111/// ```
112#[proc_macro_attribute]
113pub fn sexpr_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
114    streams([
115        expr_impl(stream([
116            TokenTree::from(Punct::new('#', Joint)),
117            Group::new(Delimiter::Bracket, attr).into(),
118        ])).map_or_else(identity, identity),
119        item,
120    ])
121}