1#![cfg_attr(feature = "docs",
2 cfg_attr(all(), doc = include_str!("../README.md")),
3)]
4use std::{
5 fs::read_to_string,
6 path::{Path, PathBuf},
7};
8
9use proc_macro::TokenStream;
10use proc_macro2::{Span, TokenStream as TokenStream2};
11use quote::{quote, ToTokens};
12use syn::{
13 parse2, parse_file, spanned::Spanned, Error, File, Item, ItemMod, Lit, LitStr, Meta,
14 MetaNameValue, Result,
15};
16
17#[proc_macro]
18pub fn inline_mod(input: TokenStream) -> TokenStream {
19 inline_mod_impl(input.into(), None)
20 .unwrap_or_else(|err| err.to_compile_error())
21 .into()
22}
23
24fn inline_mod_impl(input: TokenStream2, default_path: Option<LitStr>) -> Result<TokenStream2> {
25 let input: ItemMod = parse2(input)?;
26 if let Some((brace, _)) = input.content {
27 return Err(Error::new(
28 brace.span,
29 "This macro only accepts non-inlined modules",
30 ));
31 }
32 let path = input
33 .attrs
34 .iter()
35 .find_map(|attr| match attr.parse_meta() {
36 Ok(Meta::NameValue(MetaNameValue {
37 path,
38 lit: Lit::Str(lit),
39 ..
40 })) if path.is_ident("path") => Some(lit),
41 _ => None,
42 })
43 .or(default_path)
44 .ok_or_else(|| Error::new(Span::call_site(), "Path attribute is required"))?;
45 let (path, path_span) = (path.value(), path.span());
46 let mut path = PathBuf::from(path);
47 if path.is_relative() {
48 path = Path::new(
49 &std::env::var_os("CARGO_MANIFEST_DIR").expect("Missing `CARGO_MANIFEST_DIR` variable"),
50 )
51 .join(path);
52 }
53 let path_str = path.to_str().unwrap();
54 let root = path_str
55 .strip_suffix("/mod.rs")
56 .or_else(|| path_str.strip_suffix(".rs"))
57 .unwrap_or(path_str);
58 let root = Path::new(root);
59 let ItemMod {
60 ident, vis, attrs, ..
61 } = input;
62
63 let File {
64 attrs: file_attrs,
65 items,
66 ..
67 } = {
68 let content = read_to_string(&path).map_err(|err| {
69 Error::new(
70 path_span,
71 format!(
72 "Error reading module `{}` (path = `{:?}`): {}",
73 &ident, path, err
74 ),
75 )
76 })?;
77 parse_file(&content)?
78 };
79
80 let items = items.into_iter().map(|item| match item {
81 Item::Mod(module) if module.content.is_none() => {
82 let mut mod_path = root.join(format!("{}.rs", module.ident));
83 if !mod_path.is_file() {
84 mod_path = root.join(format!("{}/mod.rs", module.ident));
85 }
86 let mod_path = LitStr::new(mod_path.to_str().unwrap(), module.span());
87 inline_mod_impl(module.into_token_stream(), Some(mod_path))
88 .unwrap_or_else(|err| err.to_compile_error())
89 }
90 _ => item.into_token_stream(),
91 });
92
93 Ok(quote! {
94 const _: &[::core::primitive::u8] = ::core::include_bytes!( #path_str ).as_slice();
95 #( #attrs )*
96 #vis mod #ident {
97 #( #file_attrs )*
98 #( #items )*
99 }
100 })
101}