use proc_macro::TokenStream;
use proc_macro2::TokenStream as TS2;
use quote::quote;
use syn::{
parse_macro_input, spanned::Spanned, Attribute, Error, ImplItem, Item,
ItemImpl, LitInt, LitStr,
};
#[derive(Default)]
struct ParityArgs {
path: Option<LitStr>,
status: Option<syn::Ident>,
since: Option<LitStr>,
comment: Option<LitStr>,
issue: Option<LitInt>,
}
fn parse_args(attr: &Attribute) -> Result<ParityArgs, Error> {
let mut args = ParityArgs::default();
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
args.path = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("status") {
args.status = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("since") {
args.since = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("comment") {
args.comment = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("issue") {
args.issue = Some(meta.value()?.parse()?);
} else {
return Err(meta.error(format!(
"parity: unknown argument `{}` (expected one of: path, status, since, comment, issue)",
meta.path.get_ident().map(|i| i.to_string()).unwrap_or_default(),
)));
}
Ok(())
})?;
Ok(args)
}
#[proc_macro_attribute]
pub fn parity_impl(args: TokenStream, input: TokenStream) -> TokenStream {
let mut item: ItemImpl = parse_macro_input!(input as ItemImpl);
let self_ty = &item.self_ty;
let self_ty_str = quote!(#self_ty).to_string().replace(' ', "");
let args2: TS2 = args.into();
let parent_attr: Attribute = syn::parse_quote!(#[parity(#args2)]);
let parent_args = match parse_args(&parent_attr) {
Ok(a) => a,
Err(e) => return e.into_compile_error().into(),
};
let parent_path_str = parent_args.path.as_ref().map(|r| r.value());
let mut submits = TS2::new();
if parent_args.path.is_some() && parent_args.status.is_some() {
let lit = LitStr::new(&self_ty_str, proc_macro2::Span::call_site());
let tokens = build_submit(&parent_args, parent_attr.span(), quote!(#lit), None)
.unwrap_or_else(Error::into_compile_error);
submits.extend(tokens);
}
for impl_item in &mut item.items {
if let ImplItem::Fn(method) = impl_item {
let mut child_attrs = Vec::new();
method.attrs.retain(|attr| {
if attr.path().is_ident("parity") {
child_attrs.push(attr.clone());
false } else {
true }
});
for attr in child_attrs {
let fn_name = method.sig.ident.to_string();
let impl_path = format!("{}::{}", self_ty_str, fn_name);
let lit = LitStr::new(&impl_path, proc_macro2::Span::call_site());
let tokens = match parse_args(&attr) {
Ok(child) => build_submit(
&child,
attr.span(),
quote!(#lit),
parent_path_str.as_deref(),
)
.unwrap_or_else(Error::into_compile_error),
Err(e) => e.into_compile_error(),
};
submits.extend(tokens);
}
}
}
let out = quote! {
#item
#submits
};
out.into()
}
#[proc_macro_attribute]
pub fn parity(args: TokenStream, input: TokenStream) -> TokenStream {
let item: Item = parse_macro_input!(input as Item);
let name = match &item {
Item::Fn(f) => f.sig.ident.to_string(),
Item::Struct(s) => s.ident.to_string(),
Item::Enum(e) => e.ident.to_string(),
Item::Type(t) => t.ident.to_string(),
other => {
return Error::new(
other.span(),
"parity: `#[parity]` expects a `fn`, `struct`, `enum`, or `type` alias",
)
.into_compile_error()
.into();
}
};
let impl_path_expr = quote! { concat!(module_path!(), "::", #name) };
let args2: TS2 = args.into();
let attr: Attribute = syn::parse_quote!(#[parity(#args2)]);
let submit = match parse_args(&attr) {
Ok(parsed) => build_submit(&parsed, attr.span(), impl_path_expr, None)
.unwrap_or_else(Error::into_compile_error),
Err(e) => e.into_compile_error(),
};
let out = quote! {
#item
#submit
};
out.into()
}
fn build_submit(
args: &ParityArgs,
span: proc_macro2::Span,
impl_path_expr: TS2,
parent_path: Option<&str>,
) -> Result<TS2, Error> {
let path_lit = args.path.as_ref().ok_or_else(|| {
Error::new(span, "parity: missing required `path = \"...\"`")
})?;
let path_value = path_lit.value();
let path_lit = if let Some(suffix) = path_value.strip_prefix('.') {
match parent_path {
Some(parent) => LitStr::new(&format!("{parent}.{suffix}"), path_lit.span()),
None => {
return Err(Error::new(
path_lit.span(),
"parity: relative path (leading `.`) requires the enclosing \
`#[parity_impl(...)]` to declare a `path`",
));
}
}
} else {
path_lit.clone()
};
let status = args.status.as_ref().ok_or_else(|| {
Error::new(
span,
"parity: missing required `status = Implemented | Partial | Unimplemented`",
)
})?;
if status != "Implemented" && status != "Partial" && status != "Unimplemented" {
return Err(Error::new(
status.span(),
format!(
"parity: `status` must be one of `Implemented`, `Partial`, or `Unimplemented` (got `{status}`)"
),
));
}
if status == "Unimplemented" && args.comment.is_none() {
return Err(Error::new(
span,
"parity: `status = Unimplemented` requires a `comment = \"...\"` explaining why",
));
}
let since_tok = match &args.since {
Some(s) => quote!(Some(#s)),
None => quote!(None),
};
let comment_tok = match &args.comment {
Some(s) => quote!(Some(#s)),
None => quote!(None),
};
let issue_tok = match &args.issue {
Some(i) => quote!(Some(#i)),
None => quote!(None),
};
Ok(quote! {
::api_parity_rs::inventory::submit! {
::api_parity_rs::ParityEntry {
path: #path_lit,
implementation: #impl_path_expr,
status: ::api_parity_rs::Status::#status,
since: #since_tok,
comment: #comment_tok,
issue: #issue_tok,
}
}
})
}