pub trait TopLevelMeta: Sized {
fn from_attributes(attributes: &[syn::Attribute]) -> syn::Result<Self>;
}
impl TopLevelMeta for () {
#[mutants::skip]
fn from_attributes(_attributes: &[syn::Attribute]) -> syn::Result<Self> {
Ok(())
}
}
pub trait Derive {
type Input;
type TopLevelMeta: TopLevelMeta;
type Parsed<'i>;
type MappingPaths;
type Model<'p>;
type Ir<'p>;
fn input(derive_input: syn::DeriveInput) -> Self::Input;
fn parse(input: &Self::Input) -> syn::Result<Self::Parsed<'_>>;
fn map(
parsed: &Self::Parsed<'_>,
top_level_meta: Self::TopLevelMeta,
) -> syn::Result<Self::MappingPaths>;
fn analyze<'p>(parsed: &'p Self::Parsed<'_>) -> syn::Result<Self::Model<'p>>;
fn lower<'p>(model: Self::Model<'p>, parsed: &'p Self::Parsed<'_>) -> Self::Ir<'p>;
fn code_gen(ir: Self::Ir<'_>, mapping_paths: Self::MappingPaths) -> proc_macro2::TokenStream;
fn runtime(mapping_paths: &Self::MappingPaths) -> &syn::Path;
}
#[must_use]
pub fn expand<D: Derive>(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
syn::parse2::<syn::DeriveInput>(input)
.and_then(|derive_input| {
let top_level_meta = D::TopLevelMeta::from_attributes(&derive_input.attrs);
let input = D::input(derive_input);
run::<D>(&input, top_level_meta)
})
.unwrap_or_else(syn::Error::into_compile_error)
}
fn run<D: Derive>(
input: &D::Input,
top_level_meta: syn::Result<D::TopLevelMeta>,
) -> syn::Result<proc_macro2::TokenStream> {
let (parsed, top_level_meta) = crate::errors::both(D::parse(input), top_level_meta)?;
let (mapping_paths, model) =
crate::errors::both(D::map(&parsed, top_level_meta), D::analyze(&parsed))?;
let ir = D::lower(model, &parsed);
let version_check = version_check(D::runtime(&mapping_paths));
let mut code = D::code_gen(ir, mapping_paths);
code.extend(version_check);
Ok(code)
}
fn version_check(runtime: &syn::Path) -> proc_macro2::TokenStream {
let major = env!("CARGO_PKG_VERSION_MAJOR")
.parse::<u64>()
.map_or_else(|_| quote::quote!(0), |major| quote::quote!(#major));
let minor = env!("CARGO_PKG_VERSION_MINOR")
.parse::<u64>()
.map_or_else(|_| quote::quote!(0), |minor| quote::quote!(#minor));
quote::quote! {
const _: () = #runtime::__private::require_version(#major, #minor);
}
}
#[cfg(test)]
mod tests {
use super::{Derive, TopLevelMeta, expand};
struct Meta(Option<syn::Ident>);
impl TopLevelMeta for Meta {
fn from_attributes(attributes: &[syn::Attribute]) -> syn::Result<Self> {
match attributes.first() {
Some(attribute) if attribute.path().is_ident("bad_meta") => {
Err(syn::Error::new_spanned(attribute, "bad meta"))
},
attribute => {
Ok(Self(attribute.and_then(|attribute| {
attribute.path().get_ident().cloned()
})))
},
}
}
}
struct Recorder;
impl Derive for Recorder {
type Input = syn::DeriveInput;
type TopLevelMeta = Meta;
type Parsed<'i> = &'i syn::Ident;
type MappingPaths = (Option<syn::Ident>, syn::Path);
type Model<'p> = String;
type Ir<'p> = String;
fn input(derive_input: syn::DeriveInput) -> Self::Input {
derive_input
}
fn parse(input: &Self::Input) -> syn::Result<Self::Parsed<'_>> {
if input.ident == "BadParse" {
return Err(syn::Error::new_spanned(&input.ident, "bad parse"));
}
Ok(&input.ident)
}
fn map(
parsed: &Self::Parsed<'_>,
top_level_meta: Self::TopLevelMeta,
) -> syn::Result<Self::MappingPaths> {
if *parsed == "BadMapAndAnalyze" {
return Err(syn::Error::new_spanned(parsed, "bad map"));
}
Ok((top_level_meta.0, syn::parse_quote!(::runtime)))
}
fn analyze<'p>(parsed: &'p Self::Parsed<'_>) -> syn::Result<Self::Model<'p>> {
if *parsed == "BadMapAndAnalyze" {
return Err(syn::Error::new_spanned(parsed, "bad analyze"));
}
Ok(parsed.to_string())
}
fn lower<'p>(model: Self::Model<'p>, _parsed: &'p Self::Parsed<'_>) -> Self::Ir<'p> {
format!("{model}_lowered")
}
fn code_gen(
ir: Self::Ir<'_>,
mapping_paths: Self::MappingPaths,
) -> proc_macro2::TokenStream {
let ir = quote::format_ident!("{ir}");
let linked = mapping_paths.0;
quote::quote!(struct #ir; #linked)
}
fn runtime(mapping_paths: &Self::MappingPaths) -> &syn::Path {
&mapping_paths.1
}
}
fn expanded(item: proc_macro2::TokenStream) -> String {
expand::<Recorder>(item).to_string()
}
#[test]
fn every_stage_runs_in_order() {
assert_eq!(
expanded(quote::quote!(
#[linked]
struct Order;
)),
format!(
"struct Order_lowered ; linked const _ : () = :: runtime :: __private :: require_version ({}u64 , {}u64) ;",
env!("CARGO_PKG_VERSION_MAJOR"),
env!("CARGO_PKG_VERSION_MINOR"),
)
);
}
#[test]
fn meta_and_parse_errors_are_reported_together() {
let errors = expanded(quote::quote!(
#[bad_meta]
struct BadParse;
));
assert!(errors.contains("bad parse"));
assert!(errors.contains("bad meta"));
}
#[test]
fn map_and_analyze_errors_are_reported_together() {
let errors = expanded(quote::quote!(
struct BadMapAndAnalyze;
));
assert!(errors.contains("bad map"));
assert!(errors.contains("bad analyze"));
}
#[test]
fn tokens_that_are_not_an_item_are_rejected() {
assert!(expanded(quote::quote!(1 + 1)).contains("compile_error"));
}
}