1#![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}