inline_mod/
lib.rs

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}