#![doc = include_str!("../TYPES.md")]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![cfg_attr(not(test), deny(clippy::allow_attributes_without_reason))]
use proc_macro::TokenStream;
use quote::quote;
use rsbinder_aidl::render::{render_interface, FnMembers, InterfaceRender, TransactionWrite};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
FnArg, ItemTrait, Pat, PathArguments, ReturnType, Token, TraitItem, TraitItemFn, Type,
};
mod aidl_shape;
mod binder_enum;
mod parcelable;
mod service_error;
#[cfg(test)]
mod type_matrix;
mod type_str;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Dir {
In,
Out,
Inout,
}
struct Args {
descriptor: Option<String>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut descriptor = None;
if input.is_empty() {
return Ok(Self { descriptor });
}
let metas = Punctuated::<syn::MetaNameValue, Token![,]>::parse_terminated(input)?;
for meta in metas {
if meta.path.is_ident("descriptor") {
if descriptor.is_some() {
return Err(syn::Error::new_spanned(
&meta.path,
"`descriptor` is given more than once",
));
}
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &meta.value
else {
return Err(syn::Error::new_spanned(
&meta.value,
"descriptor must be a string literal",
));
};
let value = s.value();
if let Some(bad) = value.chars().find(|c| "\\\"\n\r".contains(*c)) {
return Err(syn::Error::new_spanned(
s,
format!(
"a descriptor cannot contain {bad:?} — it is written verbatim \
into the generated source and onto the wire"
),
));
}
if value.is_empty() {
return Err(syn::Error::new_spanned(
s,
"a descriptor cannot be empty — it is the wire name a peer compares, \
and `.aidl` always has a name to put there",
));
}
descriptor = Some(value);
} else {
return Err(syn::Error::new_spanned(
&meta.path,
"unknown argument; only `descriptor = \"…\"` is supported",
));
}
}
Ok(Self { descriptor })
}
}
fn attr_descriptor(attrs: &[syn::Attribute], name: &str) -> syn::Result<Option<String>> {
let mut found = None;
for attr in attrs {
if !attr.path().is_ident(name) {
continue;
}
let args: Args = attr.parse_args()?;
if args.descriptor.is_some() {
if found.is_some() {
return Err(syn::Error::new_spanned(
attr,
format!("`descriptor` is given more than once; #[{name}(..)] appears twice"),
));
}
found = args.descriptor;
}
}
Ok(found)
}
#[proc_macro_derive(Parcelable, attributes(parcelable))]
pub fn derive_parcelable(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
match parcelable::expand(&input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_derive(BinderEnum)]
pub fn derive_binder_enum(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
match binder_enum::expand(&input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_derive(ServiceSpecificError)]
pub fn derive_service_specific_error(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::DeriveInput);
match service_error::expand(&input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_attribute]
pub fn interface(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(attr as Args);
let item_trait = syn::parse_macro_input!(item as ItemTrait);
match expand(&args, &item_trait) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn expand(args: &Args, item: &ItemTrait) -> syn::Result<proc_macro2::TokenStream> {
let rendered = render_source(args, item)?;
let file = syn::parse_file(&rendered).map_err(|e| {
syn::Error::new_spanned(
&item.ident,
format!("generated code did not parse ({e}); generated source follows:\n{rendered}"),
)
})?;
let mut items = file.items.into_iter();
let (Some(syn::Item::Mod(mut module)), None) = (items.next(), items.next()) else {
return Err(syn::Error::new_spanned(
&item.ident,
"generated code was not a single module (generator contract changed)",
));
};
let raw_name = item.ident.to_string();
let mod_ident = syn::Ident::new(
&format!("{}_binder", strip_raw(&raw_name)),
item.ident.span(),
);
module.ident = mod_ident.clone();
module.vis = item.vis.clone();
let copied = by_value_types(item)?;
if let Some((_, items)) = module.content.as_mut() {
items.insert(
0,
syn::parse_quote!(
use super::*;
),
);
for ty in &copied {
items.push(syn::parse_quote!(
const _: fn() = || {
fn __rsbinder_assert_copy<T: ::core::marker::Copy>() {}
__rsbinder_assert_copy::<#ty>();
};
));
}
}
let marker = syn::Ident::new(
&format!(
"__rsbinder_one_interface_per_stem_{}",
rsbinder_aidl::render::interface_stem(strip_raw(&raw_name))
),
item.ident.span(),
);
let vis = &item.vis;
Ok(quote! {
#[doc(hidden)]
#module
#vis use #mod_ident::*;
#[doc(hidden)]
#[allow(dead_code, non_upper_case_globals)]
const #marker: () = ();
})
}
pub(crate) fn strip_raw(ident: &str) -> &str {
ident.strip_prefix("r#").unwrap_or(ident)
}
fn render_source(args: &Args, item: &ItemTrait) -> syn::Result<String> {
render_source_with(args, item, cfg!(feature = "async"))
}
fn render_source_with(args: &Args, item: &ItemTrait, enabled_async: bool) -> syn::Result<String> {
check_attrs(&item.attrs, &["deprecated"])?;
if let Some(unsafety) = &item.unsafety {
return Err(syn::Error::new_spanned(
unsafety,
"an `unsafe` binder interface is not supported — the generated trait is safe, \
so the obligation would be silently dropped",
));
}
if let Some(auto_token) = &item.modifiers.auto_token {
return Err(syn::Error::new_spanned(
auto_token,
"an `auto` trait carries no methods and cannot be a binder interface",
));
}
item.modifiers.require_empty()?;
if let Some(where_clause) = &item.generics.where_clause {
return Err(syn::Error::new_spanned(
where_clause,
"a where clause has no meaning on a binder interface — the generated trait \
requires only `rsbinder::Interface + Send`",
));
}
if !item.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&item.generics,
"a binder interface cannot be generic — the wire has no way to carry the parameter",
));
}
if !item.supertraits.is_empty() {
return Err(syn::Error::new_spanned(
&item.supertraits,
"supertraits are not supported; the generated trait already requires \
`rsbinder::Interface + Send`",
));
}
type_str::reject_reserved_ident(&item.ident)?;
let raw = item.ident.to_string();
let name = strip_raw(&raw).to_string();
let mut fn_members = Vec::new();
let mut seen = std::collections::HashSet::new();
for (i, trait_item) in item.items.iter().enumerate() {
let TraitItem::Fn(f) = trait_item else {
return Err(syn::Error::new_spanned(
trait_item,
"only methods are supported; constants and associated types need `.aidl`",
));
};
let ident = f.sig.ident.to_string();
if !seen.insert(strip_raw(&ident).to_string()) {
return Err(syn::Error::new_spanned(
&f.sig.ident,
format!("duplicate method name `{}`", strip_raw(&ident)),
));
}
let bare = strip_raw(&ident);
if matches!(
bare,
"descriptor" | "getDefaultImpl" | "setDefaultImpl" | "dump" | "as_binder"
) {
return Err(syn::Error::new_spanned(
&f.sig.ident,
format!("method `{bare}` collides with an item of the generated Rust trait"),
));
}
let args: Vec<&Type> = f
.sig
.inputs
.iter()
.filter_map(|a| match a {
FnArg::Typed(t) => Some(&*t.ty),
FnArg::Receiver(_) => None,
})
.collect();
let reserved = match (bare, args.as_slice()) {
("asBinder" | "getInterfaceHash" | "getInterfaceVersion", []) => true,
("getTransactionName", [ty]) => matches!(
type_str::unwrap_group(ty),
Type::Path(p) if p.qself.is_none()
&& p.path.segments.last().is_some_and(|s| s.ident == "i32" && s.arguments.is_none())
),
_ => false,
};
if reserved {
return Err(syn::Error::new_spanned(
&f.sig.ident,
format!("method `{bare}` is reserved for internal use"),
));
}
fn_members.push(make_fn_member(f, i as u32)?);
}
let descriptor = args.descriptor.clone().unwrap_or_else(|| name.clone());
let mut render = InterfaceRender::new(name, descriptor);
render.fn_members = fn_members;
render.enabled_async = enabled_async;
render.deprecated = deprecated_of(&item.attrs)?;
render_interface(&render)
.map(|s| s.trim().to_string())
.map_err(|e| syn::Error::new_spanned(&item.ident, format!("codegen failed: {e}")))
}
fn make_fn_member(f: &TraitItemFn, index: u32) -> syn::Result<FnMembers> {
f.modifiers.require_empty()?;
if f.default.is_some() {
return Err(syn::Error::new_spanned(
&f.sig.ident,
"a default body has no meaning on a binder interface — the server `impl` provides it",
));
}
if !f.sig.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&f.sig.generics,
"generic methods are not supported",
));
}
if let Some(where_clause) = &f.sig.generics.where_clause {
return Err(syn::Error::new_spanned(
where_clause,
"a where clause on a binder method is not supported",
));
}
if let Some(tok) = &f.sig.asyncness {
return Err(syn::Error::new_spanned(
tok,
"declare the method as sync; the `async` feature emits the `IFooAsync` halves \
alongside it",
));
}
if !matches!(f.sig.safety, syn::Safety::Default) {
return Err(syn::Error::new_spanned(
&f.sig.safety,
"an `unsafe` binder method is not supported",
));
}
if let Some(tok) = &f.sig.constness {
return Err(syn::Error::new_spanned(
tok,
"a `const` binder method is not supported",
));
}
if let Some(abi) = &f.sig.abi {
return Err(syn::Error::new_spanned(
abi,
"an explicit ABI has no meaning on a binder method",
));
}
if let Some(variadic) = &f.sig.variadic {
return Err(syn::Error::new_spanned(
variadic,
"a variadic binder method is not supported — the wire carries a fixed \
argument list",
));
}
let oneway = has_attr(&f.attrs, "oneway");
check_attrs(&f.attrs, &["oneway", "deprecated"])?;
const SHARED_RECEIVER: &str = "the first parameter must be `&self` — a binder object is \
shared, never owned or uniquely borrowed by a call";
let mut inputs = f.sig.inputs.iter();
let Some(FnArg::Receiver(receiver)) = inputs.next() else {
return Err(syn::Error::new_spanned(&f.sig, SHARED_RECEIVER));
};
let syn::ReceiverKind::Reference(_, lifetime, None) = &receiver.kind else {
return Err(syn::Error::new_spanned(&f.sig, SHARED_RECEIVER));
};
if let Some(lifetime) = lifetime.as_ref().filter(|lt| lt.ident != "_") {
return Err(syn::Error::new_spanned(
lifetime,
"an explicit lifetime on `self` is not supported — the generated trait takes a \
plain `&self`",
));
}
check_attrs(&receiver.attrs, &[])?;
let mut args = "&self".to_string();
let mut args_async = "&'a self".to_string();
let mut func_call_params = String::new();
let mut write_funcs = Vec::new();
let mut transaction_decls = Vec::new();
let mut transaction_write = Vec::new();
let mut transaction_params = String::new();
let mut read_onto_params = Vec::new();
let mut arg_names = std::collections::HashSet::new();
for input in inputs {
let FnArg::Typed(pat_ty) = input else {
return Err(syn::Error::new_spanned(input, "unexpected receiver"));
};
let Pat::Ident(pat_ident) = &*pat_ty.pat else {
return Err(syn::Error::new_spanned(
&pat_ty.pat,
"parameter patterns are not supported; use a plain name",
));
};
check_attrs(&pat_ty.attrs, &["inout"])?;
let raw_arg = pat_ident.ident.to_string();
let ident = format!("_arg_{}", strip_raw(&raw_arg));
if !arg_names.insert(ident.clone()) {
return Err(syn::Error::new_spanned(
&pat_ident.ident,
format!("duplicate argument name `{}`", strip_raw(&raw_arg)),
));
}
let dir = direction(&pat_ty.attrs, &pat_ty.ty)?;
if oneway && dir != Dir::In {
return Err(syn::Error::new_spanned(
&pat_ty.ty,
"a #[oneway] method cannot have an out/inout parameter — there is no reply to \
write it back into",
));
}
type_str::check_type_at(
&pat_ty.ty,
match dir {
Dir::In => type_str::Place::In,
Dir::Out => type_str::Place::Out,
Dir::Inout => type_str::Place::Inout,
},
)?;
type_str::reject_reserved_names(&pat_ty.ty)?;
let as_written = type_str::as_written(&pat_ty.ty)?;
let owned = type_str::owned(&pat_ty.ty)?;
let arg_str = format!(", {ident}: {as_written}");
args += &arg_str;
args_async += &arg_str.replace('&', "&'a ");
func_call_params += &format!("{ident}, ");
let is_option_vec = type_str::option_vec_elem(&pat_ty.ty).is_some();
let is_var_array = type_str::is_variable_array(&pat_ty.ty);
if dir != Dir::Out {
let param = if as_written.starts_with('&') {
ident.clone()
} else {
format!("&{ident}")
};
write_funcs.push(format!("data.write({param})?;"));
} else if is_option_vec {
write_funcs.push(format!("data.write_slice_size({ident}.as_deref())?;"));
} else if is_var_array {
write_funcs.push(format!(
"data.write_slice_size(::core::option::Option::Some({ident}))?;"
));
}
let (mutable, init) = match dir {
Dir::Out => (
"mut ",
type_str::out_default(&pat_ty.ty)?
.unwrap_or_else(|| "::core::default::Default::default()".into()),
),
Dir::Inout => ("mut ", "_reader.read()?".to_string()),
Dir::In => ("", "_reader.read()?".to_string()),
};
transaction_decls.push(format!("let {mutable}{ident}: {owned} = {init};"));
if dir == Dir::Out {
if is_option_vec {
transaction_decls.push(format!("_reader.resize_nullable_out_vec(&mut {ident})?;"));
} else if is_var_array {
transaction_decls.push(format!("_reader.resize_out_vec(&mut {ident})?;"));
}
}
if dir != Dir::In {
let mut write = TransactionWrite::new(ident.clone());
if dir == Dir::Out {
if let Some(flatten) = type_str::out_pfd_null_guard(&pat_ty.ty) {
write.needs_null_guard = true;
write.null_guard_flatten = flatten;
}
}
transaction_write.push(write);
read_onto_params.push(ident.clone());
}
transaction_params += &format!(
"{}, ",
func_call_param(&ident, &pat_ty.ty, &as_written, &owned, dir)
);
}
let return_type = return_type(f)?;
if oneway && return_type != "()" {
return Err(syn::Error::new_spanned(
&f.sig.output,
"a #[oneway] method cannot return a value — there is no reply to read it from",
));
}
let raw_ident = f.sig.ident.to_string();
let mut member = FnMembers::new(strip_raw(&raw_ident), index);
member.args = args;
member.args_async = args_async;
member.transaction_has_return = return_type != "()";
member.return_type = return_type;
member.write_funcs = write_funcs;
member.func_call_params = trim_comma(func_call_params);
member.transaction_decls = transaction_decls;
member.transaction_write = transaction_write;
member.transaction_params = trim_comma(transaction_params);
member.oneway = oneway;
member.read_onto_params = read_onto_params;
member.deprecated = deprecated_of(&f.attrs)?;
Ok(member)
}
fn func_call_param(ident: &str, ty: &Type, as_written: &str, owned: &str, dir: Dir) -> String {
if dir != Dir::In {
return format!("&mut {ident}");
}
if as_written == owned {
return ident.to_string();
}
if as_written == "&str" {
return format!("{ident}.as_str()");
}
if as_written.starts_with('&') {
return format!("&{ident}");
}
let borrowed = type_str::option_inner(ty).map(type_str::unwrap_group);
match borrowed {
Some(Type::Reference(r)) => match type_str::unwrap_group(&r.elem) {
Type::Path(p) if p.path.is_ident("str") => format!("{ident}.as_deref()"),
Type::Slice(_) => format!("{ident}.as_deref()"),
_ => format!("{ident}.as_ref()"),
},
_ => format!("&{ident}"),
}
}
fn direction(attrs: &[syn::Attribute], ty: &Type) -> syn::Result<Dir> {
let is_mut_ref =
matches!(type_str::unwrap_group(ty), Type::Reference(r) if r.mutability.is_some());
if has_attr(attrs, "inout") {
if !is_mut_ref {
return Err(syn::Error::new_spanned(
ty,
"#[inout] needs `&mut T` — the callee has to be able to write it back",
));
}
return Ok(Dir::Inout);
}
Ok(if is_mut_ref { Dir::Out } else { Dir::In })
}
fn return_type(f: &TraitItemFn) -> syn::Result<String> {
let ReturnType::Type(_, ty) = &f.sig.output else {
return Err(syn::Error::new_spanned(
&f.sig,
"a binder method must return `BinderResult<T>` — every call can fail on the wire",
));
};
let Type::Path(p) = type_str::unwrap_group(ty) else {
return Err(syn::Error::new_spanned(ty, "expected `BinderResult<T>`"));
};
let last = p.path.segments.last().expect("non-empty path");
if last.ident != "BinderResult" {
return Err(syn::Error::new_spanned(
ty,
"expected `BinderResult<T>` (aliased as `rsbinder::BinderResult`)",
));
}
let PathArguments::AngleBracketed(args) = &last.arguments else {
return Err(syn::Error::new_spanned(
ty,
"`BinderResult` needs its success type: `BinderResult<()>` for no return",
));
};
if args.args.len() != 1 {
return Err(syn::Error::new_spanned(
ty,
"`BinderResult` takes exactly one type argument",
));
}
let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
return Err(syn::Error::new_spanned(ty, "expected `BinderResult<T>`"));
};
type_str::check_type_at(inner, type_str::Place::Return)?;
type_str::reject_reserved_names(inner)?;
type_str::owned(inner)
}
fn by_value_types(item: &ItemTrait) -> syn::Result<Vec<Type>> {
let mut out = Vec::new();
for trait_item in &item.items {
let TraitItem::Fn(f) = trait_item else {
continue;
};
for input in &f.sig.inputs {
let FnArg::Typed(pat_ty) = input else {
continue;
};
if !type_str::as_written(&pat_ty.ty)?.starts_with('&') {
out.push((*pat_ty.ty).clone());
}
}
}
Ok(out)
}
pub(crate) fn deprecated_of(attrs: &[syn::Attribute]) -> syn::Result<String> {
let mut found = attrs.iter().filter(|a| a.path().is_ident("deprecated"));
let Some(attr) = found.next() else {
return Ok(String::new());
};
if let Some(second) = found.next() {
return Err(syn::Error::new_spanned(
second,
"#[deprecated] is given more than once",
));
}
match &attr.meta {
syn::Meta::Path(_) => Ok(rsbinder_aidl::render::deprecated_attr(Some(&String::new()))),
syn::Meta::NameValue(nv) => {
let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
else {
return Err(syn::Error::new_spanned(
&nv.value,
"a deprecation note must be a string literal",
));
};
let note = s.value();
Ok(rsbinder_aidl::render::deprecated_attr(Some(¬e)))
}
syn::Meta::List(list) => Err(syn::Error::new_spanned(
list,
"`.aidl` carries only `#[deprecated]` and `#[deprecated = \"…\"]`; the other fields \
of the Rust attribute have no AIDL form and would be dropped",
)),
}
}
fn has_attr(attrs: &[syn::Attribute], name: &str) -> bool {
attrs.iter().any(|a| a.path().is_ident(name))
}
fn check_attrs(attrs: &[syn::Attribute], allowed: &[&str]) -> syn::Result<()> {
for attr in attrs {
if attr.path().is_ident("doc") && matches!(attr.meta, syn::Meta::NameValue(_)) {
continue;
}
if let Some(name) = allowed.iter().find(|a| attr.path().is_ident(a)) {
if *name != "deprecated" && !matches!(attr.meta, syn::Meta::Path(_)) {
return Err(syn::Error::new_spanned(
attr,
format!("#[{name}] takes no arguments"),
));
}
continue;
}
let expected = if allowed.is_empty() {
"no attribute is supported here".to_string()
} else {
format!(
"#[rsbinder::interface] understands only {} here",
allowed
.iter()
.map(|a| format!("#[{a}]"))
.collect::<Vec<_>>()
.join(", ")
)
};
return Err(syn::Error::new_spanned(
attr,
format!("unsupported attribute; {expected}"),
));
}
Ok(())
}
fn trim_comma(mut s: String) -> String {
if s.ends_with(", ") {
s.truncate(s.len() - 2);
}
s
}
#[cfg(test)]
mod golden {
use super::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
struct TempDir(PathBuf);
impl TempDir {
fn new(tag: &str) -> Self {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"rsbinder_macros_golden_{tag}_{}_{n}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temp dir");
Self(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn from_aidl(aidl: &str, module: &str, enabled_async: bool) -> String {
let dir = TempDir::new(module);
let src = dir.path().join(format!("{module}.aidl"));
std::fs::write(&src, aidl).expect("write aidl");
rsbinder_aidl::Builder::new()
.source(src)
.include_dir(dir.path())
.dest_dir(dir.path())
.output(PathBuf::from("golden.rs"))
.set_async_support(enabled_async)
.generate()
.expect("aidl generate");
let text = std::fs::read_to_string(dir.path().join("golden.rs")).expect("read generated");
extract_module(&text, module)
}
fn extract_module(text: &str, module: &str) -> String {
let needle = format!("pub mod {module} {{");
let start_line = text
.lines()
.position(|l| l.trim_start().starts_with(&needle))
.unwrap_or_else(|| panic!("no `pub mod {module}` in:\n{text}"));
let lines: Vec<&str> = text.lines().collect();
let indent = lines[start_line].len() - lines[start_line].trim_start().len();
let mut depth = 0usize;
let mut end_line = start_line;
for (i, line) in lines.iter().enumerate().skip(start_line) {
depth += line.matches('{').count();
depth -= line.matches('}').count();
if depth == 0 {
end_line = i;
break;
}
}
lines[start_line..=end_line]
.iter()
.map(|l| {
if l.len() >= indent {
&l[indent..]
} else {
l.trim_start()
}
})
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string()
}
fn from_macro(tokens: proc_macro2::TokenStream, enabled_async: bool) -> String {
let item: ItemTrait = syn::parse2(tokens).expect("parse trait");
render_source_with(&Args { descriptor: None }, &item, enabled_async).expect("render")
}
#[track_caller]
fn assert_same(aidl: &str, module: &str, tokens: proc_macro2::TokenStream) {
for enabled_async in [false, true] {
assert_same_for(aidl, module, tokens.clone(), enabled_async);
}
}
#[track_caller]
fn assert_same_for(
aidl: &str,
module: &str,
tokens: proc_macro2::TokenStream,
enabled_async: bool,
) {
let expected = from_aidl(aidl, module, enabled_async);
let actual = from_macro(tokens, enabled_async);
if expected != actual {
let mut report = String::new();
for diff in expected
.lines()
.zip(actual.lines())
.enumerate()
.filter(|(_, (a, b))| a != b)
{
let (i, (a, b)) = diff;
report.push_str(&format!("line {i}:\n aidl : {a}\n macro: {b}\n"));
}
if expected.lines().count() != actual.lines().count() {
report.push_str(&format!(
"line count: aidl {} vs macro {}\n",
expected.lines().count(),
actual.lines().count()
));
}
panic!("generated code differs (enabled_async = {enabled_async})\n{report}\n--- aidl ---\n{expected}\n--- macro ---\n{actual}");
}
}
#[track_caller]
fn assert_same_files(
files: &[(&str, &str)],
main: &str,
module: &str,
descriptor: &str,
tokens: proc_macro2::TokenStream,
) {
for enabled_async in [false, true] {
let expected = from_aidl_files(files, main, module, enabled_async);
let item: ItemTrait = syn::parse2(tokens.clone()).expect("parse trait");
let actual = render_source_with(
&Args {
descriptor: Some(descriptor.to_string()),
},
&item,
enabled_async,
)
.expect("render");
assert_eq!(
expected, actual,
"generated code differs (enabled_async = {enabled_async})"
);
}
}
pub(crate) fn from_aidl_files(
files: &[(&str, &str)],
main: &str,
module: &str,
enabled_async: bool,
) -> String {
let dir = TempDir::new(&format!("multi_{module}"));
for (rel, body) in files {
let path = dir.path().join(rel);
std::fs::create_dir_all(path.parent().expect("relative path")).expect("mkdir");
std::fs::write(&path, body).expect("write aidl");
}
rsbinder_aidl::Builder::new()
.source(dir.path().join(main))
.include_dir(dir.path())
.dest_dir(dir.path())
.output(PathBuf::from("golden_multi.rs"))
.set_async_support(enabled_async)
.generate()
.expect("aidl generate");
let text =
std::fs::read_to_string(dir.path().join("golden_multi.rs")).expect("read generated");
extract_module(&text, module)
}
#[test]
fn scalars_and_strings() {
assert_same(
r#"
interface IGolden1 {
String echo(in String msg);
int add(in int a, in int b);
boolean flag(in boolean b);
}
"#,
"IGolden1",
quote! {
pub trait IGolden1 {
fn echo(&self, msg: &str) -> BinderResult<String>;
fn add(&self, a: i32, b: i32) -> BinderResult<i32>;
fn flag(&self, b: bool) -> BinderResult<bool>;
}
},
);
}
#[test]
fn oneway_and_void() {
assert_same(
r#"
interface IGolden2 {
oneway void ping();
void nudge(in long n);
}
"#,
"IGolden2",
quote! {
pub trait IGolden2 {
#[oneway]
fn ping(&self) -> BinderResult<()>;
fn nudge(&self, n: i64) -> BinderResult<()>;
}
},
);
}
#[test]
fn out_and_inout_vectors() {
assert_same(
r#"
interface IGolden3 {
void fill(out int[] values);
void bump(inout int[] values);
void take(in int[] values);
}
"#,
"IGolden3",
quote! {
pub trait IGolden3 {
fn fill(&self, values: &mut Vec<i32>) -> BinderResult<()>;
fn bump(&self, #[inout] values: &mut Vec<i32>) -> BinderResult<()>;
fn take(&self, values: &[i32]) -> BinderResult<()>;
}
},
);
}
#[test]
fn an_out_vec_is_the_array_form_not_the_list_form() {
assert_same(
r#"
interface IGolden12 {
void fill(out String[] values);
void maybe(out @nullable String[] values);
}
"#,
"IGolden12",
quote! {
pub trait IGolden12 {
fn fill(&self, values: &mut Vec<String>) -> BinderResult<()>;
fn maybe(&self, values: &mut Option<Vec<Option<String>>>) -> BinderResult<()>;
}
},
);
}
#[test]
fn nullable_arguments_and_returns() {
assert_same(
r#"
interface IGolden4 {
@nullable String maybe(in @nullable String msg, in @nullable byte[] blob);
@nullable String[] some();
String[] every();
}
"#,
"IGolden4",
quote! {
pub trait IGolden4 {
fn maybe(
&self,
msg: Option<&str>,
blob: Option<&[u8]>,
) -> BinderResult<Option<String>>;
fn some(&self) -> BinderResult<Option<Vec<Option<String>>>>;
fn every(&self) -> BinderResult<Vec<String>>;
}
},
);
}
#[test]
fn binder_objects() {
assert_same_files(
&[
(
"com/example/IGolden5.aidl",
"package com.example;\nimport com.example.IGolden5Cb;\n\
interface IGolden5 {\n void register(in IGolden5Cb cb);\n\
IGolden5Cb fetch();\n}\n",
),
(
"com/example/IGolden5Cb.aidl",
"package com.example;\ninterface IGolden5Cb {\n void hit();\n}\n",
),
],
"com/example/IGolden5.aidl",
"IGolden5",
"com.example.IGolden5",
quote! {
pub trait IGolden5 {
fn register(
&self,
cb: &rsbinder::Strong<dyn super::IGolden5Cb::IGolden5Cb>,
) -> BinderResult<()>;
fn fetch(
&self,
) -> BinderResult<rsbinder::Strong<dyn super::IGolden5Cb::IGolden5Cb>>;
}
},
);
}
#[test]
fn out_fd_arrays_keep_the_null_guard_at_every_arity() {
assert_same(
r#"
interface IGolden8 {
void variable(out ParcelFileDescriptor[] fds);
void fixed(out ParcelFileDescriptor[3] fds);
void nested(out ParcelFileDescriptor[2][3] fds);
}
"#,
"IGolden8",
quote! {
pub trait IGolden8 {
fn variable(
&self,
fds: &mut Vec<Option<rsbinder::ParcelFileDescriptor>>,
) -> BinderResult<()>;
fn fixed(
&self,
fds: &mut [Option<rsbinder::ParcelFileDescriptor>; 3],
) -> BinderResult<()>;
fn nested(
&self,
fds: &mut [[Option<rsbinder::ParcelFileDescriptor>; 3]; 2],
) -> BinderResult<()>;
}
},
);
}
#[test]
fn every_out_and_inout_shape_matches_aidl() {
assert_same_files(
&[
(
"p/IGolden10.aidl",
"package p;\nimport p.IGolden10Cb;\nimport p.Golden10Cfg;\n\
interface IGolden10 {\n\
\x20 void d(out Golden10Cfg v);\n\
\x20 void e(out int[] v);\n\
\x20 void f(out String[] v);\n\
\x20 void g(out ParcelFileDescriptor[] v);\n\
\x20 void h(out IGolden10Cb[] v);\n\
\x20 void i(out Golden10Cfg[] v);\n\
\x20 void j(out int[3] v);\n\
\x20 void k(out ParcelFileDescriptor[3] v);\n\
\x20 void l(out ParcelFileDescriptor[2][3] v);\n\
\x20 void m(out Golden10Cfg[3] v);\n\
\x20 void n(out @nullable Golden10Cfg v);\n\
\x20 void o(out @nullable int[] v);\n\
\x20 void p(out @nullable String[] v);\n\
\x20 void q(out @nullable ParcelFileDescriptor[] v);\n\
\x20 void r(out @nullable int[3] v);\n\
\x20 void s(out @nullable Golden10Cfg[3] v);\n\
\x20 void t(inout ParcelFileDescriptor v);\n\
\x20 void u(inout @nullable ParcelFileDescriptor v);\n\
\x20 void w(inout ParcelFileDescriptor[] v);\n\
\x20 void x(inout IGolden10Cb[] v);\n\
\x20 void y(inout @nullable Golden10Cfg v);\n\
\x20 void z(inout @nullable ParcelFileDescriptor[] v);\n\
\x20 void aa(inout ParcelFileDescriptor[3] v);\n\
\x20 void ab(inout Golden10Cfg[3] v);\n\
\x20 void ac(inout IGolden10Cb[3] v);\n\
\x20 void ad(inout @nullable ParcelFileDescriptor[3] v);\n}\n",
),
(
"p/IGolden10Cb.aidl",
"package p;\ninterface IGolden10Cb {\n void hit();\n}\n",
),
(
"p/Golden10Cfg.aidl",
"package p;\nparcelable Golden10Cfg {\n int a;\n}\n",
),
],
"p/IGolden10.aidl",
"IGolden10",
"p.IGolden10",
quote! {
pub trait IGolden10 {
fn d(&self, v: &mut super::Golden10Cfg::Golden10Cfg) -> BinderResult<()>;
fn e(&self, v: &mut Vec<i32>) -> BinderResult<()>;
fn f(&self, v: &mut Vec<String>) -> BinderResult<()>;
fn g(
&self,
v: &mut Vec<Option<rsbinder::ParcelFileDescriptor>>,
) -> BinderResult<()>;
fn h(
&self,
v: &mut Vec<Option<rsbinder::Strong<dyn super::IGolden10Cb::IGolden10Cb>>>,
) -> BinderResult<()>;
fn i(&self, v: &mut Vec<super::Golden10Cfg::Golden10Cfg>) -> BinderResult<()>;
fn j(&self, v: &mut [i32; 3]) -> BinderResult<()>;
fn k(
&self,
v: &mut [Option<rsbinder::ParcelFileDescriptor>; 3],
) -> BinderResult<()>;
fn l(
&self,
v: &mut [[Option<rsbinder::ParcelFileDescriptor>; 3]; 2],
) -> BinderResult<()>;
fn m(&self, v: &mut [super::Golden10Cfg::Golden10Cfg; 3]) -> BinderResult<()>;
fn n(
&self,
v: &mut Option<super::Golden10Cfg::Golden10Cfg>,
) -> BinderResult<()>;
fn o(&self, v: &mut Option<Vec<i32>>) -> BinderResult<()>;
fn p(&self, v: &mut Option<Vec<Option<String>>>) -> BinderResult<()>;
fn q(
&self,
v: &mut Option<Vec<Option<rsbinder::ParcelFileDescriptor>>>,
) -> BinderResult<()>;
fn r(&self, v: &mut Option<[i32; 3]>) -> BinderResult<()>;
fn s(
&self,
v: &mut Option<[Option<super::Golden10Cfg::Golden10Cfg>; 3]>,
) -> BinderResult<()>;
fn t(
&self,
#[inout] v: &mut rsbinder::ParcelFileDescriptor,
) -> BinderResult<()>;
fn u(
&self,
#[inout] v: &mut Option<rsbinder::ParcelFileDescriptor>,
) -> BinderResult<()>;
fn w(
&self,
#[inout] v: &mut Vec<rsbinder::ParcelFileDescriptor>,
) -> BinderResult<()>;
fn x(
&self,
#[inout] v: &mut Vec<rsbinder::Strong<dyn super::IGolden10Cb::IGolden10Cb>>,
) -> BinderResult<()>;
fn y(
&self,
#[inout] v: &mut Option<super::Golden10Cfg::Golden10Cfg>,
) -> BinderResult<()>;
fn z(
&self,
#[inout] v: &mut Option<Vec<Option<rsbinder::ParcelFileDescriptor>>>,
) -> BinderResult<()>;
fn aa(
&self,
#[inout] v: &mut [rsbinder::ParcelFileDescriptor; 3],
) -> BinderResult<()>;
fn ab(
&self,
#[inout] v: &mut [super::Golden10Cfg::Golden10Cfg; 3],
) -> BinderResult<()>;
fn ac(
&self,
#[inout] v: &mut [rsbinder::Strong<dyn super::IGolden10Cb::IGolden10Cb>; 3],
) -> BinderResult<()>;
fn ad(
&self,
#[inout] v: &mut Option<[Option<rsbinder::ParcelFileDescriptor>; 3]>,
) -> BinderResult<()>;
}
},
);
}
#[test]
fn deprecation_matches_aidl() {
assert_same(
r#"
/**
* @deprecated use IGolden12Next
*/
interface IGolden12 {
/**
* @deprecated use b
*/
void a();
void b();
}
"#,
"IGolden12",
quote! {
#[deprecated = "use IGolden12Next"]
pub trait IGolden12 {
#[deprecated = "use b"]
fn a(&self) -> BinderResult<()>;
fn b(&self) -> BinderResult<()>;
}
},
);
}
#[test]
fn rejects_a_deprecation_the_wire_cannot_carry() {
let err = reject(quote! {
#[deprecated(since = "1.0", note = "use b")]
pub trait IBad {
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("no AIDL form"), "{err}");
}
#[test]
fn rejects_a_repeated_deprecation() {
let err = reject(quote! {
pub trait IBad {
#[deprecated = "a"]
#[deprecated = "b"]
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("more than once"), "{err}");
}
#[test]
fn rejects_a_doc_attribute_the_re_render_would_drop() {
for attr in [quote!(#[doc(hidden)]), quote!(#[doc(alias = "x")])] {
let err = reject(quote! {
#attr
pub trait IBad {
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("unsupported attribute"), "{attr}: {err}");
}
render(quote! {
pub trait IOk {
fn go(&self) -> BinderResult<()>;
}
});
}
#[test]
fn a_nullable_by_value_path_is_pointed_at_the_borrowed_form() {
let err = reject(quote! {
pub trait IBad {
fn go(&self, c: Option<Cfg>) -> BinderResult<()>;
}
});
assert!(err.contains("`Option<&Cfg>`"), "{err}");
assert!(!err.contains("`Cfg` or"), "{err}");
}
#[test]
fn every_in_array_shape_matches_aidl() {
assert_same_files(
&[
(
"p/IGolden14.aidl",
"package p;\nimport p.IGolden14Cb;\nimport p.Golden14Cfg;\n\
interface IGolden14 {\n\
\x20 void a(in int[] v);\n\
\x20 void b(in @nullable int[] v);\n\
\x20 void c(in String[] v);\n\
\x20 void d(in @nullable String[] v);\n\
\x20 void e(in Golden14Cfg[] v);\n\
\x20 void f(in @nullable Golden14Cfg[] v);\n\
\x20 void g(in ParcelFileDescriptor[] v);\n\
\x20 void h(in @nullable ParcelFileDescriptor[] v);\n\
\x20 void i(in IGolden14Cb[] v);\n\
\x20 void j(in @nullable IGolden14Cb[] v);\n\
\x20 void k(in IBinder[] v);\n\
\x20 void l(in @nullable IBinder[] v);\n\
\x20 void m(in Golden14Cfg[3] v);\n\
\x20 void n(in @nullable Golden14Cfg[3] v);\n\
\x20 void o(in ParcelFileDescriptor[3] v);\n\
\x20 void p(in @nullable ParcelFileDescriptor[3] v);\n}\n",
),
(
"p/IGolden14Cb.aidl",
"package p;\ninterface IGolden14Cb {\n void hit();\n}\n",
),
(
"p/Golden14Cfg.aidl",
"package p;\nparcelable Golden14Cfg {\n int a;\n}\n",
),
],
"p/IGolden14.aidl",
"IGolden14",
"p.IGolden14",
quote! {
pub trait IGolden14 {
fn a(&self, v: &[i32]) -> BinderResult<()>;
fn b(&self, v: Option<&[i32]>) -> BinderResult<()>;
fn c(&self, v: &[String]) -> BinderResult<()>;
fn d(&self, v: Option<&[Option<String>]>) -> BinderResult<()>;
fn e(&self, v: &[super::Golden14Cfg::Golden14Cfg]) -> BinderResult<()>;
fn f(
&self,
v: Option<&[Option<super::Golden14Cfg::Golden14Cfg>]>,
) -> BinderResult<()>;
fn g(&self, v: &[rsbinder::ParcelFileDescriptor]) -> BinderResult<()>;
fn h(
&self,
v: Option<&[Option<rsbinder::ParcelFileDescriptor>]>,
) -> BinderResult<()>;
fn i(
&self,
v: &[rsbinder::Strong<dyn super::IGolden14Cb::IGolden14Cb>],
) -> BinderResult<()>;
fn j(
&self,
v: Option<&[Option<rsbinder::Strong<dyn super::IGolden14Cb::IGolden14Cb>>]>,
) -> BinderResult<()>;
fn k(&self, v: &[rsbinder::SIBinder]) -> BinderResult<()>;
fn l(&self, v: Option<&[Option<rsbinder::SIBinder>]>) -> BinderResult<()>;
fn m(&self, v: &[super::Golden14Cfg::Golden14Cfg; 3]) -> BinderResult<()>;
fn n(
&self,
v: Option<&[Option<super::Golden14Cfg::Golden14Cfg>; 3]>,
) -> BinderResult<()>;
fn o(&self, v: &[rsbinder::ParcelFileDescriptor; 3]) -> BinderResult<()>;
fn p(
&self,
v: Option<&[Option<rsbinder::ParcelFileDescriptor>; 3]>,
) -> BinderResult<()>;
}
},
);
}
#[test]
fn rejects_an_array_of_options_aidl_would_spell_bare() {
for decl in [
quote!(
fn go(&self, v: &[Option<rsbinder::ParcelFileDescriptor>]) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut Vec<Option<String>>) -> BinderResult<()>;
),
quote!(
fn go(
&self,
#[inout] v: &mut Vec<Option<rsbinder::ParcelFileDescriptor>>,
) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("`Option<_>` elements"), "{err}");
}
}
#[test]
fn rejects_array_elements_aidl_spells_the_other_way() {
for (decl, needle) in [
(
quote!(
fn go(
&self,
#[inout] v: &mut [Option<rsbinder::ParcelFileDescriptor>; 3],
) -> BinderResult<()>;
),
"an `inout` array cannot have `Option<_>` elements",
),
(
quote!(
fn go(
&self,
#[inout] v: &mut [Option<rsbinder::Strong<dyn IOther>>; 3],
) -> BinderResult<()>;
),
"an `inout` array cannot have `Option<_>` elements",
),
(
quote!(
fn go(&self, v: Option<&[String]>) -> BinderResult<()>;
),
"a nullable `in` array",
),
(
quote!(
fn go(&self, v: Option<&[rsbinder::ParcelFileDescriptor]>) -> BinderResult<()>;
),
"a nullable `in` array",
),
(
quote!(
fn go(&self, v: &mut Option<Vec<String>>) -> BinderResult<()>;
),
"a nullable `out` array",
),
(
quote!(
fn go(
&self,
v: &mut Option<[rsbinder::ParcelFileDescriptor; 3]>,
) -> BinderResult<()>;
),
"a nullable `out` array",
),
(
quote!(
fn go(
&self,
v: Option<&[rsbinder::ParcelFileDescriptor; 3]>,
) -> BinderResult<()>;
),
"a nullable `in` array",
),
(
quote!(
fn go(&self) -> BinderResult<Option<Vec<String>>>;
),
"a nullable returned array",
),
(
quote!(
fn go(&self) -> BinderResult<Option<Vec<rsbinder::Strong<dyn IOther>>>>;
),
"a nullable returned array",
),
(
quote!(
fn go(&self) -> BinderResult<Vec<Option<String>>>;
),
"a returned array cannot have",
),
(
quote!(
fn go(&self) -> BinderResult<Option<[String; 3]>>;
),
"a nullable returned array",
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains(needle), "{needle}: {err}");
}
render(quote! {
pub trait IOk {
fn a(
&self,
#[inout] v: &mut [rsbinder::ParcelFileDescriptor; 3],
) -> BinderResult<()>;
fn a2(&self, #[inout] v: &mut [rsbinder::Strong<dyn IOther>; 3]) -> BinderResult<()>;
fn b(&self, v: Option<&[Option<String>]>) -> BinderResult<()>;
fn c(
&self,
v: Option<&[Option<rsbinder::ParcelFileDescriptor>; 3]>,
) -> BinderResult<()>;
fn d(&self) -> BinderResult<Option<Vec<Option<String>>>>;
fn e(&self) -> BinderResult<Vec<String>>;
fn f(&self) -> BinderResult<Option<[Option<String>; 3]>>;
}
});
}
#[test]
fn rejects_arrays_nested_the_way_aidl_cannot_render() {
for (decl, needle) in [
(
quote!(
fn go(&self) -> BinderResult<Vec<[i32; 3]>>;
),
"fixed-size in every dimension",
),
(
quote!(
fn go(&self, v: &[[i32; 3]]) -> BinderResult<()>;
),
"fixed-size in every dimension",
),
(
quote!(
fn go(
&self,
v: &mut Vec<[rsbinder::ParcelFileDescriptor; 3]>,
) -> BinderResult<()>;
),
"fixed-size in every dimension",
),
(
quote!(
fn go(&self) -> BinderResult<Vec<Vec<i32>>>;
),
"fixed-size in every dimension",
),
(
quote!(
fn go(&self) -> BinderResult<Option<Vec<Option<[String; 3]>>>>;
),
"`Option` of another array",
),
(
quote!(
fn go(&self) -> BinderResult<[Option<[String; 3]>; 2]>;
),
"`Option` of another array",
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains(needle), "{needle}: {err}");
}
}
#[test]
fn rejects_a_borrowed_owned_container() {
for (ty, needle) in [
(quote!(&String), "&str"),
(quote!(&Vec<i32>), "&[T]"),
(quote!(Option<&String>), "&str"),
(quote!(Option<&Vec<i32>>), "&[T]"),
(quote!(&Option<String>), "Option<&T>"),
(quote!(Option<&Option<String>>), "Option<&T>"),
(quote!(&i32), "drop the `&`"),
(quote!(&bool), "drop the `&`"),
] {
let err = reject(quote! {
pub trait IBad {
fn go(&self, v: #ty) -> BinderResult<()>;
}
});
assert!(err.contains(needle), "{ty}: {err}");
}
render(quote! {
pub trait IOk {
fn go(&self, v: &mut Vec<i32>) -> BinderResult<()>;
}
});
}
#[test]
fn rejects_return_types_with_no_wire_form() {
for (ty, needle) in [
(quote!(dyn std::fmt::Debug), "trait object"),
(quote!(<Vec<i32> as IntoIterator>::Item), "qualified path"),
] {
let err = reject(quote! {
pub trait IBad {
fn go(&self) -> BinderResult<#ty>;
}
});
assert!(err.contains(needle), "{ty}: {err}");
}
}
#[test]
fn rejects_scalars_aidl_never_renders() {
for (decl, needle) in [
(
quote!(
fn go(&self, n: u32) -> BinderResult<()>;
),
"`u32`",
),
(
quote!(
fn go(&self, n: i16) -> BinderResult<()>;
),
"`i16`",
),
(
quote!(
fn go(&self, n: u8) -> BinderResult<()>;
),
"`u8`",
),
(
quote!(
fn go(&self, n: char) -> BinderResult<()>;
),
"`char`",
),
(
quote!(
fn go(&self, v: &[u32]) -> BinderResult<()>;
),
"`u32`",
),
(
quote!(
fn go(&self, v: &[i8]) -> BinderResult<()>;
),
"element spelling",
),
(
quote!(
fn go(&self) -> BinderResult<u64>;
),
"`u64`",
),
(
quote!(
fn go(&self) -> BinderResult<u128>;
),
"`u128`",
),
(
quote!(
fn go(&self) -> BinderResult<Box<u128>>;
),
"`u128`",
),
(
quote!(
fn go(&self, p: &Pair<u32>) -> BinderResult<()>;
),
"`u32`",
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains(needle), "{needle}: {err}");
}
render(quote! {
pub trait IOk {
fn a(&self, v: bool, w: i8, x: i32, y: i64) -> BinderResult<()>;
fn b(&self, v: f32, w: f64, x: u16) -> BinderResult<()>;
fn c(&self, v: &[u8]) -> BinderResult<()>;
fn d(&self, v: Option<&[u8]>) -> BinderResult<()>;
fn e(&self) -> BinderResult<i64>;
}
});
}
#[test]
fn rejects_a_box_aidl_never_renders() {
for decl in [
quote!(
fn go(&self) -> BinderResult<Box<Cfg>>;
),
quote!(
fn go(&self, c: &Box<Cfg>) -> BinderResult<()>;
),
quote!(
fn go(&self, c: &mut Box<Cfg>) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<Vec<Option<Box<Cfg>>>>;
),
quote!(
fn go(&self) -> BinderResult<Option<Box<i32>>>;
),
quote!(
fn go(&self, c: Option<Box<Cfg>>) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<std::boxed::Box<Cfg>>;
),
quote!(
fn go(&self) -> BinderResult<Option<Box<Cfg>>>;
),
quote!(
fn go(&self, c: Option<&Box<Cfg>>) -> BinderResult<()>;
),
quote!(
fn go(&self, c: &mut Option<Box<Cfg>>) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("never renders this `Box`"), "{decl}: {err}");
}
render(quote! {
pub trait IOk {
fn d(&self) -> BinderResult<crate::p::Box<Cfg>>;
}
});
let input: syn::DeriveInput = syn::parse2(quote! {
pub struct Node {
pub next: Option<Box<Node>>,
}
})
.unwrap();
parcelable::render_source(&input).expect("a field's `Option<Box<T>>` is accepted");
}
#[test]
fn rejects_a_generic_argument_aidl_never_renders() {
for decl in [
quote!(
fn go(&self) -> BinderResult<Pair<Vec<i32>>>;
),
quote!(
fn go(&self, p: &Pair<[i32; 3]>) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<Vec<Pair<Vec<i32>>>>;
),
quote!(
fn go(&self) -> BinderResult<crate::p::Box<Option<Cfg>>>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("names a type argument bare"), "{decl}: {err}");
}
let input: syn::DeriveInput = syn::parse2(quote! {
pub struct Holder {
pub p: Pair<Option<Cfg>>,
}
})
.unwrap();
let err = parcelable::render_source(&input)
.expect_err("a field's `Pair<Option<Cfg>>` is refused")
.to_string();
assert!(err.contains("names a type argument bare"), "{err}");
render(quote! {
pub trait IOk {
fn a(&self, p: &Pair<Cfg>) -> BinderResult<Option<Pair<Pair<String>>>>;
fn b(&self) -> BinderResult<Vec<Pair<i32>>>;
}
});
}
#[test]
fn rejects_an_empty_descriptor() {
let err = match syn::parse2::<Args>(quote!(descriptor = "")) {
Ok(_) => panic!("an empty `descriptor` must be refused"),
Err(e) => e.to_string(),
};
assert!(err.contains("cannot be empty"), "{err}");
}
#[test]
fn self_referencing_interface() {
assert_same(
r#"
interface IGolden6 {
void register(in IGolden6 cb);
IGolden6 fetch();
}
"#,
"IGolden6",
quote! {
pub trait IGolden6 {
fn register(
&self,
cb: &rsbinder::Strong<dyn IGolden6>,
) -> BinderResult<()>;
fn fetch(&self) -> BinderResult<rsbinder::Strong<dyn IGolden6>>;
}
},
);
}
#[test]
fn parcelable_codec_matches_aidl() {
let input: syn::DeriveInput = syn::parse2(quote! {
pub struct GoldenConfig {
pub name: String,
pub retries: i32,
pub verbose: bool,
pub timeoutNanos: i64,
pub ratio: f64,
pub extra: Option<Vec<u8>>,
pub tags: Vec<String>,
pub labels: Option<Vec<Option<String>>>,
pub slots: [String; 3],
pub fds: [Option<rsbinder::ParcelFileDescriptor>; 3],
pub ibinder: Option<rsbinder::SIBinder>,
}
})
.unwrap();
let actual = parcelable::render_source(&input).expect("render");
for enabled_async in [false, true] {
let expected = from_aidl(
r#"
parcelable GoldenConfig {
String name;
int retries;
boolean verbose;
long timeoutNanos;
double ratio;
@nullable byte[] extra;
String[] tags;
@nullable String[] labels;
String[3] slots;
ParcelFileDescriptor[3] fds;
@nullable IBinder ibinder;
}
"#,
"GoldenConfig",
enabled_async,
);
assert_eq!(expected, actual, "generated parcelable differs");
}
}
#[test]
fn parcelable_and_enum_arguments() {
let files = [
(
"com/example/IGolden7.aidl",
"package com.example;\nimport com.example.GoldenCfg;\n\
import com.example.GoldenMode;\n\
interface IGolden7 {\n\
\x20 GoldenCfg apply(in GoldenCfg cfg, in GoldenMode mode);\n}\n",
),
(
"com/example/GoldenCfg.aidl",
"package com.example;\nparcelable GoldenCfg {\n String name;\n\
\x20 int retries;\n}\n",
),
(
"com/example/GoldenMode.aidl",
"package com.example;\n@Backing(type=\"int\")\n\
enum GoldenMode {\n FAST = 0,\n SAFE = 1,\n}\n",
),
];
assert_same_files(
&files,
"com/example/IGolden7.aidl",
"IGolden7",
"com.example.IGolden7",
quote! {
pub trait IGolden7 {
fn apply(
&self,
cfg: &super::GoldenCfg::GoldenCfg,
mode: super::GoldenMode::GoldenMode,
) -> BinderResult<super::GoldenCfg::GoldenCfg>;
}
},
);
}
#[test]
fn rejects_generic_trait() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IBad<T> {
fn go(&self) -> BinderResult<()>;
}
})
.unwrap();
let err = render_source(&Args { descriptor: None }, &item).unwrap_err();
assert!(err.to_string().contains("cannot be generic"), "{err}");
}
#[test]
fn rejects_oneway_with_return() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IBad {
#[oneway]
fn go(&self) -> BinderResult<i32>;
}
})
.unwrap();
let err = render_source(&Args { descriptor: None }, &item).unwrap_err();
assert!(err.to_string().contains("cannot return a value"), "{err}");
}
#[test]
fn rejects_missing_receiver() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IBad {
fn go() -> BinderResult<()>;
}
})
.unwrap();
let err = render_source(&Args { descriptor: None }, &item).unwrap_err();
assert!(err.to_string().contains("&self"), "{err}");
}
#[test]
fn descriptor_defaults_to_trait_name_and_can_be_overridden() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IHello {
fn go(&self) -> BinderResult<()>;
}
})
.unwrap();
let bare = render_source(&Args { descriptor: None }, &item).unwrap();
assert!(bare.contains(r#""IHello""#), "{bare}");
let named = render_source(
&Args {
descriptor: Some("com.example.IHello".into()),
},
&item,
)
.unwrap();
assert!(named.contains(r#""com.example.IHello""#), "{named}");
}
fn render(tokens: proc_macro2::TokenStream) -> String {
let item: ItemTrait = syn::parse2(tokens).expect("parse trait");
render_source(&Args { descriptor: None }, &item).expect("render")
}
fn reject(tokens: proc_macro2::TokenStream) -> String {
let item: ItemTrait = syn::parse2(tokens).expect("parse trait");
render_source(&Args { descriptor: None }, &item)
.unwrap_err()
.to_string()
}
#[test]
fn raw_identifiers_are_not_double_escaped() {
let s = render(quote! {
pub trait IRaw {
fn r#type(&self, r#match: i32) -> BinderResult<()>;
}
});
assert!(!s.contains("r#r#"), "{s}");
assert!(!s.contains("_arg_r#"), "{s}");
assert!(s.contains("fn r#type("), "{s}");
assert!(s.contains("_arg_match"), "{s}");
syn::parse_file(&s).unwrap_or_else(|e| panic!("does not parse: {e}\n{s}"));
}
#[test]
fn raw_trait_name_renders() {
let item: ItemTrait = syn::parse2(quote! {
pub trait r#type {
fn go(&self) -> BinderResult<()>;
}
})
.unwrap();
let s = render_source(&Args { descriptor: None }, &item).expect("render");
assert!(s.contains("pub trait r#type"), "{s}");
assert!(s.contains("Bntype"), "{s}");
assert!(!s.contains("r#r#"), "{s}");
syn::parse_file(&s).unwrap_or_else(|e| panic!("does not parse: {e}\n{s}"));
}
#[test]
fn rejects_trait_level_attributes() {
let err = reject(quote! {
#[cfg(feature = "x")]
pub trait IBad {
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("unsupported attribute"), "{err}");
let err = reject(quote! {
#[oneway]
pub trait IBad {
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("unsupported attribute"), "{err}");
}
#[test]
fn rejects_where_clauses() {
let err = reject(quote! {
pub trait IBad where Self: Sized {
fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("where clause"), "{err}");
let err = reject(quote! {
pub trait IBad {
fn go(&self) -> BinderResult<()> where Self: Sized;
}
});
assert!(err.contains("where clause"), "{err}");
}
#[test]
fn rejects_signature_modifiers() {
let err = reject(quote! {
pub trait IBad {
async fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("sync"), "{err}");
let err = reject(quote! {
pub trait IBad {
unsafe fn go(&self) -> BinderResult<()>;
}
});
assert!(err.contains("unsafe"), "{err}");
}
#[test]
fn rejects_non_shared_receivers() {
for recv in [
quote!(self),
quote!(mut self),
quote!(&mut self),
quote!(&'static self),
quote!(self: Box<Self>),
] {
let err = reject(quote! {
pub trait IBad {
fn go(#recv) -> BinderResult<()>;
}
});
assert!(err.contains("`&self`"), "{recv}: {err}");
}
let err = reject(quote! {
pub trait IBad {
fn go(&'static self) -> BinderResult<()>;
}
});
assert!(err.contains("explicit lifetime on `self`"), "{err}");
render(quote! {
pub trait IOk {
fn go(&'_ self) -> BinderResult<()>;
}
});
}
#[test]
fn rejects_an_attribute_on_the_receiver() {
for recv in [
quote!(
#[oneway]
&self
),
quote!(
#[cfg(feature = "never")]
&self
),
] {
let err = reject(quote! {
pub trait IBad {
fn go(#recv) -> BinderResult<()>;
}
});
assert!(err.contains("unsupported attribute"), "{recv}: {err}");
}
}
#[test]
fn rejects_borrowed_return_inside_option() {
let err = reject(quote! {
pub trait IBad {
fn go(&self) -> BinderResult<Option<&str>>;
}
});
assert!(err.contains("borrow"), "{err}");
}
#[test]
fn rejects_out_nullable_string() {
for decl in [
quote!(
fn go(&self, s: &mut Option<String>) -> BinderResult<()>;
),
quote!(
fn go(&self, #[inout] s: &mut Option<String>) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("`String` cannot be an"), "{err}");
}
}
#[test]
fn rejects_the_directions_aidl_refuses_for_a_binder_or_fd() {
for decl in [
quote!(
fn go(&self, v: &mut rsbinder::Strong<dyn IOther>) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut Option<rsbinder::Strong<dyn IOther>>) -> BinderResult<()>;
),
quote!(
fn go(&self, #[inout] v: &mut rsbinder::Strong<dyn IOther>) -> BinderResult<()>;
),
quote!(
fn go(&self, #[inout] v: &mut Option<rsbinder::SIBinder>) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut rsbinder::SIBinder) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut Option<rsbinder::SIBinder>) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut rsbinder::ParcelFileDescriptor) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &mut Option<rsbinder::ParcelFileDescriptor>) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(
err.contains("parameter — `.aidl` passes it"),
"{decl}: {err}"
);
}
let err = reject(quote! {
pub trait IBad {
fn go(&self, #[nonnull] v: &mut Vec<rsbinder::SIBinder>) -> BinderResult<()>;
}
});
assert!(err.contains("unsupported attribute"), "{err}");
render(quote! {
pub trait IOk {
fn a(&self, #[inout] v: &mut rsbinder::ParcelFileDescriptor) -> BinderResult<()>;
fn b(&self, #[inout] v: &mut Option<rsbinder::ParcelFileDescriptor>)
-> BinderResult<()>;
fn c(&self, v: &mut Vec<Option<rsbinder::Strong<dyn IOther>>>) -> BinderResult<()>;
fn d(&self, #[inout] v: &mut Vec<rsbinder::SIBinder>) -> BinderResult<()>;
}
});
}
#[test]
fn rejects_an_out_array_of_bare_binder_objects_or_fds() {
for ty in [
quote!(&mut Vec<rsbinder::ParcelFileDescriptor>),
quote!(&mut Option<Vec<rsbinder::ParcelFileDescriptor>>),
quote!(&mut Vec<rsbinder::Strong<dyn IOther>>),
quote!(&mut [rsbinder::SIBinder; 3]),
quote!(&mut [[rsbinder::ParcelFileDescriptor; 3]; 2]),
] {
let err = reject(quote! {
pub trait IBad {
fn go(&self, v: #ty) -> BinderResult<()>;
}
});
assert!(err.contains("`Option<_>` elements"), "{ty}: {err}");
}
render(quote! {
pub trait IOk {
fn a(&self, #[inout] v: &mut Vec<rsbinder::ParcelFileDescriptor>)
-> BinderResult<()>;
}
});
}
#[test]
fn a_macro_interpolated_type_renders_like_the_bare_one() {
let str_ty = proc_macro2::Group::new(proc_macro2::Delimiter::None, quote!(str));
let ref_str = proc_macro2::Group::new(proc_macro2::Delimiter::None, quote!(&str));
assert_same(
r#"
interface IGolden10 {
void take(in String s, in @nullable String t);
}
"#,
"IGolden10",
quote! {
pub trait IGolden10 {
fn take(&self, s: &#str_ty, t: Option<#ref_str>) -> BinderResult<()>;
}
},
);
}
#[test]
fn a_macro_interpolated_parameter_keeps_its_direction() {
let vec_ty = proc_macro2::Group::new(proc_macro2::Delimiter::None, quote!(&mut Vec<i32>));
let ret_ty =
proc_macro2::Group::new(proc_macro2::Delimiter::None, quote!(BinderResult<()>));
assert_same(
r#"
interface IGolden11 {
void fill(out int[] v);
void bump(inout int[] v);
}
"#,
"IGolden11",
quote! {
pub trait IGolden11 {
fn fill(&self, v: #vec_ty) -> #ret_ty;
fn bump(&self, #[inout] v: #vec_ty) -> #ret_ty;
}
},
);
}
#[test]
fn rejects_argument_shapes_with_no_wire_form() {
for (ty, needle) in [
(quote!(()), "`()` has no wire form"),
(quote!(dyn IOther), "trait object"),
(quote!(&dyn IOther), "trait object"),
(quote!(Option<&dyn IOther>), "trait object"),
(quote!(&[()]), "`()` has no wire form"),
(quote!(Vec<()>), "`()` has no wire form"),
(quote!(&[dyn IOther]), "trait object"),
(quote!(&mut [i32]), "`&mut [T]`"),
] {
let err = reject(quote! {
pub trait IBad {
fn go(&self, v: #ty) -> BinderResult<()>;
}
});
assert!(err.contains(needle), "{ty}: {err}");
}
}
#[test]
fn rejects_a_nullable_primitive_behind_a_slice_or_array() {
for ty in [
quote!(&[Option<i32>]),
quote!([Option<i32>; 4]),
quote!(Vec<Option<i32>>),
] {
let err = reject(quote! {
pub trait IBad {
fn go(&self, v: #ty) -> BinderResult<()>;
}
});
assert!(err.contains("cannot be nullable"), "{ty}: {err}");
}
}
#[test]
fn rejects_a_parcelable_holder_in_a_signature() {
for decl in [
quote!(
fn go(&self, h: &rsbinder::ParcelableHolder) -> BinderResult<()>;
),
quote!(
fn go(&self, h: &[rsbinder::ParcelableHolder]) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<rsbinder::ParcelableHolder>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("`ParcelableHolder` cannot appear"), "{err}");
}
}
#[test]
fn rejects_duplicate_names() {
let err = reject(quote! {
pub trait IBad {
fn go(&self, a: i32, a: i32) -> BinderResult<()>;
}
});
assert!(err.contains("duplicate argument name `a`"), "{err}");
let err = reject(quote! {
pub trait IBad {
fn go(&self, r#type: i32, r#type: i32) -> BinderResult<()>;
}
});
assert!(err.contains("duplicate argument name `type`"), "{err}");
let err = reject(quote! {
pub trait IBad {
fn go(&self) -> BinderResult<()>;
fn go(&self, a: i32) -> BinderResult<()>;
}
});
assert!(err.contains("duplicate method name `go`"), "{err}");
}
#[test]
fn rejects_a_method_named_like_a_generated_trait_item() {
for name in [
"descriptor",
"getDefaultImpl",
"setDefaultImpl",
"dump",
"as_binder",
] {
let ident = syn::Ident::new(name, proc_macro2::Span::call_site());
let err = reject(quote! {
pub trait IBad {
fn #ident(&self, a: i32) -> BinderResult<()>;
}
});
assert!(
err.contains(&format!(
"method `{name}` collides with an item of the generated Rust trait"
)),
"{err}"
);
}
let err = reject(quote! {
pub trait IBad {
fn r#dump(&self) -> BinderResult<()>;
}
});
assert!(err.contains("method `dump` collides"), "{err}");
}
#[test]
fn rejects_the_aidl_reserved_method_signatures() {
for tokens in [
quote! { pub trait IBad { fn asBinder(&self) -> BinderResult<()>; } },
quote! { pub trait IBad { fn getInterfaceHash(&self) -> BinderResult<String>; } },
quote! { pub trait IBad { fn getInterfaceVersion(&self) -> BinderResult<i32>; } },
quote! { pub trait IBad { fn getTransactionName(&self, c: i32) -> BinderResult<String>; } },
] {
let err = reject(tokens);
assert!(err.contains("is reserved for internal use"), "{err}");
}
render(quote! {
pub trait IOk {
fn getInterfaceVersion(&self, a: i32) -> BinderResult<i32>;
fn getTransactionName(&self) -> BinderResult<String>;
}
});
}
#[test]
fn an_empty_allow_list_names_no_attribute() {
let err = reject(quote! {
pub trait IBad {
fn go(#[allow(dead_code)] &self) -> BinderResult<()>;
}
});
assert_eq!(
err, "unsupported attribute; no attribute is supported here",
"{err}"
);
}
#[test]
fn rejects_a_repeated_descriptor() {
let err = match syn::parse2::<Args>(quote!(descriptor = "a", descriptor = "b")) {
Ok(_) => panic!("a repeated `descriptor` must be refused"),
Err(e) => e.to_string(),
};
assert!(err.contains("more than once"), "{err}");
}
#[test]
fn rejects_what_the_re_render_would_drop() {
let err = reject(quote! {
pub trait IBad {
fn go(&self) -> BinderResult<i32, String>;
}
});
assert!(err.contains("exactly one type argument"), "{err}");
for decl in [
quote!(
#[oneway(false)]
fn go(&self) -> BinderResult<()>;
),
quote!(
#[oneway = "no"]
fn go(&self) -> BinderResult<()>;
),
quote!(
fn go(&self, #[inout(no)] v: &mut Vec<i32>) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("takes no arguments"), "{err}");
}
}
#[test]
fn rejects_self_paths() {
for decl in [
quote!(
fn go(&self, cfg: &self::Config) -> BinderResult<()>;
),
quote!(
fn go(&self, v: Vec<self::Cfg>) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<Option<Vec<self::pair::Pair<Cfg>>>>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("`self::`"), "{err}");
}
}
#[test]
fn rejects_the_reserved_name_prefix() {
for tokens in [
quote! {
pub trait __RsbPool {
fn go(&self) -> BinderResult<()>;
}
},
quote! {
pub trait IBad {
fn go(&self, v: &__RsbService) -> BinderResult<()>;
}
},
quote! {
pub trait IBad {
fn go(&self) -> BinderResult<Vec<__RsbRuntime>>;
}
},
quote! {
pub trait IBad {
fn go(&self, v: &rsbinder::Strong<dyn __RsbAsyncWrapper>) -> BinderResult<()>;
}
},
quote! {
pub trait IBad {
fn go(&self, #[inout] v: &mut [__RsbPool; 3]) -> BinderResult<()>;
}
},
] {
let err = reject(tokens);
assert!(err.contains("reserved"), "{err}");
}
render(quote! {
pub trait IOk {
fn go(&self, v: &crate::m::__RsbService) -> BinderResult<()>;
}
});
}
#[test]
fn rejects_self_in_a_signature() {
for decl in [
quote!(
fn go(&self, other: &Self) -> BinderResult<()>;
),
quote!(
fn go(&self, v: &[Self]) -> BinderResult<()>;
),
quote!(
fn go(&self) -> BinderResult<Option<Self>>;
),
quote!(
fn go(&self, v: Vec<Self>) -> BinderResult<()>;
),
] {
let err = reject(quote! {
pub trait IBad {
#decl
}
});
assert!(err.contains("cannot name `Self`"), "{err}");
}
}
#[test]
fn qualified_vec_is_still_a_length_carrying_out_vector() {
let bare = render(quote! {
pub trait IOut {
fn fill(&self, values: &mut Vec<i32>) -> BinderResult<()>;
}
});
let qualified = render(quote! {
pub trait IOut {
fn fill(&self, values: &mut std::vec::Vec<i32>) -> BinderResult<()>;
}
});
assert!(bare.contains("write_slice_size"), "{bare}");
assert!(
qualified.contains("write_slice_size"),
"a qualified `Vec` must still write the length word:\n{qualified}"
);
assert!(qualified.contains("resize_out_vec"), "{qualified}");
}
#[test]
fn qualified_paths_keep_the_nullable_and_fd_guards() {
let s = render(quote! {
pub trait IOut {
fn fill(&self, v: &mut core::option::Option<std::vec::Vec<i32>>)
-> BinderResult<()>;
}
});
assert!(s.contains("resize_nullable_out_vec"), "{s}");
let s = render(quote! {
pub trait IOut {
fn fds(
&self,
v: &mut std::vec::Vec<Option<rsbinder::ParcelFileDescriptor>>,
) -> BinderResult<()>;
}
});
assert!(
s.contains("iter().any(::core::option::Option::is_none)"),
"{s}"
);
}
#[test]
fn qualified_nullable_in_arguments_borrow_through() {
let s = render(quote! {
pub trait IIn {
fn a(&self, x: std::option::Option<&str>) -> BinderResult<()>;
fn b(&self, x: std::option::Option<&Cfg>) -> BinderResult<()>;
fn c(&self, x: core::option::Option<&[u8]>) -> BinderResult<()>;
}
});
for (method, call) in [
("a", "_arg_x.as_deref()"),
("b", "_arg_x.as_ref()"),
("c", "_arg_x.as_deref()"),
] {
assert!(
s.contains(&format!("_service.r#{method}({call})")),
"{method}: {s}"
);
}
}
#[test]
fn by_value_argument_is_asserted_copy() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IBad {
fn go(&self, mode: super::Mode::Mode) -> BinderResult<()>;
}
})
.unwrap();
let expanded = expand(&Args { descriptor: None }, &item)
.expect("expand")
.to_string();
assert!(
expanded.contains("T : :: core :: marker :: Copy"),
"{expanded}"
);
assert!(
expanded.contains("__rsbinder_assert_copy :: < super :: Mode :: Mode > ()"),
"{expanded}"
);
}
#[test]
fn copy_assertion_shares_the_generated_module_scope() {
let item: ItemTrait = syn::parse2(quote! {
pub trait IScoped {
fn go(&self, mode: super::Mode::Mode) -> BinderResult<()>;
}
})
.unwrap();
let expanded = expand(&Args { descriptor: None }, &item).expect("expand");
let file: syn::File = syn::parse2(expanded).expect("expansion parses");
let module = file
.items
.iter()
.find_map(|i| match i {
syn::Item::Mod(m) => Some(m),
_ => None,
})
.expect("generated module");
let body = quote!(#module).to_string();
assert!(body.contains("__rsbinder_assert_copy"), "{body}");
}
#[test]
fn generated_module_follows_the_trait_visibility() {
let item: ItemTrait = syn::parse2(quote! {
trait IPrivate {
fn go(&self) -> BinderResult<()>;
}
})
.unwrap();
let expanded = expand(&Args { descriptor: None }, &item).expect("expand");
let file: syn::File = syn::parse2(expanded).expect("expansion parses");
let module = file
.items
.iter()
.find_map(|i| match i {
syn::Item::Mod(m) => Some(m),
_ => None,
})
.expect("generated module");
assert!(
matches!(module.vis, syn::Visibility::Inherited),
"a private trait must not get a `pub` module"
);
}
#[test]
fn interfaces_sharing_a_stem_declare_the_same_marker() {
let marker = |tokens| {
let item: ItemTrait = syn::parse2(tokens).unwrap();
let expanded = expand(&Args { descriptor: None }, &item).expect("expand");
let file: syn::File = syn::parse2(expanded).expect("expansion parses");
file.items
.iter()
.find_map(|i| match i {
syn::Item::Const(c) => Some(c.ident.to_string()),
_ => None,
})
.expect("marker const")
};
let prefixed = marker(quote!(
pub trait IFoo {
fn go(&self) -> BinderResult<()>;
}
));
let bare = marker(quote!(
pub trait Foo {
fn go(&self) -> BinderResult<()>;
}
));
assert_eq!(prefixed, "__rsbinder_one_interface_per_stem_Foo");
assert_eq!(prefixed, bare);
let kept = marker(quote!(
pub trait Ifoo {
fn go(&self) -> BinderResult<()>;
}
));
assert_eq!(kept, "__rsbinder_one_interface_per_stem_Ifoo");
}
}