macro_string/
lib.rs

1//! [![github]](https://github.com/dtolnay/macro-string) [![crates-io]](https://crates.io/crates/macro-string) [![docs-rs]](https://docs.rs/macro-string)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! This crate is a helper library for procedural macros to perform eager
10//! evaluation of standard library string macros like `concat!` and `env!` in
11//! macro input.
12//!
13//! <table><tr><td>
14//! <b>Supported macros:</b>
15//! <code>concat!</code>,
16//! <code>env!</code>,
17//! <code>include!</code>,
18//! <code>include_str!</code>,
19//! <code>stringify!</code>
20//! </td></tr></table>
21//!
22//! For example, to implement a macro such as the following:
23//!
24//! ```
25//! # macro_rules! include_json {
26//! #     ($path:expr) => { $path };
27//! # }
28//! #
29//! // Parses JSON at compile time and expands to a serde_json::Value.
30//! let j = include_json!(concat!(env!("CARGO_MANIFEST_DIR"), "/manifest.json"));
31//! ```
32//!
33//! the implementation of `include_json!` will need to parse and eagerly
34//! evaluate the two macro calls within its input tokens.
35//!
36//! ```
37//! # extern crate proc_macro;
38//! #
39//! use macro_string::MacroString;
40//! use proc_macro::TokenStream;
41//! use proc_macro2::Span;
42//! use std::fs;
43//! use syn::parse_macro_input;
44//!
45//! # const _: &str = stringify! {
46//! #[proc_macro]
47//! # };
48//! pub fn include_json(input: TokenStream) -> TokenStream {
49//!     let MacroString(path) = parse_macro_input!(input);
50//!
51//!     let content = match fs::read(&path) {
52//!         Ok(content) => content,
53//!         Err(err) => {
54//!             return TokenStream::from(syn::Error::new(Span::call_site(), err).to_compile_error());
55//!         }
56//!     };
57//!
58//!     let json: serde_json::Value = match serde_json::from_slice(&content) {
59//!         Ok(json) => json,
60//!         Err(err) => {
61//!             return TokenStream::from(syn::Error::new(Span::call_site(), err).to_compile_error());
62//!         }
63//!     };
64//!
65//!     /*TODO: print serde_json::Value to TokenStream*/
66//!     # unimplemented!()
67//! }
68//! ```
69
70#![doc(html_root_url = "https://docs.rs/macro-string/0.1.2")]
71
72use proc_macro2::TokenStream;
73use quote::{quote, ToTokens};
74use std::env;
75use std::fs;
76use std::path::Path;
77use syn::parse::{Error, Parse, ParseBuffer, ParseStream, Parser, Result};
78use syn::punctuated::Punctuated;
79use syn::token::{Brace, Bracket, Paren};
80use syn::{
81    braced, bracketed, parenthesized, Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token,
82};
83
84mod kw {
85    syn::custom_keyword!(concat);
86    syn::custom_keyword!(env);
87    syn::custom_keyword!(include);
88    syn::custom_keyword!(include_str);
89    syn::custom_keyword!(stringify);
90}
91
92pub struct MacroString(pub String);
93
94impl Parse for MacroString {
95    fn parse(input: ParseStream) -> Result<Self> {
96        let expr = input.call(Expr::parse_strict)?;
97        let value = expr.eval()?;
98        Ok(MacroString(value))
99    }
100}
101
102enum Expr {
103    LitStr(LitStr),
104    LitChar(LitChar),
105    LitInt(LitInt),
106    LitFloat(LitFloat),
107    LitBool(LitBool),
108    Concat(Concat),
109    Env(Env),
110    Include(Include),
111    IncludeStr(IncludeStr),
112    Stringify(Stringify),
113}
114
115impl Expr {
116    fn eval(&self) -> Result<String> {
117        match self {
118            Expr::LitStr(lit) => Ok(lit.value()),
119            Expr::LitChar(lit) => Ok(lit.value().to_string()),
120            Expr::LitInt(lit) => Ok(lit.base10_digits().to_owned()),
121            Expr::LitFloat(lit) => Ok(lit.base10_digits().to_owned()),
122            Expr::LitBool(lit) => Ok(lit.value.to_string()),
123            Expr::Concat(expr) => {
124                let mut concat = String::new();
125                for arg in &expr.args {
126                    concat += &arg.eval()?;
127                }
128                Ok(concat)
129            }
130            Expr::Env(expr) => {
131                let key = expr.arg.eval()?;
132                match env::var(&key) {
133                    Ok(value) => Ok(value),
134                    Err(err) => Err(Error::new_spanned(expr, err)),
135                }
136            }
137            Expr::Include(expr) => {
138                let path = expr.arg.eval()?;
139                let content = fs_read(&expr, &path)?;
140                let inner = Expr::parse_strict.parse_str(&content)?;
141                inner.eval()
142            }
143            Expr::IncludeStr(expr) => {
144                let path = expr.arg.eval()?;
145                fs_read(&expr, &path)
146            }
147            Expr::Stringify(expr) => Ok(expr.tokens.to_string()),
148        }
149    }
150}
151
152fn fs_read(span: &dyn ToTokens, path: impl AsRef<Path>) -> Result<String> {
153    let path = path.as_ref();
154    if path.is_relative() {
155        let name = span.to_token_stream().into_iter().next().unwrap();
156        return Err(Error::new_spanned(
157            span,
158            format!("a relative path is not supported here; use `{name}!(concat!(env!(\"CARGO_MANIFEST_DIR\"), ...))`"),
159        ));
160    }
161    match fs::read_to_string(path) {
162        Ok(content) => Ok(content),
163        Err(err) => Err(Error::new_spanned(
164            span,
165            format!("{} {}", err, path.display()),
166        )),
167    }
168}
169
170struct Concat {
171    name: kw::concat,
172    bang_token: Token![!],
173    delimiter: MacroDelimiter,
174    args: Punctuated<Expr, Token![,]>,
175}
176
177struct Env {
178    name: kw::env,
179    bang_token: Token![!],
180    delimiter: MacroDelimiter,
181    arg: Box<Expr>,
182    trailing_comma: Option<Token![,]>,
183}
184
185struct Include {
186    name: kw::include,
187    bang_token: Token![!],
188    delimiter: MacroDelimiter,
189    arg: Box<Expr>,
190    trailing_comma: Option<Token![,]>,
191}
192
193struct IncludeStr {
194    name: kw::include_str,
195    bang_token: Token![!],
196    delimiter: MacroDelimiter,
197    arg: Box<Expr>,
198    trailing_comma: Option<Token![,]>,
199}
200
201struct Stringify {
202    name: kw::stringify,
203    bang_token: Token![!],
204    delimiter: MacroDelimiter,
205    tokens: TokenStream,
206}
207
208enum MacroDelimiter {
209    Paren(Paren),
210    Brace(Brace),
211    Bracket(Bracket),
212}
213
214impl Expr {
215    fn parse_strict(input: ParseStream) -> Result<Self> {
216        Self::parse(input, false)
217    }
218
219    fn parse_any(input: ParseStream) -> Result<Self> {
220        Self::parse(input, true)
221    }
222
223    fn parse(input: ParseStream, allow_nonstring_literals: bool) -> Result<Self> {
224        let lookahead = input.lookahead1();
225        if lookahead.peek(LitStr) {
226            let lit: LitStr = input.parse()?;
227            if !lit.suffix().is_empty() {
228                return Err(Error::new(
229                    lit.span(),
230                    "unexpected suffix on string literal",
231                ));
232            }
233            Ok(Expr::LitStr(lit))
234        } else if allow_nonstring_literals && input.peek(LitChar) {
235            let lit: LitChar = input.parse()?;
236            if !lit.suffix().is_empty() {
237                return Err(Error::new(lit.span(), "unexpected suffix on char literal"));
238            }
239            Ok(Expr::LitChar(lit))
240        } else if allow_nonstring_literals && input.peek(LitInt) {
241            let lit: LitInt = input.parse()?;
242            match lit.suffix() {
243                "" | "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64"
244                | "u128" | "f16" | "f32" | "f64" | "f128" => {}
245                _ => {
246                    return Err(Error::new(
247                        lit.span(),
248                        "unexpected suffix on integer literal",
249                    ));
250                }
251            }
252            Ok(Expr::LitInt(lit))
253        } else if allow_nonstring_literals && input.peek(LitFloat) {
254            let lit: LitFloat = input.parse()?;
255            match lit.suffix() {
256                "" | "f16" | "f32" | "f64" | "f128" => {}
257                _ => return Err(Error::new(lit.span(), "unexpected suffix on float literal")),
258            }
259            Ok(Expr::LitFloat(lit))
260        } else if allow_nonstring_literals && input.peek(LitBool) {
261            input.parse().map(Expr::LitBool)
262        } else if lookahead.peek(kw::concat) {
263            input.parse().map(Expr::Concat)
264        } else if lookahead.peek(kw::env) {
265            input.parse().map(Expr::Env)
266        } else if lookahead.peek(kw::include) {
267            input.parse().map(Expr::Include)
268        } else if lookahead.peek(kw::include_str) {
269            input.parse().map(Expr::IncludeStr)
270        } else if lookahead.peek(kw::stringify) {
271            input.parse().map(Expr::Stringify)
272        } else if input.peek(Ident) && input.peek2(Token![!]) && input.peek3(Paren) {
273            let ident: Ident = input.parse()?;
274            let bang_token: Token![!] = input.parse()?;
275            let unsupported = quote!(#ident #bang_token);
276            Err(Error::new_spanned(
277                unsupported,
278                "unsupported macro, expected one of: `concat!`, `env!`, `include!`, `include_str!`, `stringify!`",
279            ))
280        } else {
281            Err(lookahead.error())
282        }
283    }
284}
285
286impl ToTokens for Expr {
287    fn to_tokens(&self, tokens: &mut TokenStream) {
288        match self {
289            Expr::LitStr(expr) => expr.to_tokens(tokens),
290            Expr::LitChar(expr) => expr.to_tokens(tokens),
291            Expr::LitInt(expr) => expr.to_tokens(tokens),
292            Expr::LitFloat(expr) => expr.to_tokens(tokens),
293            Expr::LitBool(expr) => expr.to_tokens(tokens),
294            Expr::Concat(expr) => expr.to_tokens(tokens),
295            Expr::Env(expr) => expr.to_tokens(tokens),
296            Expr::Include(expr) => expr.to_tokens(tokens),
297            Expr::IncludeStr(expr) => expr.to_tokens(tokens),
298            Expr::Stringify(expr) => expr.to_tokens(tokens),
299        }
300    }
301}
302
303macro_rules! macro_delimiter {
304    ($var:ident in $input:ident) => {{
305        let (delim, content) = $input.call(macro_delimiter)?;
306        $var = content;
307        delim
308    }};
309}
310
311fn macro_delimiter(input: ParseStream) -> Result<(MacroDelimiter, ParseBuffer)> {
312    let content;
313    let lookahead = input.lookahead1();
314    let delim = if input.peek(Paren) {
315        MacroDelimiter::Paren(parenthesized!(content in input))
316    } else if input.peek(Brace) {
317        MacroDelimiter::Brace(braced!(content in input))
318    } else if input.peek(Bracket) {
319        MacroDelimiter::Bracket(bracketed!(content in input))
320    } else {
321        return Err(lookahead.error());
322    };
323    Ok((delim, content))
324}
325
326impl MacroDelimiter {
327    fn surround<F>(&self, tokens: &mut TokenStream, f: F)
328    where
329        F: FnOnce(&mut TokenStream),
330    {
331        match self {
332            MacroDelimiter::Paren(delimiter) => delimiter.surround(tokens, f),
333            MacroDelimiter::Brace(delimiter) => delimiter.surround(tokens, f),
334            MacroDelimiter::Bracket(delimiter) => delimiter.surround(tokens, f),
335        }
336    }
337}
338
339impl Parse for Concat {
340    fn parse(input: ParseStream) -> Result<Self> {
341        let content;
342        Ok(Concat {
343            name: input.parse()?,
344            bang_token: input.parse()?,
345            delimiter: macro_delimiter!(content in input),
346            args: Punctuated::parse_terminated_with(&content, Expr::parse_any)?,
347        })
348    }
349}
350
351impl ToTokens for Concat {
352    fn to_tokens(&self, tokens: &mut TokenStream) {
353        self.name.to_tokens(tokens);
354        self.bang_token.to_tokens(tokens);
355        self.delimiter
356            .surround(tokens, |tokens| self.args.to_tokens(tokens));
357    }
358}
359
360impl Parse for Env {
361    fn parse(input: ParseStream) -> Result<Self> {
362        let content;
363        Ok(Env {
364            name: input.parse()?,
365            bang_token: input.parse()?,
366            delimiter: macro_delimiter!(content in input),
367            arg: Expr::parse_strict(&content).map(Box::new)?,
368            trailing_comma: content.parse()?,
369        })
370    }
371}
372
373impl ToTokens for Env {
374    fn to_tokens(&self, tokens: &mut TokenStream) {
375        self.name.to_tokens(tokens);
376        self.bang_token.to_tokens(tokens);
377        self.delimiter.surround(tokens, |tokens| {
378            self.arg.to_tokens(tokens);
379            self.trailing_comma.to_tokens(tokens);
380        });
381    }
382}
383
384impl Parse for Include {
385    fn parse(input: ParseStream) -> Result<Self> {
386        let content;
387        Ok(Include {
388            name: input.parse()?,
389            bang_token: input.parse()?,
390            delimiter: macro_delimiter!(content in input),
391            arg: Expr::parse_strict(&content).map(Box::new)?,
392            trailing_comma: content.parse()?,
393        })
394    }
395}
396
397impl ToTokens for Include {
398    fn to_tokens(&self, tokens: &mut TokenStream) {
399        self.name.to_tokens(tokens);
400        self.bang_token.to_tokens(tokens);
401        self.delimiter.surround(tokens, |tokens| {
402            self.arg.to_tokens(tokens);
403            self.trailing_comma.to_tokens(tokens);
404        });
405    }
406}
407
408impl Parse for IncludeStr {
409    fn parse(input: ParseStream) -> Result<Self> {
410        let content;
411        Ok(IncludeStr {
412            name: input.parse()?,
413            bang_token: input.parse()?,
414            delimiter: macro_delimiter!(content in input),
415            arg: Expr::parse_strict(&content).map(Box::new)?,
416            trailing_comma: content.parse()?,
417        })
418    }
419}
420
421impl ToTokens for IncludeStr {
422    fn to_tokens(&self, tokens: &mut TokenStream) {
423        self.name.to_tokens(tokens);
424        self.bang_token.to_tokens(tokens);
425        self.delimiter.surround(tokens, |tokens| {
426            self.arg.to_tokens(tokens);
427            self.trailing_comma.to_tokens(tokens);
428        });
429    }
430}
431
432impl Parse for Stringify {
433    fn parse(input: ParseStream) -> Result<Self> {
434        let content;
435        Ok(Stringify {
436            name: input.parse()?,
437            bang_token: input.parse()?,
438            delimiter: macro_delimiter!(content in input),
439            tokens: content.parse()?,
440        })
441    }
442}
443
444impl ToTokens for Stringify {
445    fn to_tokens(&self, tokens: &mut TokenStream) {
446        self.name.to_tokens(tokens);
447        self.bang_token.to_tokens(tokens);
448        self.delimiter
449            .surround(tokens, |tokens| self.tokens.to_tokens(tokens));
450    }
451}