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.4")]
71
72use proc_macro2::TokenStream;
73use quote::{quote, ToTokens};
74use std::env;
75use std::fs;
76use std::path::{Component, Path, PathBuf};
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 mut 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
162    // Make Windows verbatim paths work even with mixed path separators, which
163    // can happen when a path is produced using `concat!`.
164    let path_buf: PathBuf;
165    if let Some(Component::Prefix(prefix)) = path.components().next() {
166        if prefix.kind().is_verbatim() {
167            path_buf = path.components().collect();
168            path = &path_buf;
169        }
170    }
171
172    match fs::read_to_string(path) {
173        Ok(content) => Ok(content),
174        Err(err) => Err(Error::new_spanned(
175            span,
176            format!("{} {}", err, path.display()),
177        )),
178    }
179}
180
181struct Concat {
182    name: kw::concat,
183    bang_token: Token![!],
184    delimiter: MacroDelimiter,
185    args: Punctuated<Expr, Token![,]>,
186}
187
188struct Env {
189    name: kw::env,
190    bang_token: Token![!],
191    delimiter: MacroDelimiter,
192    arg: Box<Expr>,
193    trailing_comma: Option<Token![,]>,
194}
195
196struct Include {
197    name: kw::include,
198    bang_token: Token![!],
199    delimiter: MacroDelimiter,
200    arg: Box<Expr>,
201    trailing_comma: Option<Token![,]>,
202}
203
204struct IncludeStr {
205    name: kw::include_str,
206    bang_token: Token![!],
207    delimiter: MacroDelimiter,
208    arg: Box<Expr>,
209    trailing_comma: Option<Token![,]>,
210}
211
212struct Stringify {
213    name: kw::stringify,
214    bang_token: Token![!],
215    delimiter: MacroDelimiter,
216    tokens: TokenStream,
217}
218
219enum MacroDelimiter {
220    Paren(Paren),
221    Brace(Brace),
222    Bracket(Bracket),
223}
224
225impl Expr {
226    fn parse_strict(input: ParseStream) -> Result<Self> {
227        Self::parse(input, false)
228    }
229
230    fn parse_any(input: ParseStream) -> Result<Self> {
231        Self::parse(input, true)
232    }
233
234    fn parse(input: ParseStream, allow_nonstring_literals: bool) -> Result<Self> {
235        let lookahead = input.lookahead1();
236        if lookahead.peek(LitStr) {
237            let lit: LitStr = input.parse()?;
238            if !lit.suffix().is_empty() {
239                return Err(Error::new(
240                    lit.span(),
241                    "unexpected suffix on string literal",
242                ));
243            }
244            Ok(Expr::LitStr(lit))
245        } else if allow_nonstring_literals && input.peek(LitChar) {
246            let lit: LitChar = input.parse()?;
247            if !lit.suffix().is_empty() {
248                return Err(Error::new(lit.span(), "unexpected suffix on char literal"));
249            }
250            Ok(Expr::LitChar(lit))
251        } else if allow_nonstring_literals && input.peek(LitInt) {
252            let lit: LitInt = input.parse()?;
253            match lit.suffix() {
254                "" | "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64"
255                | "u128" | "f16" | "f32" | "f64" | "f128" => {}
256                _ => {
257                    return Err(Error::new(
258                        lit.span(),
259                        "unexpected suffix on integer literal",
260                    ));
261                }
262            }
263            Ok(Expr::LitInt(lit))
264        } else if allow_nonstring_literals && input.peek(LitFloat) {
265            let lit: LitFloat = input.parse()?;
266            match lit.suffix() {
267                "" | "f16" | "f32" | "f64" | "f128" => {}
268                _ => return Err(Error::new(lit.span(), "unexpected suffix on float literal")),
269            }
270            Ok(Expr::LitFloat(lit))
271        } else if allow_nonstring_literals && input.peek(LitBool) {
272            input.parse().map(Expr::LitBool)
273        } else if lookahead.peek(kw::concat) {
274            input.parse().map(Expr::Concat)
275        } else if lookahead.peek(kw::env) {
276            input.parse().map(Expr::Env)
277        } else if lookahead.peek(kw::include) {
278            input.parse().map(Expr::Include)
279        } else if lookahead.peek(kw::include_str) {
280            input.parse().map(Expr::IncludeStr)
281        } else if lookahead.peek(kw::stringify) {
282            input.parse().map(Expr::Stringify)
283        } else if input.peek(Ident) && input.peek2(Token![!]) && input.peek3(Paren) {
284            let ident: Ident = input.parse()?;
285            let bang_token: Token![!] = input.parse()?;
286            let unsupported = quote!(#ident #bang_token);
287            Err(Error::new_spanned(
288                unsupported,
289                "unsupported macro, expected one of: `concat!`, `env!`, `include!`, `include_str!`, `stringify!`",
290            ))
291        } else {
292            Err(lookahead.error())
293        }
294    }
295}
296
297impl ToTokens for Expr {
298    fn to_tokens(&self, tokens: &mut TokenStream) {
299        match self {
300            Expr::LitStr(expr) => expr.to_tokens(tokens),
301            Expr::LitChar(expr) => expr.to_tokens(tokens),
302            Expr::LitInt(expr) => expr.to_tokens(tokens),
303            Expr::LitFloat(expr) => expr.to_tokens(tokens),
304            Expr::LitBool(expr) => expr.to_tokens(tokens),
305            Expr::Concat(expr) => expr.to_tokens(tokens),
306            Expr::Env(expr) => expr.to_tokens(tokens),
307            Expr::Include(expr) => expr.to_tokens(tokens),
308            Expr::IncludeStr(expr) => expr.to_tokens(tokens),
309            Expr::Stringify(expr) => expr.to_tokens(tokens),
310        }
311    }
312}
313
314macro_rules! macro_delimiter {
315    ($var:ident in $input:ident) => {{
316        let (delim, content) = $input.call(macro_delimiter)?;
317        $var = content;
318        delim
319    }};
320}
321
322fn macro_delimiter(input: ParseStream) -> Result<(MacroDelimiter, ParseBuffer)> {
323    let content;
324    let lookahead = input.lookahead1();
325    let delim = if input.peek(Paren) {
326        MacroDelimiter::Paren(parenthesized!(content in input))
327    } else if input.peek(Brace) {
328        MacroDelimiter::Brace(braced!(content in input))
329    } else if input.peek(Bracket) {
330        MacroDelimiter::Bracket(bracketed!(content in input))
331    } else {
332        return Err(lookahead.error());
333    };
334    Ok((delim, content))
335}
336
337impl MacroDelimiter {
338    fn surround<F>(&self, tokens: &mut TokenStream, f: F)
339    where
340        F: FnOnce(&mut TokenStream),
341    {
342        match self {
343            MacroDelimiter::Paren(delimiter) => delimiter.surround(tokens, f),
344            MacroDelimiter::Brace(delimiter) => delimiter.surround(tokens, f),
345            MacroDelimiter::Bracket(delimiter) => delimiter.surround(tokens, f),
346        }
347    }
348}
349
350impl Parse for Concat {
351    fn parse(input: ParseStream) -> Result<Self> {
352        let content;
353        Ok(Concat {
354            name: input.parse()?,
355            bang_token: input.parse()?,
356            delimiter: macro_delimiter!(content in input),
357            args: Punctuated::parse_terminated_with(&content, Expr::parse_any)?,
358        })
359    }
360}
361
362impl ToTokens for Concat {
363    fn to_tokens(&self, tokens: &mut TokenStream) {
364        self.name.to_tokens(tokens);
365        self.bang_token.to_tokens(tokens);
366        self.delimiter
367            .surround(tokens, |tokens| self.args.to_tokens(tokens));
368    }
369}
370
371impl Parse for Env {
372    fn parse(input: ParseStream) -> Result<Self> {
373        let content;
374        Ok(Env {
375            name: input.parse()?,
376            bang_token: input.parse()?,
377            delimiter: macro_delimiter!(content in input),
378            arg: Expr::parse_strict(&content).map(Box::new)?,
379            trailing_comma: content.parse()?,
380        })
381    }
382}
383
384impl ToTokens for Env {
385    fn to_tokens(&self, tokens: &mut TokenStream) {
386        self.name.to_tokens(tokens);
387        self.bang_token.to_tokens(tokens);
388        self.delimiter.surround(tokens, |tokens| {
389            self.arg.to_tokens(tokens);
390            self.trailing_comma.to_tokens(tokens);
391        });
392    }
393}
394
395impl Parse for Include {
396    fn parse(input: ParseStream) -> Result<Self> {
397        let content;
398        Ok(Include {
399            name: input.parse()?,
400            bang_token: input.parse()?,
401            delimiter: macro_delimiter!(content in input),
402            arg: Expr::parse_strict(&content).map(Box::new)?,
403            trailing_comma: content.parse()?,
404        })
405    }
406}
407
408impl ToTokens for Include {
409    fn to_tokens(&self, tokens: &mut TokenStream) {
410        self.name.to_tokens(tokens);
411        self.bang_token.to_tokens(tokens);
412        self.delimiter.surround(tokens, |tokens| {
413            self.arg.to_tokens(tokens);
414            self.trailing_comma.to_tokens(tokens);
415        });
416    }
417}
418
419impl Parse for IncludeStr {
420    fn parse(input: ParseStream) -> Result<Self> {
421        let content;
422        Ok(IncludeStr {
423            name: input.parse()?,
424            bang_token: input.parse()?,
425            delimiter: macro_delimiter!(content in input),
426            arg: Expr::parse_strict(&content).map(Box::new)?,
427            trailing_comma: content.parse()?,
428        })
429    }
430}
431
432impl ToTokens for IncludeStr {
433    fn to_tokens(&self, tokens: &mut TokenStream) {
434        self.name.to_tokens(tokens);
435        self.bang_token.to_tokens(tokens);
436        self.delimiter.surround(tokens, |tokens| {
437            self.arg.to_tokens(tokens);
438            self.trailing_comma.to_tokens(tokens);
439        });
440    }
441}
442
443impl Parse for Stringify {
444    fn parse(input: ParseStream) -> Result<Self> {
445        let content;
446        Ok(Stringify {
447            name: input.parse()?,
448            bang_token: input.parse()?,
449            delimiter: macro_delimiter!(content in input),
450            tokens: content.parse()?,
451        })
452    }
453}
454
455impl ToTokens for Stringify {
456    fn to_tokens(&self, tokens: &mut TokenStream) {
457        self.name.to_tokens(tokens);
458        self.bang_token.to_tokens(tokens);
459        self.delimiter
460            .surround(tokens, |tokens| self.tokens.to_tokens(tokens));
461    }
462}