pub mod attrs;
mod kv;
pub mod leak;
mod transformer;
mod typeleak;
pub use attrs::{remove_attributes, CheckCall, OwnedAttrs, Query};
pub use kv::KvArgs;
pub use transformer::{signature_only, transformer};
pub use type_leak;
pub use typeleak::TypeLeak;
#[doc(hidden)]
pub use crate::{callback, transformer};
use proc_macro2::{Ident, Span, TokenStream};
use std::sync::atomic::{AtomicU64, Ordering};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Attribute, Path, Result, Token};
use template_quote::{quote, ToTokens};
pub const ANNOUNCE_FORMAT: &str = "1";
const FORMAT_TAG: &str = "announce_format";
const FORMAT_ERROR: &str = "announce: incompatible wire format";
pub fn format_prefix() -> TokenStream {
quote! { @announce_format #ANNOUNCE_FORMAT }
}
fn format_error(span: Span, found: &str) -> syn::Error {
syn::Error::new(
span,
format!(
"{}: found `@announce_format {:?}`, expected `@announce_format {:?}`. The item was \
announced, or the macro that reads it was built, with another version of \
`announce`, whose wire format this version cannot read: use one version of \
`announce` in the whole chain",
FORMAT_ERROR, found, ANNOUNCE_FORMAT
),
)
}
pub(crate) fn is_format_error(e: &syn::Error) -> bool {
e.to_string().starts_with(FORMAT_ERROR)
}
pub(crate) fn parse_format(input: ParseStream) -> Result<Option<String>> {
if !input.peek(Token![@]) {
return Ok(None);
}
let fork = input.fork();
let _: Token![@] = fork.parse()?;
match fork.parse::<Ident>() {
Ok(i) if i == FORMAT_TAG => {}
_ => return Ok(None),
}
let lit: syn::LitStr = fork.parse().map_err(|e| {
syn::Error::new(
e.span(),
"announce: `@announce_format` must be followed by a string literal, like \
`@announce_format \"1\"`",
)
})?;
syn::parse::discouraged::Speculative::advance_to(input, &fork);
let found = lit.value();
if found != ANNOUNCE_FORMAT {
return Err(format_error(lit.span(), &found));
}
Ok(Some(found))
}
fn split_format(ts: TokenStream) -> (Option<TokenStream>, TokenStream) {
use proc_macro2::TokenTree;
let mut it = ts.into_iter();
let first: Vec<TokenTree> = it.by_ref().take(3).collect();
let is_prefix = matches!(
first.as_slice(),
[TokenTree::Punct(p), TokenTree::Ident(i), TokenTree::Literal(_)]
if p.as_char() == '@' && i == FORMAT_TAG
);
if is_prefix {
(Some(first.into_iter().collect()), it.collect())
} else {
(None, first.into_iter().chain(it).collect())
}
}
pub fn gensym(prefix: &str) -> Ident {
use std::hash::{BuildHasher, Hasher};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
Ident::new(&format!("{}{:016x}", prefix, h.finish()), Span::call_site())
}
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Cont {
Bang(Path),
Attr {
attr: Attribute,
name: Option<Ident>,
},
}
impl Cont {
pub fn bang(mut path: Path) -> Self {
for seg in &mut path.segments {
seg.arguments = syn::PathArguments::None;
}
Cont::Bang(path)
}
pub fn derive(path: Path) -> Self {
Cont::Attr {
attr: syn::parse_quote!(#[derive(#path)]),
name: None,
}
}
pub fn path(&self) -> Path {
match self {
Cont::Bang(p) => p.clone(),
Cont::Attr { attr, .. } => {
if attr.path().is_ident("derive") {
let paths =
attr.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated);
if let Some(p) = paths.ok().and_then(|p| p.into_iter().next()) {
return p;
}
}
attr.path().clone()
}
}
}
}
fn parse_macro_path(input: ParseStream) -> Result<Path> {
let leading_colon: Option<Token![::]> = input.parse()?;
let mut segments = Punctuated::new();
loop {
let ident = Ident::parse_any(input)?;
segments.push_value(syn::PathSegment::from(ident));
if input.peek(Token![::]) {
segments.push_punct(input.parse()?);
} else {
break;
}
}
Ok(Path {
leading_colon,
segments,
})
}
impl Parse for Cont {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(Token![#]) {
let pound_token = input.parse()?;
let content;
let bracket_token = syn::bracketed!(content in input);
let attr = Attribute {
pound_token,
style: syn::AttrStyle::Outer,
bracket_token,
meta: content.parse()?,
};
let name = if input.peek(syn::Ident) {
Some(input.parse()?)
} else {
None
};
Ok(Cont::Attr { attr, name })
} else {
let path = parse_macro_path(input)?;
input.parse::<Token![!]>()?;
Ok(Cont::Bang(path))
}
}
}
impl Cont {
fn eq_key(&self) -> String {
match self {
Cont::Bang(_) => self.to_token_stream().to_string(),
Cont::Attr { attr, .. } => attr.to_token_stream().to_string(),
}
}
}
impl PartialEq for Cont {
fn eq(&self, other: &Self) -> bool {
self.eq_key() == other.eq_key()
}
}
impl Eq for Cont {}
impl std::hash::Hash for Cont {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.eq_key().hash(state)
}
}
impl ToTokens for Cont {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Cont::Bang(p) => {
let mut p = p.clone();
for seg in &mut p.segments {
seg.arguments = syn::PathArguments::None;
}
tokens.extend(quote! { #p ! })
}
Cont::Attr { attr, name } => tokens.extend(quote! { #attr #name }),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ContList(pub Vec<Cont>);
impl Parse for ContList {
fn parse(input: ParseStream) -> Result<Self> {
let p: Punctuated<Cont, Token![,]> = Punctuated::parse_terminated(input)?;
Ok(ContList(p.into_iter().collect()))
}
}
impl ToTokens for ContList {
fn to_tokens(&self, tokens: &mut TokenStream) {
let items = &self.0;
tokens.extend(quote! { [ #(#items),* ] });
}
}
impl FromIterator<Cont> for ContList {
fn from_iter<I: IntoIterator<Item = Cont>>(iter: I) -> Self {
ContList(iter.into_iter().collect())
}
}
pub fn is_expression_position(options: &TokenStream) -> bool {
matches!(
options.clone().into_iter().next(),
Some(proc_macro2::TokenTree::Ident(i)) if i == "expr"
)
}
impl ContList {
pub fn name_dummies(&mut self) {
for c in &mut self.0 {
if let Cont::Attr { name, .. } = c {
if name.is_none() {
*name = Some(gensym("__AnnounceT_"));
}
}
}
}
pub fn invoke(&self, body: TokenStream) -> TokenStream {
self.invoke_with_options(None, body)
}
pub fn invoke_with_options(
&self,
options: Option<&TokenStream>,
body: TokenStream,
) -> TokenStream {
let opts = options.map(|o| quote! { ( #o ) });
let in_expr = options.map_or(false, is_expression_position);
let mut list = self.clone();
list.name_dummies();
let format = format_prefix();
match list.0.first() {
None => quote! { compile_error! { "announce: empty continuation list" } },
Some(Cont::Attr { .. }) if in_expr => quote! {
compile_error! { "announce: a derive/attribute continuation emits items, so it cannot be used with `(expr)`" }
},
Some(Cont::Bang(p)) => quote! { #p ! { #format #opts #list #body } },
Some(Cont::Attr { attr, name }) => quote! {
#attr
#[allow(dead_code, non_camel_case_types)]
enum #name { __AnnouncePayload = (stringify! { #format #opts #list #body }, 0).1 }
},
}
}
}
#[derive(Clone, Debug)]
pub struct Meta {
pub path: Path,
pub args: Option<TokenStream>,
}
impl Meta {
pub fn parse_args_with<P: syn::parse::Parser>(&self, parser: P) -> Result<P::Output> {
parser.parse2(self.args.clone().unwrap_or_default())
}
pub fn kv(&self) -> Result<KvArgs> {
self.parse_args_with(KvArgs::parse)
}
pub fn name_values(&self) -> Result<Vec<syn::MetaNameValue>> {
let p: Punctuated<syn::MetaNameValue, Token![,]> =
self.parse_args_with(Punctuated::parse_terminated)?;
Ok(p.into_iter().collect())
}
}
impl Parse for Meta {
fn parse(input: ParseStream) -> Result<Self> {
let path = Path::parse_mod_style(input)?;
let args = if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
Some(content.parse()?)
} else {
None
};
Ok(Meta { path, args })
}
}
impl ToTokens for Meta {
fn to_tokens(&self, tokens: &mut TokenStream) {
let p = &self.path;
match &self.args {
Some(a) => tokens.extend(quote! { #p(#a) }),
None => tokens.extend(quote! { #p }),
}
}
}
#[derive(Clone, Debug)]
pub struct Entry {
pub key: Cont,
pub meta: Vec<Meta>,
pub item: TokenStream,
}
impl Entry {
pub fn item(&self) -> Result<syn::Item> {
syn::parse2(self.item.clone())
}
pub fn mock(&self) -> Option<Path> {
self.meta
.iter()
.find(|m| m.path.is_ident("mock"))
.and_then(|m| syn::parse2(m.args.clone()?).ok())
}
pub fn find_meta(&self, name: &str) -> Option<&Meta> {
self.meta
.iter()
.find(|m| m.path.segments.last().map_or(false, |s| s.ident == name))
}
pub fn type_leak(&self) -> Result<Option<TypeLeak>> {
let m = match self.find_meta("type_leak") {
Some(m) => m,
None => return Ok(None),
};
TypeLeak::from_kv(&mut m.kv()?).map(Some)
}
pub fn has_check(&self, name: &str) -> bool {
self.meta
.iter()
.any(|m| m.path.segments.last().map_or(false, |s| s.ident == name))
}
pub fn owned_attrs(&self) -> OwnedAttrs {
attrs::owned_from_metas(&self.meta)
}
pub fn item_without_owned_attrs(&self) -> Result<syn::Item> {
let mut item = self.item()?;
self.owned_attrs().strip(&mut item)?;
Ok(item)
}
}
impl Parse for Entry {
fn parse(input: ParseStream) -> Result<Self> {
let key = input.parse()?;
input.parse::<Token![=>]>()?;
let meta_content;
syn::bracketed!(meta_content in input);
let meta: Punctuated<Meta, Token![,]> = Punctuated::parse_terminated(&meta_content)?;
let item_content;
syn::braced!(item_content in input);
Ok(Entry {
key,
meta: meta.into_iter().collect(),
item: item_content.parse()?,
})
}
}
impl ToTokens for Entry {
fn to_tokens(&self, tokens: &mut TokenStream) {
let (key, meta, item) = (&self.key, &self.meta, &self.item);
tokens.extend(quote! { #key => [ #(#meta),* ] { #item } });
}
}
#[derive(Clone, Debug)]
pub struct Input {
pub format: Option<String>,
pub options: Option<TokenStream>,
pub list: ContList,
pub entries: Vec<Entry>,
pub rest: TokenStream,
}
impl Parse for Input {
fn parse(input: ParseStream) -> Result<Self> {
let format = parse_format(input)?;
while input.peek(Token![@]) {
let fork = input.fork();
let _: Token![@] = fork.parse()?;
match fork.parse::<Ident>() {
Ok(i) if i == "__announce_dollar" || i == "__announce_self" => {
let _g: proc_macro2::Group = fork.parse()?;
syn::parse::discouraged::Speculative::advance_to(input, &fork);
}
_ => break,
}
}
let options = if input.peek(syn::token::Paren) {
let o;
syn::parenthesized!(o in input);
Some(o.parse()?)
} else {
None
};
let content;
syn::bracketed!(content in input);
let list = content.parse()?;
let mut entries = Vec::new();
loop {
let fork = input.fork();
match fork.parse::<Entry>() {
Ok(e) => {
entries.push(e);
syn::parse::discouraged::Speculative::advance_to(input, &fork);
}
Err(e) => {
let probe = input.fork();
if probe.parse::<Cont>().is_ok()
&& probe.parse::<Token![=>]>().is_ok()
&& (probe.peek(syn::token::Bracket) || probe.peek(syn::token::Brace))
{
return Err(syn::Error::new(
e.span(),
format!("announce: malformed entry: {}", e),
));
}
break;
}
}
}
Ok(Input {
format,
options,
list,
entries,
rest: input.parse()?,
})
}
}
impl Input {
pub fn entries_in_order(&self) -> impl Iterator<Item = &Entry> {
self.entries.iter().rev()
}
pub fn fetch(&self, new: impl IntoIterator<Item = Cont>) -> TokenStream {
self.fetch_conts(new.into_iter().collect())
}
fn fetch_conts(&self, new: Vec<Cont>) -> TokenStream {
let fetched = self.fetched_paths();
if let Some(c) = new
.iter()
.find(|c| fetched.contains(&path_key(&c.path())) && self.entry_for(c).is_none())
{
let msg = format!(
"announce: `{}` was fetched but did not provide a definition; is it an `#[announce]` trampoline?",
c.to_token_stream()
);
return quote! { compile_error! { #msg } };
}
let mut options: Vec<TokenStream> = self.option_items();
if !new.is_empty() {
let paths = new.iter().map(|c| c.path());
options.push(quote! { __announce_fetched(#(#paths),*) });
}
let opts = if options.is_empty() {
None
} else {
Some(quote! { #(#options),* })
};
let list: ContList = new.into_iter().chain(self.list.0.iter().cloned()).collect();
let (entries, rest) = (&self.entries, &self.rest);
list.invoke_with_options(opts.as_ref(), quote! { #(#entries)* #rest })
}
pub fn has_option(&self, name: &str) -> bool {
self.options.as_ref().map_or(false, |o| {
o.clone()
.into_iter()
.any(|tt| matches!(tt, proc_macro2::TokenTree::Ident(i) if i == name))
})
}
pub fn option_items(&self) -> Vec<TokenStream> {
let mut items = Vec::new();
let mut cur = TokenStream::new();
for tt in self.options.clone().unwrap_or_default() {
if matches!(&tt, proc_macro2::TokenTree::Punct(p) if p.as_char() == ',') {
if !cur.is_empty() {
items.push(std::mem::take(&mut cur));
}
} else {
cur.extend(Some(tt));
}
}
if !cur.is_empty() {
items.push(cur);
}
items
}
fn fetched_paths(&self) -> Vec<String> {
let mut out = Vec::new();
for item in self.option_items() {
let parsed: Result<Meta> = syn::parse2(item);
if let Ok(m) = parsed {
if m.path.is_ident("__announce_fetched") {
let paths: Result<Punctuated<Path, Token![,]>> =
m.parse_args_with(Punctuated::parse_terminated);
if let Ok(paths) = paths {
out.extend(paths.iter().map(path_key));
}
}
}
}
out
}
pub fn rest_as<T: Parse>(&self) -> Result<T> {
syn::parse2(self.rest.clone())
}
pub fn entry_for(&self, cont: &Cont) -> Option<&Entry> {
let want = path_key(&cont.path());
self.entries
.iter()
.find(|e| path_key(&e.key.path()) == want)
}
pub fn gather(&self, needed: impl IntoIterator<Item = Cont>) -> Gather<'_> {
let needed: Vec<Cont> = needed.into_iter().collect();
if let Some(head) = self.list.0.first() {
if let Some(c) = needed
.iter()
.find(|c| path_key(&c.path()) == path_key(&head.path()))
{
let msg = format!(
"announce: `gather` asked for `{}`, which is the callback itself; \
request the definitions' trampolines instead",
c.to_token_stream()
);
return Gather::Fetch(quote! { compile_error! { #msg } });
}
}
let mut missing: Vec<Cont> = Vec::new();
for c in &needed {
if self.entry_for(c).is_none()
&& !missing
.iter()
.any(|m| path_key(&m.path()) == path_key(&c.path()))
{
missing.push(c.clone());
}
}
if missing.is_empty() {
return Gather::Ready(
needed
.iter()
.map(|c| self.entry_for(c).expect("checked above"))
.collect(),
);
}
Gather::Fetch(self.fetch_conts(missing))
}
pub fn forward(&self) -> Option<TokenStream> {
if self.list.0.len() < 2 {
return None;
}
let tail: ContList = self.list.0[1..].iter().cloned().collect();
let (entries, rest) = (&self.entries, &self.rest);
Some(tail.invoke_with_options(self.options.as_ref(), quote! { #(#entries)* #rest }))
}
}
pub enum Gather<'a> {
Ready(Vec<&'a Entry>),
Fetch(TokenStream),
}
fn path_key(p: &Path) -> String {
let mut p = p.clone();
for seg in &mut p.segments {
seg.arguments = syn::PathArguments::None;
}
p.to_token_stream().to_string()
}
#[doc(hidden)]
pub mod __private {
pub use proc_macro2;
pub use syn;
}
pub fn callback(input: TokenStream, f: impl FnOnce(Input) -> Result<TokenStream>) -> TokenStream {
syn::parse2::<Input>(input)
.and_then(f)
.unwrap_or_else(|e| e.to_compile_error())
}
#[doc(hidden)]
#[macro_export]
macro_rules! callback {
($(#[$m:meta])* $vis:vis fn $name:ident ($input:ident) $body:block) => {
$(#[$m])*
#[proc_macro]
$vis fn $name(__announce_ts: ::proc_macro::TokenStream) -> ::proc_macro::TokenStream {
::core::convert::Into::into($crate::helper::callback(
::core::convert::Into::into(__announce_ts),
|$input: $crate::helper::Input|
-> $crate::helper::__private::syn::Result<$crate::helper::__private::proc_macro2::TokenStream> {
$body
},
))
}
};
}
pub fn check(
input: TokenStream,
attributes: &[&str],
f: impl FnOnce(&mut CheckInput) -> Result<TokenStream>,
) -> TokenStream {
let run = || -> Result<TokenStream> {
for name in attributes {
if !is_attribute_name(name) {
return Err(syn::Error::new(
Span::call_site(),
format!(
"announce: `{}` is not an identifier: it cannot be an owned attribute name",
name
),
));
}
}
let owned = OwnedAttrs::from_strs(attributes);
match syn::parse2::<CheckCall>(input)? {
CheckCall::Query(q) => Ok(q.answer(&owned)),
CheckCall::Run(c) => {
owned.check_names()?;
f(&mut c.with_owned(owned))
}
}
};
run().unwrap_or_else(|e| e.to_compile_error())
}
fn is_attribute_name(name: &str) -> bool {
let name = name.strip_prefix("r#").unwrap_or(name);
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c == '_' || c.is_alphabetic())
&& chars.all(|c| c == '_' || c.is_alphanumeric())
&& name != "_"
}
pub fn payload(item: TokenStream) -> Option<TokenStream> {
let di: syn::DeriveInput = syn::parse2(item).ok()?;
let e = match di.data {
syn::Data::Enum(e) => e,
_ => return None,
};
let v = e.variants.first()?;
if v.ident != "__AnnouncePayload" || e.variants.len() != 1 {
return None;
}
let f = match &v.discriminant.as_ref()?.1 {
syn::Expr::Field(f) => f,
_ => return None,
};
let t = match &*f.base {
syn::Expr::Tuple(t) => t,
_ => return None,
};
let m = match t.elems.first()? {
syn::Expr::Macro(m) => m,
_ => return None,
};
if !m.mac.path.is_ident("stringify") {
return None;
}
Some(m.mac.tokens.clone())
}
pub fn redirect(item: TokenStream, target: &Path) -> Option<TokenStream> {
let (format, body) = split_format(payload(item)?);
let format = format.unwrap_or_else(format_prefix);
let dollar = proc_macro2::Punct::new('$', proc_macro2::Spacing::Alone);
Some(quote! {
#target ! { #format @__announce_self { #target } @__announce_dollar { #dollar } #body }
})
}
pub struct CheckInput {
pub args: TokenStream,
pub item: syn::Item,
pub owned: OwnedAttrs,
}
impl CheckInput {
pub fn with_owned(mut self, owned: OwnedAttrs) -> Self {
self.owned = owned;
self
}
pub fn owned_attributes(&self) -> Vec<Attribute> {
self.owned.find(&self.item)
}
}
impl Parse for CheckInput {
fn parse(input: ParseStream) -> Result<Self> {
parse_format(input)?;
if input.peek(Token![@]) {
return Err(input.error(
"announce: this check received the attribute query `@__announce_attrs` that \
`#[announce]` sends first, and does not answer it. Run the check with \
`announce::helper::check`, or parse `announce::helper::CheckCall` and answer \
`Query` (a `macro_rules!` check needs one more arm); every check must \
answer. See the `announce::helper::attrs` docs",
));
}
let content;
syn::parenthesized!(content in input);
let args = content.parse()?;
input.parse::<Token![,]>()?;
Ok(CheckInput {
args,
item: input.parse()?,
owned: OwnedAttrs::new(),
})
}
}
pub fn check_type(krate: &Path, ty: &syn::Type) -> TokenStream {
quote! {
const _: () = {
fn __announce_chk<__AnnounceT: ?::core::marker::Sized + #krate::Announce>() {}
let _ = __announce_chk::<#ty>;
};
}
}
pub fn check_trait(krate: &Path, path: &Path) -> TokenStream {
check_type(krate, &syn::parse_quote!(dyn #path))
}
#[cfg(test)]
mod tests {
use super::*;
fn s(ts: impl ToTokens) -> String {
ts.to_token_stream()
.to_string()
.split_whitespace()
.collect()
}
#[test]
fn bang_drops_generic_arguments() {
assert_eq!(s(Cont::bang(syn::parse_quote!(a::Enum<A, B>))), "a::Enum!");
}
#[test]
fn input_roundtrip_and_meta() {
let input: Input = syn::parse_quote! {
[cb!, #[derive(p::D)] N]
a::F! => [x::check(1), mock(::core::iter::Iterator)] { pub trait F {} }
#[derive(p::D)] M => [] { pub struct S; }
(rest)
};
assert_eq!(input.list.0.len(), 2);
assert_eq!(input.entries.len(), 2);
assert_eq!(s(&input.entries[0].meta[0]), "x::check(1)");
assert_eq!(
s(input.entries[0].find_meta("check").unwrap().args.clone()),
"1"
);
assert!(input.entries[0].has_check("check"));
assert_eq!(
s(input.entries[0].mock().unwrap()),
"::core::iter::Iterator"
);
assert_eq!(s(input.entries[1].key.path()), "p::D");
assert_eq!(s(&input.rest), "(rest)");
let order: Vec<String> = input.entries_in_order().map(|e| s(e.key.path())).collect();
assert_eq!(order, ["p::D", "a::F"]);
let fwd = input.forward().unwrap().to_string();
assert!(fwd.contains("__AnnouncePayload"), "{}", fwd);
}
#[test]
fn entries_know_the_attributes_their_checks_own() {
let input: Input = syn::parse_quote! {
[cb!]
a::F! => [x::check, attrs(my_lib, other), m::info(1), attrs(other, third)] {
#[my_lib(on_item)]
pub struct F { #[other] pub a: u8, #[third(x)] #[doc = "d"] pub b: u8 }
}
a::G! => [] { pub struct G; }
};
let owned = input.entries[0].owned_attrs();
let names: Vec<String> = owned.names().iter().map(|n| n.to_string()).collect();
assert_eq!(names, ["my_lib", "other", "third"]);
assert_eq!(owned.find(&input.entries[0].item().unwrap()).len(), 3);
let stripped = input.entries[0].item_without_owned_attrs().unwrap();
assert_eq!(s(&stripped), "pubstructF{puba:u8,#[doc=\"d\"]pubb:u8}");
assert!(input.entries[1].owned_attrs().is_empty());
assert_eq!(
s(input.entries[1].item_without_owned_attrs().unwrap()),
"pubstructG;"
);
let input: Input = syn::parse_quote! { [cb!] a::F! => [lib::attrs(x)] { struct F; } };
assert!(input.entries[0].owned_attrs().is_empty());
}
#[test]
fn check_input_reports_the_query_it_does_not_answer() {
let e = syn::parse2::<CheckInput>(template_quote::quote! {
@__announce_attrs [ c!, f! ] { st }
})
.err()
.unwrap()
.to_string();
assert!(
e.contains("does not answer it") && e.contains("announce::helper::check`"),
"{}",
e
);
let ok: CheckInput = syn::parse_quote! { (x, y), struct S; };
assert!(ok.owned.is_empty() && ok.owned_attributes().is_empty());
let ok = ok.with_owned(OwnedAttrs::from_strs(&["a"]));
assert!(ok.owned.contains("a"));
}
#[test]
fn check_answers_the_query_without_running_the_closure() {
let q = || -> TokenStream {
syn::parse_quote! { @__announce_attrs [ a::check!, b::check!, f::finish! ] { st } }
};
let mut ran = false;
let out = check(q(), &["my_lib", "other"], |_| {
ran = true;
Ok(TokenStream::new())
});
assert!(!ran);
assert_eq!(
s(out),
"b::check!{@announce_format\"1\"@__announce_attrs[b::check!,f::finish!]{stattrs(my_lib,other)}}"
);
let out = check(q(), &[], |_| panic!("the query is not a call"));
assert_eq!(
s(out),
"b::check!{@announce_format\"1\"@__announce_attrs[b::check!,f::finish!]{st}}"
);
let out = check(q(), &["r#type", "type"], |_| unreachable!());
assert_eq!(
s(out),
"b::check!{@announce_format\"1\"@__announce_attrs[b::check!,f::finish!]{stattrs(type)}}"
);
let out = s(check(
syn::parse_quote! { @__announce_attrs [ a::check! ] { st } },
&[],
|_| unreachable!(),
));
assert!(out.contains("compile_error") && out.contains("nomacrotocallafterthischeck"));
}
#[test]
fn check_runs_the_closure_with_the_owned_names() {
let mut seen = None;
let out = check(
syn::parse_quote! { (x, y), #[my_lib(a)] struct S { #[other] a: u8, #[kept] b: u8 } },
&["my_lib", "other"],
|c| {
seen = Some((
s(&c.args),
s(&c.item),
c.owned
.names()
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>(),
c.owned_attributes().len(),
));
Ok(quote! { pub const SEEN: u8 = 1; })
},
);
assert_eq!(s(out), "pubconstSEEN:u8=1;");
let (args, item, owned, found) = seen.unwrap();
assert_eq!(args, "x,y");
assert!(
item.starts_with("#[my_lib(a)]structS{#[other]a:u8"),
"{}",
item
);
assert_eq!(owned, ["my_lib", "other"]);
assert_eq!(found, 2);
let out = check(syn::parse_quote! { (), struct S; }, &[], |c| {
assert!(c.owned.is_empty());
c.args = quote! { changed };
Ok(c.args.clone())
});
assert_eq!(s(out), "changed");
}
#[test]
fn check_turns_errors_into_compile_errors() {
let out = s(check(
syn::parse_quote! { (), struct S(u8); },
&[],
|c| match &c.item {
syn::Item::Struct(it) => Err(syn::Error::new_spanned(&it.fields, "no tuples")),
_ => Ok(TokenStream::new()),
},
));
assert_eq!(out, "::core::compile_error!{\"notuples\"}");
let mut ran = false;
let out = s(check(syn::parse_quote! { not a check call }, &[], |_| {
ran = true;
Ok(TokenStream::new())
}));
assert!(!ran && out.contains("compile_error"), "{}", out);
for input in [
syn::parse_quote! { @__announce_attrs [ a!, f::finish! ] { st } },
syn::parse_quote! { (), struct S; },
] {
let out = s(check(input, &["my_lib", "doc"], |_| unreachable!()));
assert!(
out.contains("compile_error") && out.contains("`doc`cannotbeanownedattributename"),
"{}",
out
);
}
for bad in ["", "my-lib", "1st", "_", "a b", "r#"] {
let out = s(check(
syn::parse_quote! { (), struct S; },
&[bad],
|_| unreachable!(),
));
assert!(
out.contains("compile_error") && out.contains("isnotanidentifier"),
"{:?}: {}",
bad,
out
);
}
}
#[test]
fn fetch_prepends_and_returns() {
let input: Input = syn::parse_quote! { [me!, next!] a::F! => [] { struct S; } (rest) };
let out = s(input.fetch([Cont::bang(syn::parse_quote!(g::G))]));
assert_eq!(
out,
"g::G!{@announce_format\"1\"(__announce_fetched(g::G))[g::G!,me!,next!]a::F!=>[]{structS;}(rest)}"
);
assert_eq!(Cont::bang(syn::parse_quote!(a::F)), input.entries[0].key);
let out = s(input.fetch([]));
assert_eq!(
out,
"me!{@announce_format\"1\"[me!,next!]a::F!=>[]{structS;}(rest)}"
);
}
#[test]
fn fetch_of_a_path_that_left_no_entry_is_an_error() {
let again: Input = syn::parse_quote! {
(__announce_fetched(g::G)) [me!] a::F! => [] { struct S; }
};
let out = s(again.fetch([Cont::bang(syn::parse_quote!(g::G))]));
assert_eq!(
out,
"compile_error!{\"announce:`g::G!`wasfetchedbutdidnotprovideadefinition;isitan`#[announce]`trampoline?\"}"
);
let from_gather = match again.gather([Cont::bang(syn::parse_quote!(g::G))]) {
Gather::Fetch(ts) => s(ts),
Gather::Ready(_) => panic!(),
};
assert_eq!(out, from_gather);
let out = s(again.fetch([Cont::bang(syn::parse_quote!(g::G<u8>))]));
assert!(out.contains("wasfetchedbutdidnotprovide"), "{}", out);
let answered: Input = syn::parse_quote! {
(mine, __announce_fetched(g::G)) [me!] g::G! => [] { struct G; }
};
let out = s(answered.fetch([Cont::bang(syn::parse_quote!(g::G))]));
assert!(
out.starts_with("g::G!{@announce_format\"1\"(mine,__announce_fetched(g::G),__announce_fetched(g::G))"),
"{}",
out
);
let out = s(answered.fetch([Cont::bang(syn::parse_quote!(h::H))]));
assert!(
out.starts_with("h::H!{@announce_format\"1\"(mine,__announce_fetched(g::G),__announce_fetched(h::H))"),
"{}",
out
);
let out = s(again.fetch([
Cont::bang(syn::parse_quote!(h::H)),
Cont::bang(syn::parse_quote!(g::G)),
]));
assert!(out.starts_with("compile_error!"), "{}", out);
}
#[test]
fn fetch_and_gather_share_the_record() {
let input: Input = syn::parse_quote! { [me!] };
let first = match input.gather([Cont::bang(syn::parse_quote!(g::G))]) {
Gather::Fetch(ts) => ts,
Gather::Ready(_) => panic!(),
};
assert_eq!(
s(&first),
"g::G!{@announce_format\"1\"(__announce_fetched(g::G))[g::G!,me!]}"
);
let body: Input =
syn::parse2(template_quote::quote! { (__announce_fetched(g::G)) [me!] }).unwrap();
assert!(s(body.fetch([Cont::bang(syn::parse_quote!(g::G))]))
.contains("wasfetchedbutdidnotprovide"));
let raw = input.fetch([Cont::bang(syn::parse_quote!(g::G))]);
assert_eq!(
s(&raw),
"g::G!{@announce_format\"1\"(__announce_fetched(g::G))[g::G!,me!]}"
);
match body.gather([Cont::bang(syn::parse_quote!(g::G))]) {
Gather::Fetch(ts) => assert!(s(ts).contains("wasfetchedbutdidnotprovide")),
Gather::Ready(_) => panic!(),
}
}
#[test]
fn malformed_entry_is_an_error() {
let r = syn::parse2::<Input>(template_quote::quote! { [cb!] a::F! => { struct S; } });
assert!(r.err().unwrap().to_string().contains("malformed entry"));
}
#[test]
fn invoke_names_attribute_dummies() {
let list: ContList = [
Cont::derive(syn::parse_quote!(p::D)),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
let out = s(list.invoke(TokenStream::new()));
assert!(
out.starts_with(
"#[derive(p::D)]#[allow(dead_code,non_camel_case_types)]enum__AnnounceT_"
),
"{}",
out
);
let body = payload(list.invoke(TokenStream::new())).map(s).unwrap();
assert!(
body.starts_with("@announce_format\"1\"[#[derive(p::D)]__AnnounceT_"),
"{}",
body
);
assert!(redirect(list.invoke(TokenStream::new()), &syn::parse_quote!(::x::Y)).is_some());
}
}
#[cfg(test)]
mod kv_tests {
use super::*;
#[test]
fn kv_types_lists_flags_and_errors() {
let m: Meta = syn::parse_quote!(lib::info(ty = ::std::collections::HashMap<u8, Vec<u8>>, out = fn() -> u8, xs = [A, b::C], on));
let mut kv = m.kv().unwrap();
let ty: syn::Type = kv.require("ty").unwrap();
assert_eq!(
ty.to_token_stream().to_string().replace(' ', ""),
"::std::collections::HashMap<u8,Vec<u8>>"
);
let out: syn::Type = kv.require("out").unwrap();
assert_eq!(
out.to_token_stream().to_string().replace(' ', ""),
"fn()->u8"
);
let xs: Vec<Path> = kv.list("xs").unwrap().unwrap();
assert_eq!(xs.len(), 2);
assert!(kv.flag("on").unwrap());
assert!(kv.get::<syn::Type>("absent").unwrap().is_none());
kv.finish().unwrap();
let mut kv: KvArgs = syn::parse_quote!(a = 1, b = 2);
let _: syn::LitInt = kv.require("a").unwrap();
assert!(kv
.finish()
.unwrap_err()
.to_string()
.contains("unknown key `b`"));
assert!(syn::parse2::<KvArgs>(template_quote::quote!(a = 1, a = 2)).is_err());
let mut kv: KvArgs = syn::parse_quote!();
assert!(kv
.require::<syn::Type>("x")
.unwrap_err()
.to_string()
.contains("missing `x`"));
}
#[test]
fn stable_referrer_is_deterministic() {
let item: syn::ItemStruct = syn::parse_quote! {
pub struct S { a: Foo, b: Bar, c: Baz, d: Qux, e: Quux, f: Corge }
};
let analyse = || {
let mut l = type_leak::Leaker::new();
let context = l.intern_struct(&item, true).unwrap();
(
crate::helper::leak::StableReferrer::new(l.finish()),
context,
)
};
let order = || {
let (r, _) = analyse();
r.types()
.iter()
.map(|t| t.to_token_stream().to_string())
.collect::<Vec<_>>()
};
let first = order();
assert_eq!(first.len(), 6);
let mut sorted = first.clone();
sorted.sort();
assert_eq!(first, sorted);
for _ in 0..20 {
assert_eq!(order(), first);
}
let (r, context) = analyse();
let mut it: syn::Item = syn::Item::Struct(item.clone());
r.visit_item_mut(context, &mut it, |_, i| {
syn::parse_str(&format!("T{}", i)).unwrap()
});
let s = it.to_token_stream().to_string();
assert!(
s.contains("a : T3") && s.contains("b : T0") && s.contains("f : T2"),
"{}",
s
);
let ty: syn::Type = syn::parse_quote!(Qux);
assert_eq!(r.index_of(&ty), Some(5));
let ty = r.expand(context, ty, |_, i| {
syn::parse_str(&format!("T{}", i)).unwrap()
});
assert_eq!(ty.to_token_stream().to_string(), "T5");
}
#[test]
fn stable_referrer_numbers_required_entries_only() {
let item: syn::ItemStruct = syn::parse_quote! {
pub struct S { a: Foo, b: ::other::X, c: Bar, d: ::std::string::String }
};
let mut l = type_leak::Leaker::new();
let context = l.intern_struct(&item, true).unwrap();
let r = crate::helper::leak::StableReferrer::new(l.finish());
let types: Vec<String> = r
.types()
.iter()
.map(|t| t.to_token_stream().to_string())
.collect();
assert_eq!(types, ["Bar", "Foo"]);
let mut it = syn::Item::Struct(item);
r.visit_item_mut(context, &mut it, |_, i| {
syn::parse_str(&format!("T{}", i)).unwrap()
});
let s = it.to_token_stream().to_string();
assert!(
s.contains("a : T1") && s.contains("b : :: other :: X") && s.contains("c : T0"),
"{}",
s
);
}
}
#[cfg(test)]
mod options_tests {
use super::*;
#[test]
fn options_are_parsed_and_forwarded() {
let mut leaker = type_leak::Leaker::new();
let context = leaker
.intern_struct(
&syn::parse_quote!(
struct F<T> {
a: Local,
b: Vec<T>,
}
),
false,
)
.unwrap();
let tl = TypeLeak::new(
syn::parse_quote!(::my::A),
syn::parse_quote!(7u64),
leaker.finish(),
context,
);
let meta = tl.meta();
let input: Input = syn::parse_quote! {
(type_leak) [me!, next!]
a::F! => [#meta] { struct F<T>(T); }
(rest)
};
assert!(input.has_option("type_leak"));
let fwd = input.forward().unwrap().to_string().replace(' ', "");
assert!(
fwd.starts_with("next!{@announce_format\"1\"(type_leak)[next!]a::F!=>"),
"{}",
fwd
);
let fetch = input
.fetch([Cont::bang(syn::parse_quote!(g::G))])
.to_string()
.replace(' ', "");
assert!(
fetch.starts_with(
"g::G!{@announce_format\"1\"(type_leak,__announce_fetched(g::G))[g::G!,me!,next!]"
),
"{}",
fetch
);
let read = input.entries[0].type_leak().unwrap().unwrap();
assert_eq!(read.types().count(), 2);
assert_eq!(read.id.base10_digits(), "7");
assert_eq!(read.referrer, tl.referrer);
let proj = read.projection(1, &syn::parse_quote!((u8,)));
assert_eq!(
proj.to_string().replace(' ', ""),
"<()as::my::A<(u8,),7u64,1usize>>::Type"
);
let plain: Input = syn::parse_quote! { [me!] a::F! => [] { struct F; } };
assert!(!plain.has_option("type_leak"));
assert!(plain.entries[0].type_leak().unwrap().is_none());
}
}
#[cfg(test)]
mod kv_balance_tests {
use super::*;
#[test]
fn unbalanced_angle_is_an_error() {
let e = syn::parse2::<KvArgs>(template_quote::quote!(a = x < y, b = 1)).unwrap_err();
assert!(
e.to_string().contains("unbalanced `<` in the value of `a`"),
"{}",
e
);
assert!(syn::parse2::<KvArgs>(template_quote::quote!(a = 1 << 2, b = 3)).is_err());
let mut kv: KvArgs = syn::parse_quote!(a = (x < y), b = 1, f = fn() -> u8);
assert!(kv.tokens("a").is_some() && kv.tokens("b").is_some() && kv.tokens("f").is_some());
kv.finish().unwrap();
}
}
#[cfg(test)]
mod low_audit_tests {
use super::*;
#[test]
fn cont_equality_ignores_dummy_name_and_generics() {
let a: Cont = syn::parse_quote!(
#[derive(p::D)]
N1
);
let b: Cont = syn::parse_quote!(
#[derive(p::D)]
N2
);
assert_eq!(a, b);
let c = Cont::Bang(syn::parse_quote!(a::E<T>));
assert_eq!(c.to_token_stream().to_string().replace(' ', ""), "a::E!");
let d: Cont = syn::parse_quote!(#[derive(x::A, y::B)]);
assert_eq!(
d.path().to_token_stream().to_string().replace(' ', ""),
"x::A"
);
}
#[test]
fn rest_like_an_entry_without_meta_is_rest() {
let i: Input = syn::parse_quote! { [cb!] foo! => bar };
assert!(i.entries.is_empty());
assert_eq!(i.rest.to_string().replace(' ', ""), "foo!=>bar");
}
#[test]
fn gather_refuses_self_request() {
let i: Input = syn::parse_quote! { [me!] };
match i.gather([Cont::bang(syn::parse_quote!(me))]) {
Gather::Fetch(ts) => assert!(ts.to_string().contains("which is the callback itself")),
Gather::Ready(_) => panic!(),
}
}
}
#[cfg(test)]
mod kv_error_tests {
use super::*;
fn parse(ts: TokenStream) -> Result<KvArgs> {
syn::parse2(ts)
}
#[test]
fn missing_value_after_equals_is_an_error() {
let e = parse(template_quote::quote!(a =)).unwrap_err().to_string();
assert_eq!(e, "missing value for `a`");
let e = parse(template_quote::quote!(a = , b = 1))
.unwrap_err()
.to_string();
assert_eq!(e, "missing value for `a`");
let e = parse(template_quote::quote!(ok = 1, b =))
.unwrap_err()
.to_string();
assert_eq!(e, "missing value for `b`");
}
#[test]
fn get_on_a_flag_key_needs_a_value() {
let mut kv: KvArgs = syn::parse_quote!(f, a = 1);
let e = kv.get::<syn::Type>("f").unwrap_err().to_string();
assert_eq!(e, "`f` needs a value");
let mut kv: KvArgs = syn::parse_quote!(f);
assert_eq!(
kv.require::<syn::Type>("f").unwrap_err().to_string(),
"`f` needs a value"
);
}
#[test]
fn list_on_a_flag_key_needs_a_bracketed_value() {
let mut kv: KvArgs = syn::parse_quote!(f, a = 1);
let e = kv.list::<syn::Path>("f").unwrap_err().to_string();
assert_eq!(e, "`f` needs a `[..]` value");
}
#[test]
fn flag_on_a_key_with_a_value_takes_no_value() {
let mut kv: KvArgs = syn::parse_quote!(a = 1);
let e = kv.flag("a").unwrap_err().to_string();
assert_eq!(e, "`a` takes no value");
let mut kv: KvArgs = syn::parse_quote!(b = 1);
assert!(!kv.flag("a").unwrap());
assert!(kv.tokens("b").is_some());
}
#[test]
fn list_on_a_non_bracketed_value_is_an_error() {
let mut kv: KvArgs = syn::parse_quote!(a = 1);
let e = kv.list::<syn::LitInt>("a").unwrap_err().to_string();
assert!(e.contains("expected square brackets"), "{}", e);
let mut kv: KvArgs = syn::parse_quote!(a = (x, y));
let e = kv.list::<syn::Path>("a").unwrap_err().to_string();
assert!(e.contains("expected square brackets"), "{}", e);
let mut kv: KvArgs = syn::parse_quote!(a = [1, 2] extra);
assert!(kv.list::<syn::LitInt>("a").is_err());
}
#[test]
fn list_element_and_value_parse_errors_surface() {
let mut kv: KvArgs = syn::parse_quote!(a = [x, 1]);
assert!(kv.list::<syn::Path>("a").is_err());
let mut kv: KvArgs = syn::parse_quote!(a = 1);
assert!(kv.get::<syn::Path>("a").is_err());
}
#[test]
fn list_accepts_empty_and_trailing_comma() {
let mut kv: KvArgs = syn::parse_quote!(a = [], b = [x::Y, z,]);
assert!(kv.list::<Path>("a").unwrap().unwrap().is_empty());
assert_eq!(kv.list::<Path>("b").unwrap().unwrap().len(), 2);
kv.finish().unwrap();
}
#[test]
fn taken_keys_are_gone_and_finish_names_the_first_leftover() {
let mut kv: KvArgs = syn::parse_quote!(a = 1, b, c = 2);
assert!(kv.tokens("a").is_some());
assert!(kv.tokens("a").is_none());
assert_eq!(kv.finish().unwrap_err().to_string(), "unknown key `b`");
}
#[test]
fn duplicate_keys_and_keywords() {
let e = parse(template_quote::quote!(a, a)).unwrap_err().to_string();
assert_eq!(e, "duplicate key `a`");
let mut kv: KvArgs = syn::parse_quote!(type = u8, crate = a::b, fn);
assert!(kv.get::<syn::Type>("type").unwrap().is_some());
assert!(kv.get::<Path>("crate").unwrap().is_some());
assert!(kv.flag("fn").unwrap());
kv.finish().unwrap();
}
#[test]
fn values_run_to_the_next_top_level_comma() {
let mut kv = parse(template_quote::quote!(a = 1 b = 2)).unwrap();
assert_eq!(kv.tokens("a").unwrap().to_string().replace(' ', ""), "1b=2");
assert!(parse(template_quote::quote!(a b)).is_err());
}
#[test]
fn equality_comparison_is_a_flag_followed_by_junk() {
assert!(parse(template_quote::quote!(a == b)).is_err());
}
}
#[cfg(test)]
mod gap_tests {
use super::*;
use proc_macro2::{Punct, Spacing};
fn s(ts: impl ToTokens) -> String {
ts.to_token_stream()
.to_string()
.split_whitespace()
.collect()
}
fn mk_dollar() -> Punct {
Punct::new('$', Spacing::Alone)
}
#[test]
fn check_trait_asserts_the_dyn_trait_is_announced() {
let out = s(check_trait(
&syn::parse_quote!(::announce),
&syn::parse_quote!(my::Tr),
));
assert!(out.contains("dynmy::Tr"), "{}", out);
assert!(out.contains("::announce::Announce"), "{}", out);
assert!(out.contains("?::core::marker::Sized"), "{}", out);
assert!(out.starts_with("const_:()="), "{}", out);
let direct = s(check_type(
&syn::parse_quote!(::announce),
&syn::parse_quote!(dyn my::Tr),
));
assert_eq!(out, direct);
}
#[test]
fn check_type_uses_the_given_krate_path() {
let out = s(check_type(
&syn::parse_quote!(::renamed),
&syn::parse_quote!(Vec<u8>),
));
assert!(out.contains("::renamed::Announce"), "{}", out);
assert!(out.contains("__announce_chk::<Vec<u8>>"), "{}", out);
}
#[test]
fn meta_name_values_parse_expression_values() {
let m: Meta = syn::parse_quote!(lib::info(a = 1, b = "x", c = 1 + 2));
let nv = m.name_values().unwrap();
assert_eq!(nv.len(), 3);
assert_eq!(s(&nv[0].path), "a");
assert_eq!(s(&nv[1].value), "\"x\"");
assert_eq!(s(&nv[2].value), "1+2");
let m: Meta = syn::parse_quote!(lib::info);
assert!(m.name_values().unwrap().is_empty());
let m: Meta = syn::parse_quote!(lib::info(a = 1, on));
assert!(m.name_values().is_err());
let m: Meta = syn::parse_quote!(lib::info(a = 1, on));
assert!(m.kv().is_ok());
let m: Meta = syn::parse_quote!(lib::info(xs = [A, B]));
assert_eq!(m.name_values().unwrap().len(), 1);
let m: Meta = syn::parse_quote!(lib::info(ty = Vec<u8, A>));
assert!(m.name_values().is_err());
assert!(m.kv().is_ok());
}
#[test]
fn meta_args_default_to_empty() {
let m: Meta = syn::parse_quote!(lib::info);
assert!(m.args.is_none());
assert!(m.kv().unwrap().finish().is_ok());
assert_eq!(s(&m), "lib::info");
let m: Meta = syn::parse_quote!(lib::info(1, 2));
assert_eq!(s(&m), "lib::info(1,2)");
}
fn hash_of<T: std::hash::Hash>(t: &T) -> u64 {
use std::hash::Hasher;
let mut h = std::collections::hash_map::DefaultHasher::new();
t.hash(&mut h);
h.finish()
}
#[test]
fn cont_hash_agrees_with_eq() {
let a: Cont = syn::parse_quote!(
#[derive(p::D)]
N1
);
let b: Cont = syn::parse_quote!(
#[derive(p::D)]
N2
);
let c: Cont = syn::parse_quote!(#[derive(p::D)]);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
assert_eq!(hash_of(&a), hash_of(&c));
let bang = Cont::bang(syn::parse_quote!(p::D));
let bang_generic = Cont::Bang(syn::parse_quote!(p::D<T>));
assert_eq!(bang, bang_generic);
assert_eq!(hash_of(&bang), hash_of(&bang_generic));
let other: Cont = syn::parse_quote!(
#[derive(p::E)]
N1
);
assert_ne!(a, other);
assert_ne!(a, bang);
let set: std::collections::HashSet<Cont> = [a.clone(), b, c, other].into_iter().collect();
assert_eq!(set.len(), 2);
assert!(set.contains(&a));
}
#[test]
fn cont_path_of_attribute_and_derive() {
let c: Cont = syn::parse_quote!(
#[my::attr(x, y)]
N
);
assert_eq!(s(c.path()), "my::attr");
let d = Cont::derive(syn::parse_quote!(::p::D));
assert_eq!(s(d.path()), "::p::D");
assert_eq!(s(&d), "#[derive(::p::D)]");
let e: Cont = syn::parse_quote!(
#[derive()]
N
);
assert_eq!(s(e.path()), "derive");
}
#[test]
fn input_skips_a_leading_dollar_prefix() {
let dollar = mk_dollar();
let ts = template_quote::quote! {
@ __announce_dollar { #dollar }
(opt) [cb!, n!]
a::F! => [x::check(1)] { struct S; }
(rest)
};
let input: Input = syn::parse2(ts).unwrap();
assert_eq!(s(input.options.as_ref().unwrap()), "opt");
assert!(input.has_option("opt"));
assert_eq!(input.list.0.len(), 2);
assert_eq!(s(&input.list), "[cb!,n!]");
assert_eq!(input.entries.len(), 1);
assert_eq!(s(input.entries[0].key.path()), "a::F");
assert_eq!(s(&input.entries[0].meta[0]), "x::check(1)");
assert_eq!(s(&input.rest), "(rest)");
let out = s(input.forward().unwrap());
assert!(!out.contains("__announce_dollar"), "{}", out);
}
#[test]
fn input_dollar_prefix_without_options() {
let dollar = mk_dollar();
let ts = template_quote::quote! { @ __announce_dollar { #dollar } [cb!] };
let input: Input = syn::parse2(ts).unwrap();
assert!(input.options.is_none());
assert_eq!(input.list.0.len(), 1);
assert!(input.entries.is_empty());
assert!(input.rest.is_empty());
}
#[test]
fn input_skips_the_self_and_dollar_prefixes() {
let dollar = mk_dollar();
let ts = template_quote::quote! {
@ __announce_self { ::a::F }
@ __announce_dollar { #dollar }
[cb!, n!] REST
};
let input: Input = syn::parse2(ts).unwrap();
assert!(input.options.is_none());
assert_eq!(s(&input.list), "[cb!,n!]");
assert_eq!(s(&input.rest), "REST");
let ts = template_quote::quote! { @ __announce_self { F } (o) [cb!] };
let input: Input = syn::parse2(ts).unwrap();
assert!(input.has_option("o"));
assert_eq!(input.list.0.len(), 1);
let ts = template_quote::quote! { @ __announce_self { F } [cb!, n!] };
let out = s(syn::parse2::<Input>(ts).unwrap().forward().unwrap());
assert!(!out.contains("__announce_self"), "{}", out);
}
#[test]
fn input_rejects_other_prefixes() {
assert!(syn::parse2::<Input>(template_quote::quote! { @ other { x } [cb!] }).is_err());
assert!(syn::parse2::<Input>(template_quote::quote! { @ [cb!] }).is_err());
let dollar = mk_dollar();
let ts = template_quote::quote! { @ __announce_dollar { #dollar } };
assert!(syn::parse2::<Input>(ts).is_err());
}
#[test]
fn invoke_with_an_empty_list_is_a_compile_error() {
let out = s(ContList::default().invoke(template_quote::quote!(x y)));
assert_eq!(out, "compile_error!{\"announce:emptycontinuationlist\"}");
let opts = template_quote::quote!(expr);
let out = s(ContList::default().invoke_with_options(Some(&opts), TokenStream::new()));
assert!(out.contains("emptycontinuationlist"), "{}", out);
}
#[test]
fn invoke_in_expression_position_refuses_the_derive_route() {
let list: ContList = [
Cont::derive(syn::parse_quote!(p::D)),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
let opts = template_quote::quote!(expr);
let out = s(list.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert!(out.starts_with("compile_error!{"), "{}", out);
assert!(out.contains("cannotbeusedwith`(expr)`"), "{}", out);
assert!(!out.contains("__AnnouncePayload"), "{}", out);
let opts = template_quote::quote!(expr, type_leak);
let out = s(list.invoke_with_options(Some(&opts), TokenStream::new()));
assert!(out.contains("cannotbeusedwith`(expr)`"), "{}", out);
}
#[test]
fn only_a_leading_expr_selects_expression_position() {
let yes = |ts: TokenStream| is_expression_position(&ts);
assert!(yes(template_quote::quote!(expr)));
assert!(yes(template_quote::quote!(expr, type_leak)));
assert!(yes(template_quote::quote!(expr __announce_fetched(a))));
assert!(!yes(template_quote::quote!(type_leak, expr)));
assert!(!yes(template_quote::quote!(__announce_fetched(expr))));
assert!(!yes(template_quote::quote!((expr))));
assert!(!yes(template_quote::quote!(expression)));
assert!(!yes(TokenStream::new()));
let list: ContList = [
Cont::derive(syn::parse_quote!(p::D)),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
let opts = template_quote::quote!(type_leak, expr);
let out = s(list.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert!(out.contains("__AnnouncePayload"), "{}", out);
assert!(!out.contains("cannotbeusedwith"), "{}", out);
let opts = template_quote::quote!(__announce_fetched(expr));
let out = s(list.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert!(out.contains("__AnnouncePayload"), "{}", out);
let bang: ContList = [Cont::bang(syn::parse_quote!(p::cb))].into_iter().collect();
let opts = template_quote::quote!(type_leak, expr);
let out = s(bang.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert_eq!(
out,
"p::cb!{@announce_format\"1\"(type_leak,expr)[p::cb!]body}"
);
}
#[test]
fn expression_option_is_fine_for_a_bang_head_and_other_options_for_derives() {
let bang: ContList = [Cont::bang(syn::parse_quote!(p::cb))].into_iter().collect();
let opts = template_quote::quote!(expr);
let out = s(bang.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert_eq!(out, "p::cb!{@announce_format\"1\"(expr)[p::cb!]body}");
let derive: ContList = [Cont::derive(syn::parse_quote!(p::D))]
.into_iter()
.collect();
let opts = template_quote::quote!(type_leak);
let out = s(derive.invoke_with_options(Some(&opts), template_quote::quote!(body)));
assert!(out.contains("__AnnouncePayload"), "{}", out);
assert!(
out.contains("stringify!{@announce_format\"1\"(type_leak)[#[derive(p::D)]__AnnounceT_"),
"{}",
out
);
}
#[test]
fn name_dummies_keeps_existing_names_and_gives_distinct_new_ones() {
let mut list: ContList = [
Cont::derive(syn::parse_quote!(p::A)),
Cont::derive(syn::parse_quote!(p::B)),
syn::parse_quote!(
#[derive(p::C)]
Fixed
),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
list.name_dummies();
let names: Vec<String> = list
.0
.iter()
.filter_map(|c| match c {
Cont::Attr { name: Some(n), .. } => Some(n.to_string()),
_ => None,
})
.collect();
assert_eq!(names.len(), 3);
assert_eq!(names[2], "Fixed");
assert!(names[0].starts_with("__AnnounceT_") && names[1].starts_with("__AnnounceT_"));
assert_ne!(names[0], names[1]);
let before = s(&list);
list.name_dummies();
assert_eq!(s(&list), before);
}
#[test]
fn redirect_ignores_ordinary_items() {
let target: Path = syn::parse_quote!(::x::Y);
for item in [
template_quote::quote! { struct S; },
template_quote::quote! { enum E { A, B } },
template_quote::quote! { enum E { __AnnouncePayload } },
template_quote::quote! { enum E { __AnnouncePayload = 1 } },
template_quote::quote! { enum E { __AnnouncePayload = (1, 0).1 } },
template_quote::quote! { enum E { __AnnouncePayload = (other!{ x }, 0).1 } },
template_quote::quote! { enum E { __AnnouncePayload = (stringify!{ x }, 0).1, B } },
template_quote::quote! { enum E { Other = (stringify!{ x }, 0).1 } },
template_quote::quote! { fn f() {} },
] {
assert!(payload(item.clone()).is_none(), "{}", item);
assert!(redirect(item.clone(), &target).is_none(), "{}", item);
}
}
#[test]
fn redirect_wraps_the_payload_with_the_dollar_prefix() {
let list: ContList = [
Cont::derive(syn::parse_quote!(p::D)),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
let dummy = list.invoke(template_quote::quote!(body));
let out = redirect(dummy.clone(), &syn::parse_quote!(::x::Y)).unwrap();
let out_s = s(&out);
assert!(
out_s.starts_with(
"::x::Y!{@announce_format\"1\"@__announce_self{::x::Y}@__announce_dollar{$}[#[derive(p::D)]__AnnounceT_"
),
"{}",
out_s
);
assert!(out_s.ends_with("cb!]body}"), "{}", out_s);
assert_eq!(out_s.matches("@announce_format").count(), 1, "{}", out_s);
let has_dollar = out.into_iter().any(|tt| match tt {
proc_macro2::TokenTree::Group(g) => g
.stream()
.into_iter()
.any(|t| matches!(t, proc_macro2::TokenTree::Group(g2) if g2.stream().into_iter().any(|x| matches!(x, proc_macro2::TokenTree::Punct(p) if p.as_char() == '$')))),
_ => false,
});
assert!(has_dollar);
let input: Input = syn::parse2(
match redirect(dummy, &syn::parse_quote!(::x::Y))
.unwrap()
.into_iter()
.last()
{
Some(proc_macro2::TokenTree::Group(g)) => g.stream(),
other => panic!("{:?}", other),
},
)
.unwrap();
assert_eq!(input.list.0.len(), 2);
assert_eq!(s(input.rest), "body");
}
}
#[cfg(test)]
mod format_tests {
use super::*;
use proc_macro2::{Punct, Spacing};
fn s(ts: impl ToTokens) -> String {
ts.to_token_stream()
.to_string()
.split_whitespace()
.collect()
}
const PREFIX: &str = "@announce_format\"1\"";
#[test]
fn the_version_is_written_once_and_the_prefix_is_made_of_it() {
assert_eq!(ANNOUNCE_FORMAT, "1");
assert_eq!(s(format_prefix()), PREFIX);
assert_eq!(
s(format_prefix()),
format!("@announce_format{:?}", ANNOUNCE_FORMAT)
);
}
#[test]
fn input_reads_a_call_with_the_prefix_and_one_without() {
let with: Input = syn::parse_quote! { @announce_format "1" (expr) [cb!] REST };
assert_eq!(with.format.as_deref(), Some(ANNOUNCE_FORMAT));
assert!(with.has_option("expr"));
assert_eq!(s(&with.rest), "REST");
let without: Input = syn::parse_quote! { (expr) [cb!] REST };
assert_eq!(without.format, None);
assert_eq!(s(&without.rest), "REST");
assert_eq!(s(&with.list), s(&without.list));
let all: Input = syn::parse_quote! {
@announce_format "1" @__announce_self { a::b } @__announce_dollar { $ } (x) [cb!] REST
};
assert_eq!(all.format.as_deref(), Some("1"));
assert!(all.has_option("x"));
assert_eq!(s(&all.rest), "REST");
let e: Input = syn::parse_quote! {
@announce_format "1" [cb!] a::F! => [] { struct F; } b::G! => [] { struct G; }
};
assert_eq!(e.entries.len(), 2);
}
#[test]
fn the_prefix_after_the_other_prefixes_is_not_a_prefix() {
let late = syn::parse2::<Input>(template_quote::quote! {
@__announce_self { a::b } @announce_format "1" [cb!]
});
assert!(late.is_err());
let twice = syn::parse2::<Input>(template_quote::quote! {
@announce_format "1" @announce_format "1" [cb!]
});
assert!(twice.is_err());
}
#[test]
fn another_version_is_one_error_that_names_both() {
let e = syn::parse2::<Input>(template_quote::quote! { @announce_format "2" [cb!] })
.unwrap_err()
.to_string();
assert_eq!(
e.matches("announce: incompatible wire format").count(),
1,
"{}",
e
);
assert!(
e.contains("found `@announce_format \"2\"`")
&& e.contains("expected `@announce_format \"1\"`"),
"{}",
e
);
let query = template_quote::quote! { @announce_format "7" @__announce_attrs [ f! ] { } };
let call = template_quote::quote! { @announce_format "7" (), struct S; };
let errors = vec![
syn::parse2::<CheckCall>(query.clone()).err().unwrap(),
syn::parse2::<CheckCall>(call.clone()).err().unwrap(),
syn::parse2::<Query>(query).err().unwrap(),
syn::parse2::<CheckInput>(call).err().unwrap(),
];
for e in errors {
let e = e.to_string();
assert!(
e.starts_with("announce: incompatible wire format")
&& e.contains("found `@announce_format \"7\"`")
&& e.contains("expected `@announce_format \"1\"`"),
"{}",
e
);
}
}
#[test]
fn a_prefix_without_a_string_is_an_error() {
for bad in [
template_quote::quote! { @announce_format [cb!] },
template_quote::quote! { @announce_format 1 [cb!] },
template_quote::quote! { @announce_format },
] {
let e = syn::parse2::<Input>(bad).unwrap_err().to_string();
assert!(e.contains("followed by a string literal"), "{}", e);
}
let e = syn::parse2::<Input>(template_quote::quote! { @other [cb!] })
.unwrap_err()
.to_string();
assert!(!e.contains("wire format"), "{}", e);
}
#[test]
fn checks_take_a_call_with_the_prefix_and_one_without() {
for tokens in [
template_quote::quote! { @announce_format "1" (a, b), struct S; },
template_quote::quote! { (a, b), struct S; },
] {
let i: CheckInput = syn::parse2(tokens.clone()).unwrap();
assert_eq!(s(&i.args), "a,b");
match syn::parse2::<CheckCall>(tokens).unwrap() {
CheckCall::Run(i) => assert_eq!(s(&i.args), "a,b"),
CheckCall::Query(_) => panic!("the check itself"),
}
}
for tokens in [
template_quote::quote! { @announce_format "1" @__announce_attrs [ me!, f! ] { st } },
template_quote::quote! { @__announce_attrs [ me!, f! ] { st } },
] {
let q: Query = syn::parse2(tokens.clone()).unwrap();
assert_eq!(q.list.0.len(), 2);
assert!(matches!(
syn::parse2::<CheckCall>(tokens).unwrap(),
CheckCall::Query(_)
));
assert!(
s(q.answer(&OwnedAttrs::new())).starts_with(&format!("f!{{{}", PREFIX)),
"{}",
s(q.answer(&OwnedAttrs::new()))
);
}
for tokens in [
template_quote::quote! { @announce_format "1" (), struct S; },
template_quote::quote! { (), struct S; },
] {
let mut ran = false;
let out = check(tokens, &[], |_| {
ran = true;
Ok(TokenStream::new())
});
assert!(ran && out.is_empty());
}
let out = s(check(
template_quote::quote! { @announce_format "3" @__announce_attrs [ me!, f! ] { st } },
&[],
|_| unreachable!(),
));
assert!(
out.starts_with("::core::compile_error!") && out.contains("incompatiblewireformat"),
"{}",
out
);
}
#[test]
fn every_call_that_a_helper_makes_starts_with_the_prefix() {
let starts = |ts: TokenStream| {
let out = s(&ts);
assert!(
out.contains(&format!("!{{{}", PREFIX))
|| out.contains(&format!("stringify!{{{}", PREFIX)),
"{}",
out
);
let at = out.find(PREFIX).unwrap();
assert!(
out[..at].ends_with("!{") || out[..at].ends_with("stringify!{"),
"{}",
out
);
assert_eq!(out.matches(PREFIX).count(), 1, "{}", out);
};
let input: Input = syn::parse_quote! { [me!, next!] a::F! => [] { struct F; } (rest) };
starts(input.forward().unwrap());
starts(input.fetch([Cont::bang(syn::parse_quote!(g::G))]));
match input.gather([Cont::bang(syn::parse_quote!(g::G))]) {
Gather::Fetch(ts) => starts(ts),
Gather::Ready(_) => panic!("g::G is not there"),
}
starts(input.forward_transformed(input.entries[0].clone()).unwrap());
let bang: ContList = [Cont::bang(syn::parse_quote!(p::cb))].into_iter().collect();
starts(bang.invoke(TokenStream::new()));
starts(bang.invoke_with_options(Some(&template_quote::quote!(expr)), TokenStream::new()));
let derive: ContList = [
Cont::derive(syn::parse_quote!(p::D)),
Cont::bang(syn::parse_quote!(cb)),
]
.into_iter()
.collect();
let dummy = derive.invoke(template_quote::quote!(body));
starts(dummy.clone());
assert!(s(payload(dummy.clone()).unwrap()).starts_with(&format!("{}[", PREFIX)));
let out = redirect(dummy, &syn::parse_quote!(::x::Y)).unwrap();
starts(out.clone());
assert!(s(out).starts_with(&format!(
"::x::Y!{{{}@__announce_self{{::x::Y}}@__announce_dollar{{$}}",
PREFIX
)));
let q: Query = syn::parse_quote! { @__announce_attrs [ me!, f! ] { st } };
starts(q.answer(&OwnedAttrs::new()));
starts(check(
template_quote::quote! { @__announce_attrs [ me!, f! ] { st } },
&["a"],
|_| unreachable!(),
));
}
#[test]
fn the_callback_and_transformer_functions_take_and_write_the_prefix() {
let call = |prefix: TokenStream| {
template_quote::quote! { #prefix [me!, next!] a::S! => [] { struct S; } NAME }
};
let with = call(format_prefix());
let without = call(TokenStream::new());
for input in [with.clone(), without.clone()] {
let out = callback(input, |i| {
let name: syn::Ident = i.rest_as()?;
Ok(template_quote::quote! { const #name: usize = 1; })
});
assert_eq!(s(out), "constNAME:usize=1;");
}
for input in [with.clone(), without.clone()] {
let out = s(callback(input, |i| Ok(i.forward().unwrap())));
assert!(out.starts_with(&format!("next!{{{}", PREFIX)), "{}", out);
}
for input in [with.clone(), without] {
let out = s(transformer(input, |mut entry| {
entry.meta.push(syn::parse_quote!(seen));
Ok(entry)
}));
assert!(
out.starts_with(&format!("next!{{{}[next!]a::S!=>[seen]", PREFIX)),
"{}",
out
);
}
let other = template_quote::quote! { @announce_format "5" [me!, next!] a::S! => [] { struct S; } NAME };
for out in [
s(callback(other.clone(), |_| unreachable!())),
s(transformer(other, |_| unreachable!())),
] {
assert!(
out.starts_with("::core::compile_error!")
&& out.contains(
"found`@announce_format\\\"5\\\"`,expected`@announce_format\\\"1\\\"`"
),
"{}",
out
);
}
}
#[test]
fn a_call_without_the_prefix_is_never_checked_for_a_version() {
let no_error = |what: &str, out: &str| {
assert!(
!out.contains("wire format") && !out.contains("compile_error"),
"{}: {}",
what,
out
);
};
let dollar = Punct::new('$', Spacing::Alone);
let calls = [
template_quote::quote! { [cb!] },
template_quote::quote! { (expr) [cb!] a::F! => [] { struct F; } REST },
template_quote::quote! { @__announce_dollar { #dollar } [cb!] REST },
template_quote::quote! {
@__announce_self { ::a::F } @__announce_dollar { #dollar } (o) [cb!] REST
},
];
for call in calls {
let input: Input = syn::parse2(call.clone()).unwrap();
assert_eq!(input.format, None, "{}", s(&call));
let fetched = s(input.fetch([Cont::bang(syn::parse_quote!(g::G))]));
assert!(fetched.contains(&format!("!{{{}", PREFIX)), "{}", fetched);
let out = s(callback(call.clone(), |_| Ok(TokenStream::new())));
assert_eq!(out, "", "{}", s(&call));
}
let query = template_quote::quote! { @__announce_attrs [ me!, f! ] { st } };
let own = template_quote::quote! { (a), struct S; };
let q: Query = syn::parse2(query.clone()).unwrap();
no_error(
"Query::answer",
&s(q.answer(&OwnedAttrs::from_strs(&["x"]))),
);
assert!(matches!(
syn::parse2::<CheckCall>(query.clone()).unwrap(),
CheckCall::Query(_)
));
assert!(matches!(
syn::parse2::<CheckCall>(own.clone()).unwrap(),
CheckCall::Run(_)
));
syn::parse2::<CheckInput>(own.clone()).unwrap();
let answered = s(check(query, &["x"], |_| unreachable!()));
no_error("check (query)", &answered);
assert!(
answered.starts_with(&format!("f!{{{}", PREFIX)),
"{}",
answered
);
let mut ran = false;
let out = check(own, &["x"], |_| {
ran = true;
Ok(TokenStream::new())
});
assert!(ran && out.is_empty());
let call = template_quote::quote! { [me!, next!] a::S! => [] { struct S; } NAME };
no_error(
"callback",
&s(callback(call.clone(), |i| Ok(i.forward().unwrap()))),
);
no_error("transformer", &s(transformer(call, Ok)));
let dummy = template_quote::quote! {
#[derive(p::D)]
enum N { __AnnouncePayload = (stringify!{ [cb!] rest }, 0).1 }
};
let out = redirect(dummy, &syn::parse_quote!(::x::Y)).unwrap();
no_error("redirect", &s(&out));
assert!(s(out).starts_with(&format!("::x::Y!{{{}", PREFIX)));
}
#[test]
fn check_answers_a_query_with_the_prefix_and_without_it() {
let answer = |tokens: TokenStream| s(check(tokens, &["my_lib"], |_| unreachable!()));
let with = answer(template_quote::quote! {
@announce_format "1" @__announce_attrs [ me!, f! ] { st }
});
let without = answer(template_quote::quote! { @__announce_attrs [ me!, f! ] { st } });
assert_eq!(with, without);
assert_eq!(
with,
format!("f!{{{}@__announce_attrs[f!]{{stattrs(my_lib)}}}}", PREFIX)
);
}
#[test]
fn redirect_moves_the_prefix_of_the_payload_to_the_front_and_keeps_its_version() {
let dollar = Punct::new('$', Spacing::Alone);
let dummy = |payload: TokenStream| {
template_quote::quote! {
#[derive(p::D)]
enum N { __AnnouncePayload = (stringify!{ #payload }, 0).1 }
}
};
let target: Path = syn::parse_quote!(::x::Y);
let out = redirect(
dummy(template_quote::quote!(@announce_format "1" [cb!] rest)),
&target,
)
.unwrap();
assert_eq!(
s(out),
format!(
"::x::Y!{{{}@__announce_self{{::x::Y}}@__announce_dollar{{{}}}[cb!]rest}}",
PREFIX,
dollar.as_char()
)
);
let out = redirect(
dummy(template_quote::quote!(@announce_format "9" [cb!] rest)),
&target,
)
.unwrap();
assert!(s(out).starts_with("::x::Y!{@announce_format\"9\"@__announce_self"));
let out = redirect(dummy(template_quote::quote!([cb!] rest)), &target).unwrap();
assert!(s(out).starts_with(&format!("::x::Y!{{{}@__announce_self", PREFIX)));
let out = redirect(dummy(template_quote::quote!(@announce_format)), &target).unwrap();
assert!(s(out).ends_with("@__announce_dollar{$}@announce_format}"));
}
}