use crate::helper::OwnedAttrs;
use crate::options::{MergeConflict, Options};
use crate::scan::{self, Outer, OuterAttr};
use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream, TokenTree};
use template_quote::{quote, ToTokens};
pub(crate) const RUNNER_FORMAT: &str = "1";
pub(crate) const RUNNER: &str = "__announce_runner";
const VERSION_KEY: &str = "__announce_format_version";
const OWNED_KEY: &str = "__announce_owned";
const SENTINEL_KEY: &str = "__announce_sentinel";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sentinel {
Impl,
Macro,
}
impl Sentinel {
fn word(self) -> &'static str {
match self {
Sentinel::Impl => "impl",
Sentinel::Macro => "macro",
}
}
fn merge(target: Option<Sentinel>, folder: Option<Sentinel>) -> Option<Sentinel> {
match (target, folder) {
(Some(Sentinel::Impl), _) | (_, Some(Sentinel::Impl)) => Some(Sentinel::Impl),
(a, b) => a.or(b),
}
}
pub(crate) fn capture_word(self) -> &'static str {
match self {
Sentinel::Impl => "sentinel_impl",
Sentinel::Macro => "sentinel_macro",
}
}
pub(crate) fn from_capture_word(word: &str) -> Option<Sentinel> {
match word {
"sentinel_impl" => Some(Sentinel::Impl),
"sentinel_macro" => Some(Sentinel::Macro),
_ => None,
}
}
fn from_word(word: &str) -> Option<Sentinel> {
match word {
"impl" => Some(Sentinel::Impl),
"macro" => Some(Sentinel::Macro),
_ => None,
}
}
pub(crate) fn of(head: &scan::Head, opts: &Options) -> Sentinel {
let adt = matches!(
head.kind,
scan::Kind::Struct | scan::Kind::Enum | scan::Kind::Union
);
if adt && !head.generic && opts.mock.is_none() && opts.emit_item {
Sentinel::Impl
} else {
Sentinel::Macro
}
}
fn tokens(self, head: &scan::Head, opts: &Options, cfgs: &TokenStream) -> TokenStream {
match self {
Sentinel::Impl => {
let krate = opts.krate();
let ident = &head.ident;
let bound = respan(quote! { #krate::Announce }, ident.span());
quote! {
#cfgs
#[allow(deprecated)]
const _: () = {
trait __AnnounceSentinel {}
impl __AnnounceSentinel for #ident where #ident: #bound {}
};
}
}
Sentinel::Macro => {
let name = opts
.macro_name
.clone()
.unwrap_or_else(|| head.ident.clone());
let format = crate::helper::format_prefix();
quote! { #cfgs #name! { #format @__announce_exists } }
}
}
}
}
fn respan(ts: TokenStream, span: Span) -> TokenStream {
ts.into_iter()
.map(|mut tt| {
match &mut tt {
TokenTree::Group(g) => {
let mut ng = Group::new(g.delimiter(), respan(g.stream(), span));
ng.set_span(span);
*g = ng;
}
TokenTree::Ident(i) if i == "$crate" => {}
t => t.set_span(span),
}
tt
})
.collect()
}
pub(crate) fn error_with_item(message: String, item: &TokenStream) -> TokenStream {
let mut out = syn::Error::new(Span::call_site(), message).to_compile_error();
out.extend(item.clone());
out
}
fn conflict(c: &MergeConflict, item: &TokenStream) -> TokenStream {
error_with_item(c.message(), item)
}
fn entries(ts: TokenStream) -> Vec<Vec<TokenTree>> {
let mut out = vec![Vec::new()];
for t in ts {
match &t {
TokenTree::Punct(p) if p.as_char() == ',' => out.push(Vec::new()),
_ => out.last_mut().unwrap().push(t),
}
}
out.retain(|e| !e.is_empty());
out
}
fn version_entry(entry: &[TokenTree]) -> Option<String> {
string_entry(entry, VERSION_KEY)
}
fn string_entry(entry: &[TokenTree], key: &str) -> Option<String> {
match entry {
[TokenTree::Ident(k), TokenTree::Punct(p), TokenTree::Literal(l)]
if k == key && p.as_char() == '=' =>
{
let text = l.to_string();
Some(
match text.strip_prefix('"').and_then(|t| t.strip_suffix('"')) {
Some(inner) => inner.to_string(),
None => text,
},
)
}
_ => None,
}
}
fn versions_in(args: &TokenStream) -> Vec<String> {
entries(args.clone())
.iter()
.filter_map(|e| version_entry(e))
.collect()
}
#[derive(Clone)]
pub(crate) struct RunnerArgs {
pub owned: OwnedAttrs,
pub opts: Options,
pub sentinel: Option<Sentinel>,
}
impl ToTokens for RunnerArgs {
fn to_tokens(&self, tokens: &mut TokenStream) {
let key = Ident::new(VERSION_KEY, Span::call_site());
let mut parts = vec![quote! { #key = #RUNNER_FORMAT }];
if let Some(sentinel) = self.sentinel {
let key = Ident::new(SENTINEL_KEY, Span::call_site());
let word = sentinel.word();
parts.push(quote! { #key = #word });
}
if !self.owned.is_empty() {
let key = Ident::new(OWNED_KEY, Span::call_site());
let names = self.owned.names();
parts.push(quote! { #key(#(#names),*) });
}
let opts = self.opts.to_token_stream();
if !opts.is_empty() {
parts.push(opts);
}
tokens.extend(quote! { #(#parts),* });
}
}
impl RunnerArgs {
pub(crate) fn read(args: TokenStream) -> syn::Result<RunnerArgs> {
let mut owned = OwnedAttrs::new();
let mut sentinel = None;
let mut rest: Vec<TokenStream> = Vec::new();
for e in entries(args) {
if version_entry(&e).is_some() {
continue;
}
if let Some(word) = string_entry(&e, SENTINEL_KEY) {
match Sentinel::from_word(&word) {
Some(s) => sentinel = Sentinel::merge(sentinel, Some(s)),
None => {
return Err(syn::Error::new(
Span::call_site(),
format!(
"announce: unknown sentinel `{}` in the arguments of `#[{}]`",
word, RUNNER
),
))
}
}
continue;
}
match e.as_slice() {
[TokenTree::Ident(k), TokenTree::Group(g)]
if k == OWNED_KEY && g.delimiter() == Delimiter::Parenthesis =>
{
owned.extend(OwnedAttrs::parse_names(g.stream())?.names().iter().cloned());
}
_ => rest.push(e.into_iter().collect()),
}
}
let opts = syn::parse2(quote! { #(#rest),* })?;
Ok(RunnerArgs {
owned,
opts,
sentinel,
})
}
fn merge(&mut self, folder: &RunnerArgs, name: &Ident) -> Result<(), MergeConflict> {
self.opts = self.opts.merge(&folder.opts, name)?;
self.owned.extend(folder.owned.names().iter().cloned());
self.sentinel = Sentinel::merge(self.sentinel, folder.sentinel);
Ok(())
}
fn check_sentinel(&self) -> Result<(), String> {
if self.sentinel == Some(Sentinel::Impl)
&& (self.opts.mock.is_some() || !self.opts.emit_item)
{
let option = if self.opts.mock.is_some() {
"mock"
} else {
"emit_item = false"
};
return Err(format!(
"announce: `{}` must be on the first announcement of an item (the one that expands \
first, the highest in the attributes of the item), not on a later one: the first \
announcement checks that the item has its `Announce` impl, which `mock` and \
`emit_item = false` remove",
option
));
}
Ok(())
}
}
fn is_runner(a: &OuterAttr) -> bool {
a.segments.last().map_or(false, |s| s == RUNNER)
}
fn runners_of(outer: &Outer) -> Vec<usize> {
(0..outer.attrs.len())
.filter(|&k| is_runner(&outer.attrs[k]))
.collect()
}
struct Seen {
version: String,
path: Option<String>,
}
fn crate_path(a: &OuterAttr) -> String {
let mut segments: Vec<String> = a.segments.iter().map(|s| s.to_string()).collect();
segments.pop();
if segments.last().map_or(false, |s| s == "__private") {
segments.pop();
}
let path = segments.join("::");
if a.leading_colon {
format!("::{}", path)
} else {
path
}
}
fn seen_on(outer: &Outer, at: &[usize]) -> Vec<Seen> {
let mut seen = Vec::new();
for &k in at {
let a = &outer.attrs[k];
let path = Some(crate_path(a));
let versions = a.args.as_ref().map(|g| versions_in(&g.stream()));
match versions {
Some(v) if !v.is_empty() => {
seen.extend(v.into_iter().map(|version| Seen {
version,
path: path.clone(),
}));
}
_ => seen.push(Seen {
version: "unknown".to_string(),
path,
}),
}
}
seen
}
fn version_error(seen: &[Seen]) -> Option<String> {
if seen.iter().all(|s| s.version == RUNNER_FORMAT) {
return None;
}
let mut paths: Vec<&String> = seen.iter().filter_map(|s| s.path.as_ref()).collect();
paths.dedup();
let mut distinct: Vec<&String> = Vec::new();
for p in paths {
if !distinct.contains(&p) {
distinct.push(p);
}
}
let paths_differ = distinct.len() > 1;
let mut versions: Vec<&String> = Vec::new();
for s in seen {
if !versions.contains(&&s.version) {
versions.push(&s.version);
}
}
let listed: Vec<String> = versions
.iter()
.map(|v| {
let mut at: Vec<String> = Vec::new();
for s in seen.iter().filter(|s| &&s.version == v) {
if let Some(p) = &s.path {
let p = format!("`{}`", p);
if !at.contains(&p) {
at.push(p);
}
}
}
if paths_differ && !at.is_empty() {
format!("`{}` (in {})", v, at.join(", "))
} else {
format!("`{}`", v)
}
})
.collect();
Some(format!(
"announce: this item is announced by several versions of the `announce` crate (or by \
an incompatible one): the format versions found on its `#[{}]` attributes are {}; \
this `announce` expects `{}`. Use one version of `announce` in every crate that \
announces the item (`cargo tree -i announce` shows the versions)",
RUNNER,
listed.join(", "),
RUNNER_FORMAT
))
}
fn merged_runners(
outer: &Outer,
at: &[usize],
name: &Ident,
) -> Result<RunnerArgs, Result<MergeConflict, syn::Error>> {
let read = |k: usize| {
let args = outer.attrs[k]
.args
.as_ref()
.map(|g| g.stream())
.unwrap_or_default();
RunnerArgs::read(args).map_err(Err)
};
let (last, earlier) = at.split_last().expect("a runner");
let mut target = read(*last)?;
for &k in earlier {
target.merge(&read(k)?, name).map_err(Ok)?;
}
Ok(target)
}
fn owns(a: &OuterAttr, owned: &OwnedAttrs) -> bool {
fn in_cfg_attr(args: TokenStream, owned: &OwnedAttrs) -> bool {
entries(args).iter().skip(1).any(|e| match e.as_slice() {
[TokenTree::Ident(id), TokenTree::Group(g)]
if id == "cfg_attr" && g.delimiter() == Delimiter::Parenthesis =>
{
in_cfg_attr(g.stream(), owned)
}
[TokenTree::Ident(id), ..] => owned.contains(id.to_string().trim_start_matches("r#")),
_ => false,
})
}
a.is_owned_by(owned)
|| (a.is_bare("cfg_attr")
&& a.args
.as_ref()
.map_or(false, |g| in_cfg_attr(g.stream(), owned)))
}
const INERT: &[&str] = &[
"doc",
"repr",
"allow",
"warn",
"deny",
"forbid",
"expect",
"must_use",
"deprecated",
"non_exhaustive",
"inline",
"cold",
"track_caller",
"no_mangle",
"automatically_derived",
"macro_use",
];
pub(crate) fn can_expand_after(item: &TokenStream, owned: &OwnedAttrs) -> bool {
if !scan::starts_with_attr(item) {
return false;
}
scan::outer(item).attrs.iter().any(|a| {
let inert = !a.leading_colon
&& a.segments.len() == 1
&& INERT.contains(&a.segments[0].to_string().as_str());
!inert && !owns(a, owned)
})
}
enum Run {
None,
New(TokenStream),
Replace(usize, TokenStream),
}
fn assemble(
outer: &Outer,
drop: &[usize],
moved: &[usize],
owned: &OwnedAttrs,
run: Run,
) -> TokenStream {
let tokens = |k: usize| {
let a = &outer.attrs[k];
[outer.tts[a.at].clone(), outer.tts[a.at + 1].clone()]
};
let gone = |k: usize| drop.contains(&k) || moved.contains(&k);
let after = match &run {
Run::Replace(at, _) => {
let mut last = *at;
while last + 1 < outer.attrs.len()
&& !gone(last + 1)
&& owns(&outer.attrs[last + 1], owned)
{
last += 1;
}
Some(last)
}
_ => None,
};
let mut out = TokenStream::new();
let emit_moved = |out: &mut TokenStream| {
for &k in moved {
out.extend(tokens(k));
}
};
for (k, a) in outer.attrs.iter().enumerate() {
if gone(k) {
continue;
}
match &run {
Run::Replace(at, args) if *at == k => {
let span = match &outer.tts[a.at + 1] {
TokenTree::Group(g) => g.span(),
_ => Span::call_site(),
};
let mut group = Group::new(Delimiter::Bracket, a.with_args(args.clone()));
group.set_span(span);
out.extend([outer.tts[a.at].clone(), TokenTree::Group(group)]);
}
_ => out.extend(tokens(k)),
}
if after == Some(k) {
emit_moved(&mut out);
}
}
if let Run::New(attr) = &run {
out.extend(attr.clone());
emit_moved(&mut out);
}
out.extend(outer.tts[outer.attrs_end()..].iter().cloned());
out
}
fn sentinel_cfgs(outer: &Outer) -> Option<TokenStream> {
fn mentions_cfg(ts: TokenStream) -> bool {
ts.into_iter().any(|t| match t {
TokenTree::Ident(i) => i == "cfg",
TokenTree::Group(g) => mentions_cfg(g.stream()),
_ => false,
})
}
let mut cfgs = TokenStream::new();
for a in &outer.attrs {
if a.is_bare("cfg") {
cfgs.extend([outer.tts[a.at].clone(), outer.tts[a.at + 1].clone()]);
} else if a.is_bare("cfg_attr")
&& a.args.as_ref().map_or(true, |g| mentions_cfg(g.stream()))
{
return None;
}
}
Some(cfgs)
}
pub(crate) fn announce(
opts: &Options,
head: &scan::Head,
item: TokenStream,
owned: &OwnedAttrs,
) -> TokenStream {
let name = &head.ident;
let outer = scan::outer(&item);
let runners = runners_of(&outer);
let above = runners.last().copied().unwrap_or(outer.attrs.len());
let moved: Vec<usize> = (0..above)
.filter(|&k| !runners.contains(&k) && owns(&outer.attrs[k], owned))
.collect();
if runners.is_empty() {
let cfgs = sentinel_cfgs(&outer);
let sentinel = cfgs.as_ref().map(|_| Sentinel::of(head, opts));
let args = RunnerArgs {
owned: owned.clone(),
opts: opts.clone(),
sentinel,
};
let krate = opts.krate();
let runner = Ident::new(RUNNER, Span::call_site());
let attr = quote! { #[#krate::__private::#runner(#args)] };
let mut out = assemble(&outer, &[], &moved, owned, Run::New(attr));
if let (Some(sentinel), Some(cfgs)) = (sentinel, cfgs) {
out.extend(sentinel.tokens(head, opts, &cfgs));
}
return out;
}
if let Some(message) = version_error(&seen_on(&outer, &runners)) {
return error_with_item(
message,
&assemble(&outer, &runners, &[], &OwnedAttrs::new(), Run::None),
);
}
let mut target = match merged_runners(&outer, &runners, name) {
Ok(t) => t,
Err(Ok(c)) => return conflict(&c, &item),
Err(Err(e)) => return error_with_item(e.to_string(), &item),
};
let mine = RunnerArgs {
owned: owned.clone(),
opts: opts.clone(),
sentinel: None,
};
if let Err(c) = target.merge(&mine, name) {
return conflict(&c, &item);
}
if let Err(message) = target.check_sentinel() {
return error_with_item(message, &item);
}
let last = *runners.last().unwrap();
let earlier = &runners[..runners.len() - 1];
assemble(
&outer,
earlier,
&moved,
&target.owned,
Run::Replace(last, target.to_token_stream()),
)
}
const LATE: &str = "announce: this item has an `#[announce]` that expands after its \
announcements were run, so it makes a second trampoline: a macro of the item has put it \
below the other attributes of its output (for example by appending it to the attributes \
of the item). It is removed. Put it first among the attributes of the output \
(`quote! { #[announce(..)] #item }`)";
fn is_late(a: &OuterAttr) -> bool {
match a.segments.last() {
Some(l) if l == "announce" => a.leading_colon || a.segments.len() > 1,
_ => false,
}
}
pub(crate) fn run(args: TokenStream, item: TokenStream) -> TokenStream {
let outer = scan::outer(&item);
let others = runners_of(&outer);
let mut seen: Vec<Seen> = versions_in(&args)
.into_iter()
.map(|version| Seen {
version,
path: None,
})
.collect();
if seen.is_empty() {
seen.push(Seen {
version: "unknown".to_string(),
path: None,
});
}
seen.extend(seen_on(&outer, &others));
if let Some(message) = version_error(&seen) {
let mut out = error_with_item(
message,
&assemble(&outer, &others, &[], &OwnedAttrs::new(), Run::None),
);
if let (Ok(mine), Ok(head)) = (RunnerArgs::read(args), scan::head(&item)) {
if let Some(sentinel) = mine.sentinel {
out.extend(crate::generate::sentinel_stub(
sentinel,
&head.ident,
&mine.opts,
crate::random_u64(),
));
}
}
return out;
}
let head = match scan::head(&item) {
Ok(h) => h,
Err(e) => return error_with_item(e.to_string(), &item),
};
let mine = match RunnerArgs::read(args) {
Ok(a) => a,
Err(e) => return error_with_item(e.to_string(), &item),
};
if !others.is_empty() {
let mut target = match merged_runners(&outer, &others, &head.ident) {
Ok(t) => t,
Err(Ok(c)) => return conflict(&c, &item),
Err(Err(e)) => return error_with_item(e.to_string(), &item),
};
if let Err(c) = target.merge(&mine, &head.ident) {
return conflict(&c, &item);
}
if let Err(message) = target.check_sentinel() {
return error_with_item(message, &item);
}
let last = *others.last().unwrap();
return assemble(
&outer,
&others[..others.len() - 1],
&[],
&target.owned,
Run::Replace(last, target.to_token_stream()),
);
}
let mut errors = TokenStream::new();
let late: Vec<usize> = (0..outer.attrs.len())
.filter(|&k| is_late(&outer.attrs[k]))
.collect();
let item = if late.is_empty() {
item
} else {
errors = syn::Error::new(Span::call_site(), LATE).to_compile_error();
assemble(&outer, &late, &[], &OwnedAttrs::new(), Run::None)
};
let random = crate::random_u64();
let attr = mine.opts.to_token_stream();
errors.extend(crate::capture_output(
&mine.opts,
&attr,
item,
random,
&mine.owned,
mine.sentinel,
));
errors
}
#[cfg(test)]
mod tests {
use super::*;
fn ts(s: &str) -> TokenStream {
s.parse().unwrap()
}
fn norm(t: &TokenStream) -> String {
t.to_string().split_whitespace().collect()
}
fn opts(s: &str) -> Options {
syn::parse2(ts(s)).unwrap()
}
fn made(me: &str, owned: &[&str], item: &str) -> String {
let head = scan::head(&ts(item)).unwrap();
norm(&announce(
&opts(me),
&head,
ts(item),
&OwnedAttrs::from_strs(owned),
))
}
fn runner(entries: &str) -> String {
format!(
"#[::l::announce::__private::__announce_runner({})]",
entries
)
}
const V: &str = "__announce_format_version = \"1\"";
const CHK: &str = "#[allow(deprecated)]const_:()={trait__AnnounceSentinel{}\
impl__AnnounceSentinelforSwhereS:::l::announce::Announce{}};";
#[test]
fn the_runner_is_put_last_with_the_version() {
assert_eq!(
made(
"crate = ::l::announce, meta = a::x",
&[],
"#[b] #[c(1)] pub struct S(pub u8);"
),
[
"#[b]#[c(1)]#[::l::announce::__private::__announce_runner(\
__announce_format_version=\"1\",__announce_sentinel=\"impl\",\
crate=::l::announce,meta=a::x)]pubstructS(pubu8);",
CHK
]
.concat()
);
for item in [
"pub enum S {}",
"union S { a: u8 }",
"pub trait S {}",
"type S = u8;",
"mod S {}",
] {
let out = made("crate = ::l::announce", &[], &format!("#[x] {}", item));
assert!(
out.starts_with("#[x]#[::l::announce::__private::__announce_runner(")
&& out.contains("__announce_format_version=\"1\""),
"{}",
out
);
assert!(
!out.contains("Capture") && !out.contains("macro_rules"),
"{}",
out
);
}
let out = made("", &[], "#[x] struct S;");
assert!(
out.contains(
"__announce_runner(__announce_format_version=\"1\",__announce_sentinel=\"impl\")]structS;"
),
"{}",
out
);
}
#[test]
fn the_names_that_checks_own_are_in_the_runner() {
assert_eq!(
made(
"crate = ::l::announce, a::check",
&["my_lib", "other"],
"#[x] pub struct S;"
),
[
"#[x]#[::l::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_sentinel=\"impl\",__announce_owned(my_lib,other),\
crate=::l::announce,a::check)]pubstructS;",
CHK
]
.concat()
);
}
#[test]
fn the_attributes_that_checks_own_follow_the_runner() {
assert_eq!(
made(
"crate = ::l::announce, a::check",
&["my_lib", "other"],
"#[my_lib(1)] #[doc = \"a\"] #[other] #[my_lib::y = 1] #[::my_lib] #[derive(Debug)] struct S;"
),
[
"#[doc=\"a\"]#[::my_lib]#[derive(Debug)]\
#[::l::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_sentinel=\"impl\",__announce_owned(my_lib,other),\
crate=::l::announce,a::check)]\
#[my_lib(1)]#[other]#[my_lib::y=1]structS;",
CHK
]
.concat()
);
assert_eq!(
made(
"crate = ::l::announce",
&["my_lib"],
"#[cfg_attr(a, my_lib(1))] #[cfg_attr(a, inline, cfg_attr(b, my_lib))] #[cfg_attr(a, inline)] #[cfg_attr(my_lib, inline)] struct S;"
),
[
"#[cfg_attr(a,inline)]#[cfg_attr(my_lib,inline)]\
#[::l::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_sentinel=\"impl\",__announce_owned(my_lib),crate=::l::announce)]\
#[cfg_attr(a,my_lib(1))]#[cfg_attr(a,inline,cfg_attr(b,my_lib))]structS;",
CHK
]
.concat()
);
assert_eq!(
made(
"crate = ::l::announce, b::check",
&["other"],
&format!(
"#[other(2)] #[x] {} #[my_lib(1)] #[y] struct S;",
runner(&format!("{}, __announce_owned(my_lib)", V))
)
),
"#[x]#[::l::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_owned(my_lib,other),crate=::l::announce,b::check)]\
#[my_lib(1)]#[other(2)]#[y]structS;"
);
}
#[test]
fn an_announcement_merges_into_the_runner_that_is_there() {
assert_eq!(
made(
"meta = b::x, c::check",
&["other"],
&format!(
"#[x] {} #[y] pub struct S(pub u8);",
runner(&format!("{}, __announce_owned(my_lib), crate = ::l::announce, meta = a::x", V))
)
),
"#[x]#[::l::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_owned(my_lib,other),crate=::l::announce,meta=a::x,meta=b::x,c::check)]#[y]pubstructS(pubu8);"
);
let item = format!(
"#[x] {} struct S;",
runner(&format!("{}, macro_name = A", V))
);
let out = made("macro_name = B", &[], &item);
assert!(out.starts_with("::core::compile_error!"), "{}", out);
assert!(out.contains("(`B`and`A`)"), "{}", out);
assert!(out.ends_with(&norm(&ts(&item))), "{}", out);
assert_eq!(out.matches("compile_error").count(), 1);
}
fn attribute(me: &str, item: &str) -> String {
norm(&crate::announce(ts(me), ts(item)))
}
#[test]
fn an_item_with_inert_attributes_only_is_announced_at_once() {
let me = "crate = ::l::announce";
for item in [
"pub struct S;",
"/// d\n#[repr(C)] pub struct S;",
"#[doc = \"d\"] #[repr(C)] #[allow(dead_code)] #[must_use] #[deprecated] #[non_exhaustive] pub struct S;",
"#[inline] #[cold] #[track_caller] #[no_mangle] #[automatically_derived] #[macro_use] #[warn(x)] #[deny(x)] #[forbid(x)] #[expect(x)] pub struct S;",
"/// d\npub trait S {}",
] {
let out = attribute(me, item);
assert!(
out.contains("Capture") && !out.contains("__announce_runner"),
"{}: {}",
item,
out
);
}
}
#[test]
fn an_item_with_an_attribute_that_may_expand_has_the_runner() {
let me = "crate = ::l::announce";
for item in [
"/// d\n#[repr(C)] #[derive(Debug)] pub struct S;",
"#[cfg_attr(a, derive(Debug))] pub struct S;",
"#[cfg(a)] pub struct S;",
"#[x::doc] pub struct S;",
"#[::doc] pub struct S;",
"#[other] pub struct S;",
"#[announce] pub struct S;",
"#[announce(level = 2)] pub struct S;",
"/// d\n#[repr(C)] #[x] pub trait S {}",
"#[docs] pub struct S;",
"#[reprs] pub struct S;",
] {
let out = attribute(me, item);
assert!(
out.contains("__announce_runner") && !out.contains("Capture"),
"{}: {}",
item,
out
);
}
}
#[test]
fn the_attributes_that_the_checks_own_do_not_count() {
let state = |item: &str, owned: &str| {
ts(&format!(
"@__announce_attrs [ ::l::announce::__private::finish! ] {{ [7u64; attr (crate=::l::announce,a::check) {{ {} }}] {} }}",
item, owned
))
};
let out = norm(&crate::finish(state(
"/// d\n#[my_lib(x)] pub struct S { #[my_lib(y)] pub a: u8 }",
"attrs(my_lib)",
)));
assert!(!out.contains("__announce_runner"), "{}", out);
assert!(
out.starts_with("#[derive(::l::announce::__private::Capture)]"),
"{}",
out
);
assert!(
out.contains("[my_lib];crate=::l::announce,a::check)]"),
"{}",
out
);
assert_eq!(out.matches("#[my_lib(x)]").count(), 1, "{}", out);
let out = norm(&crate::finish(state(
"#[cfg_attr(a, my_lib(x))] pub struct S;",
"attrs(my_lib)",
)));
assert!(!out.contains("__announce_runner"), "{}", out);
let out = norm(&crate::finish(state("#[my_lib(x)] pub struct S;", "")));
assert!(out.contains("__announce_runner"), "{}", out);
let out = norm(&crate::finish(state(
"#[my_lib(x)] #[derive(Debug)] pub struct S;",
"attrs(my_lib)",
)));
assert!(
out.starts_with("#[derive(Debug)]#[::l::announce::__private::__announce_runner(")
&& out.contains("#[my_lib(x)]pubstructS;"),
"{}",
out
);
}
#[test]
fn the_sentinel_is_an_impl_check_for_a_struct_enum_or_union_without_generics() {
for item in [
"pub struct S(pub u8);",
"enum S { A }",
"pub(crate) union S { a: u8 }",
] {
let out = made("crate = ::l::announce", &[], &format!("#[x] {}", item));
assert!(out.contains("__announce_sentinel=\"impl\""), "{}", out);
assert!(out.ends_with(CHK), "{}", out);
assert_eq!(out.matches("__AnnounceSentinel").count(), 2, "{}", out);
assert!(!out.contains("__announce_exists"), "{}", out);
}
}
#[test]
fn the_sentinel_is_the_call_of_the_trampoline_for_everything_else() {
for (opts, item, name) in [
("", "struct S<T>(T);", "S"),
("", "struct S<'a>(&'a u8);", "S"),
("", "pub enum S<const N: usize> { A }", "S"),
("", "union S<T: Copy> { a: T }", "S"),
("", "pub trait S {}", "S"),
("", "type S = u8;", "S"),
("", "mod S {}", "S"),
("mock = a::B", "struct S;", "S"),
("emit_item = false", "struct S;", "S"),
("macro_name = N", "trait S {}", "N"),
("macro_name = N, emit_item = false", "mod S {}", "N"),
] {
let opts = format!("crate = ::l::announce, {}", opts);
let out = made(&opts, &[], &format!("#[x] {}", item));
assert!(
out.contains("__announce_sentinel=\"macro\""),
"{}: {}",
item,
out
);
assert!(
out.ends_with(&format!(
"{}!{{@announce_format\"1\"@__announce_exists}}",
name
)),
"{}: {}",
item,
out
);
assert!(!out.contains("__AnnounceSentinel"), "{}: {}", item, out);
}
let out = made("", &[], "#[x] trait r#match {}");
assert!(
out.ends_with("r#match!{@announce_format\"1\"@__announce_exists}"),
"{}",
out
);
}
#[test]
fn the_sentinel_has_the_cfgs_of_the_item() {
let out = made(
"crate = ::l::announce",
&[],
"#[cfg(a)] #[x] #[cfg(not(b))] struct S;",
);
assert!(
out.ends_with(&format!("#[cfg(a)]#[cfg(not(b))]{}", CHK)),
"{}",
out
);
let out = made("crate = ::l::announce", &[], "#[cfg(a)] #[x] trait S {}");
assert!(
out.ends_with("#[cfg(a)]S!{@announce_format\"1\"@__announce_exists}"),
"{}",
out
);
let out = made(
"crate = ::l::announce",
&[],
"#[cfg_attr(a, derive(Debug))] struct S;",
);
assert!(out.ends_with(CHK), "{}", out);
for item in [
"#[cfg_attr(a, cfg(b))] #[x] struct S;",
"#[x] #[cfg_attr(a, cfg_attr(b, cfg(c)))] trait S {}",
] {
let out = made("crate = ::l::announce", &[], item);
assert!(
out.contains("__announce_runner(__announce_format_version=\"1\"")
&& !out.contains("__announce_sentinel")
&& !out.contains("__AnnounceSentinel")
&& !out.contains("__announce_exists"),
"{}: {}",
item,
out
);
}
}
#[test]
fn only_the_announcement_that_puts_the_runner_emits_a_sentinel() {
let item = format!(
"#[x] {} struct S;",
runner(&format!("{}, __announce_sentinel = \"impl\"", V))
);
let out = made("meta = b::x", &[], &item);
assert!(!out.contains("compile_error"), "{}", out);
assert_eq!(out.matches("__announce_sentinel").count(), 1, "{}", out);
assert!(!out.contains("__AnnounceSentinel"), "{}", out);
let out = norm(&run(
ts(&format!(
"{}, __announce_sentinel = \"macro\", meta = a::x",
V
)),
ts(&item),
));
assert!(
out.contains("__announce_sentinel=\"impl\"") && !out.contains("\"macro\""),
"{}",
out
);
assert!(!out.contains("__AnnounceSentinel"), "{}", out);
let out = attribute("crate = ::l::announce", "/// d\npub struct S;");
assert!(
!out.contains("__announce_sentinel") && !out.contains("__AnnounceSentinel"),
"{}",
out
);
let args =
RunnerArgs::read(ts(&format!("{}, __announce_sentinel = \"macro\"", V))).unwrap();
assert_eq!(args.sentinel, Some(Sentinel::Macro));
assert_eq!(
norm(&args.to_token_stream()),
"__announce_format_version=\"1\",__announce_sentinel=\"macro\""
);
let e = RunnerArgs::read(ts(&format!("{}, __announce_sentinel = \"cake\"", V)))
.err()
.unwrap();
assert!(e.to_string().contains("unknown sentinel `cake`"), "{}", e);
}
#[test]
fn mock_and_emit_item_false_belong_to_the_first_announcement_of_a_struct() {
let with = |sentinel: &str, opt: &str| {
format!(
"#[x] {} struct S;",
runner(&format!(
"{}, __announce_sentinel = \"{}\", {}",
V, sentinel, opt
))
)
};
for (opt, shown) in [
("emit_item = false", "`emit_item=false`"),
("mock = a::B", "`mock`"),
] {
let item = format!(
"#[x] {} struct S;",
runner(&format!("{}, __announce_sentinel = \"impl\"", V))
);
let out = made(opt, &[], &item);
assert!(out.starts_with("::core::compile_error!"), "{}", out);
assert!(
out.contains("mustbeonthefirstannouncementofanitem") && out.contains(shown),
"{}",
out
);
assert_eq!(out.matches("compile_error").count(), 1, "{}", out);
assert!(out.ends_with(&norm(&ts(&item))), "{}", out);
let out = norm(&run(ts(&format!("{}, {}", V, opt)), ts(&item)));
assert!(
out.contains("mustbeonthefirstannouncementofanitem"),
"{}",
out
);
assert!(out.ends_with(&norm(&ts(&item))), "{}", out);
let item = with("macro", opt);
let out = made("meta = a::x", &[], &item);
assert!(!out.contains("compile_error"), "{}", out);
}
let out = made(
"meta = a::x",
&[],
&format!(
"#[x] {} struct S;",
runner(&format!(
"{}, __announce_sentinel = \"macro\", mock = a::B",
V
))
),
);
assert!(!out.contains("compile_error"), "{}", out);
}
#[test]
fn a_runner_of_the_current_version_is_read() {
let item = format!("#[x] {} struct S;", runner(&format!("{}, meta = a::x", V)));
let out = made("meta = b::x", &[], &item);
assert!(!out.contains("compile_error"), "{}", out);
let item = format!(
"#[x] {} struct S;",
runner(&format!("{}, meta = a::x, {}", V, V))
);
let out = made("meta = b::x", &[], &item);
assert!(!out.contains("compile_error"), "{}", out);
assert_eq!(
out.matches("__announce_format_version").count(),
1,
"{}",
out
);
let item = format!("#[x] {} struct S;", runner(&format!("meta = a::x, {}", V)));
let out = made("", &[], &item);
assert!(
out.contains("meta=a::x") && !out.contains("compile_error"),
"{}",
out
);
}
fn version_errors(first: &str, others: &[String]) -> (String, String) {
let item = format!("#[x] {} struct S;", others.join(" "));
let from_announce = made("", &[], &item);
let from_runner = norm(&run(ts(first), ts(&item)));
(from_announce, from_runner)
}
fn assert_version_error(out: &str, listed: &[&str]) {
assert!(out.starts_with("::core::compile_error!"), "{}", out);
assert_eq!(out.matches("compile_error").count(), 1, "{}", out);
assert!(
out.contains("severalversionsofthe`announce`crate"),
"{}",
out
);
assert!(out.contains("this`announce`expects`1`"), "{}", out);
assert!(out.contains("cargotree-iannounce"), "{}", out);
for l in listed {
assert!(out.contains(l), "{}: {}", l, out);
}
assert!(!out.contains("macro_rules"), "{}", out);
}
#[test]
fn a_runner_of_another_version_is_refused_without_reading_it() {
let both =
runner("__announce_format_version = \"1\", ?? !!, __announce_format_version = \"2\"");
let (a, r) = version_errors(V, std::slice::from_ref(&both));
for out in [norm(&ts(&a)), r] {
assert_version_error(&out, &["`1`,`2`"]);
}
let (a, _) = version_errors(V, std::slice::from_ref(&both));
assert!(a.ends_with("#[x]structS;"), "{}", a);
let out = norm(&run(ts(V), ts(&format!("#[x] {} struct S;", both))));
assert!(out.ends_with("#[x]structS;"), "{}", out);
let (a, r) = version_errors(V, &[runner("__announce_format_version = \"2\", ?? !!")]);
assert_version_error(&a, &["`2`"]);
assert_version_error(&r, &["`2`"]);
let r = norm(&run(
ts("__announce_format_version = \"7\""),
ts("#[x] struct S;"),
));
assert_version_error(&r, &["`7`"]);
}
#[test]
fn runners_of_different_versions_are_listed_with_their_crate_paths() {
let a =
"#[::one::announce::__private::__announce_runner(__announce_format_version = \"1\")]";
let b = "#[::two::announce::__private::__announce_runner(__announce_format_version = \"2\", ?? !!)]";
let c = "#[::three::__announce_runner(__announce_format_version = \"2\")]";
let item = format!("#[x] {} {} {} struct S;", a, b, c);
let out = made("", &[], &item);
assert_version_error(
&out,
&["`1`(in`::one::announce`),`2`(in`::two::announce`,`::three`)"],
);
assert_eq!(out.matches("`2`(in`::two").count(), 1, "{}", out);
let item = format!("#[x] {} {} {} struct S;", b, a, b);
let out = made("", &[], &item);
assert_version_error(&out, &["`2`(in`::two::announce`),`1`(in`::one::announce`)"]);
assert_eq!(out.matches("`2`(in`::two").count(), 1, "{}", out);
let r = norm(&run(
ts("__announce_format_version = \"1\""),
ts(&format!("#[x] {} struct S;", b)),
));
assert_version_error(&r, &["`1`,`2`"]);
let item = format!(
"#[x] {} {} struct S;",
runner("__announce_format_version = \"3\""),
runner("__announce_format_version = \"3\", __announce_format_version = \"4\"")
);
let out = made("", &[], &item);
assert_version_error(&out, &["`3`,`4`"]);
assert!(!out.contains("(in"), "{}", out);
}
#[test]
fn a_runner_without_a_version_is_version_unknown() {
for r in [
runner("meta = a::x"),
runner(""),
"#[::l::announce::__private::__announce_runner]".to_string(),
runner("__announce_format_version, meta = a::x"),
runner("__announce_format_version(1)"),
runner("x::__announce_format_version = \"1\""),
] {
let item = format!("#[x] {} struct S;", r);
let out = made("", &[], &item);
assert_version_error(&out, &["`unknown`"]);
assert!(out.ends_with("#[x]structS;"), "{}: {}", r, out);
}
for args in ["", "meta = a::x", "__announce_format_version, meta = a::x"] {
let out = norm(&run(ts(args), ts("#[x] struct S;")));
assert_version_error(&out, &["`unknown`"]);
}
let item = format!("#[x] {} {} struct S;", runner(V), runner("meta = a::x"));
assert_version_error(&made("", &[], &item), &["`1`", "`unknown`"]);
}
#[test]
fn several_runners_of_the_current_version_are_merged_into_the_last() {
let first = "#[::one::announce::__private::__announce_runner(__announce_format_version = \"1\", __announce_owned(a), crate = ::one::announce, meta = one::x)]";
let second = "#[::two::announce::__private::__announce_runner(__announce_format_version = \"1\", __announce_owned(b), crate = ::two::announce, meta = two::x)]";
let out = made(
"meta = three::x",
&["c"],
&format!("#[x] {} #[y] {} struct S;", first, second),
);
assert_eq!(
out,
"#[x]#[y]#[::two::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_owned(b,a,c),crate=::two::announce,meta=two::x,meta=one::x,meta=three::x)]structS;"
);
let out = norm(&run(
ts("__announce_format_version = \"1\", crate = ::zero::announce, meta = zero::x"),
ts(&format!("#[x] {} {} struct S;", first, second)),
));
assert_eq!(
out,
"#[x]#[::two::announce::__private::__announce_runner(__announce_format_version=\"1\",\
__announce_owned(b,a),crate=::two::announce,meta=two::x,meta=one::x,meta=zero::x)]structS;"
);
let clash = "#[::two::announce::__private::__announce_runner(__announce_format_version = \"1\", macro_name = Other)]";
let out = made("", &[], &format!("{} {} struct S;", first, clash));
assert!(
out.contains("`Other`") && out.starts_with("::core::compile_error!"),
"{}",
out
);
}
#[test]
fn the_runner_makes_the_trampoline_without_the_attributes_that_checks_own() {
let out = norm(&run(
ts(&format!(
"{}, __announce_owned(my_lib), crate = ::l::announce, a::check",
V
)),
ts("#[doc = \"d\"] #[my_lib(1)] pub struct S { #[my_lib(2)] pub a: u8 }"),
));
assert!(
out.starts_with("#[derive(::l::announce::__private::Capture)]"),
"{}",
out
);
assert!(
out.contains("#[__announce(")
&& out.contains("[my_lib];crate=::l::announce,a::check)]"),
"{}",
out
);
assert_eq!(out.matches("#[my_lib(1)]").count(), 1, "{}", out);
assert!(!out.contains("__announce_runner"), "{}", out);
}
#[test]
fn an_announcement_below_the_runner_is_an_error_and_is_removed() {
for late in ["#[::announce::announce(meta = l::x)]", "#[x::announce]"] {
let out = norm(&run(
ts(&format!("{}, crate = ::l::announce", V)),
ts(&format!("#[x] {} {} pub struct S;", late, "#[y]")),
));
assert!(
out.starts_with("::core::compile_error!"),
"{}: {}",
late,
out
);
assert!(
out.contains("expandsafteritsannouncementswererun"),
"{}",
out
);
assert_eq!(out.matches("compile_error").count(), 1, "{}", out);
assert!(
out.contains("Capture") && out.matches("pubstructS").count() == 2,
"{}",
out
);
assert!(
!out.contains("announce::announce") && !out.contains("derive(Debug"),
"{}",
out
);
}
let out = norm(&run(
ts(V),
ts("#[derive(Debug, Announce)] #[x::announced] #[ann] #[announce(meta = h::x)] pub struct S;"),
));
assert!(!out.contains("compile_error"), "{}", out);
assert!(
out.contains("#[derive(Debug,Announce)]")
&& out.contains("#[ann]")
&& out.contains("#[announce(meta=h::x)]"),
"{}",
out
);
assert_eq!(out.matches("#[announce(meta=h::x)]").count(), 2, "{}", out);
}
#[test]
fn the_runner_of_an_item_that_is_not_emitted_drops_it() {
for opt in ["emit_item = false", "mock = a::B"] {
let out = norm(&run(
ts(&format!("{}, crate = ::l::announce, {}", V, opt)),
ts("#[doc = \"d\"] pub struct S;"),
));
assert!(
out.starts_with("#[derive(::l::announce::__private::Capture)]"),
"{}",
out
);
assert_eq!(out.matches("pubstructS").count(), 1, "{}", out);
}
}
fn expand(item: &str) -> (usize, String) {
let mut item = ts(item);
let mut steps = 0;
loop {
let o = scan::outer(&item);
let next = o.attrs.iter().position(|a| {
let last = a.segments.last().map(|l| l.to_string()).unwrap_or_default();
match last.as_str() {
"announce" => a.segments.len() > 1,
"derive" => !a
.args
.as_ref()
.map_or(false, |g| g.stream().to_string().contains("Capture")),
"emit" | "pass" | "late" | "dup" | "__announce_runner" => true,
_ => false,
}
});
let k = match next {
Some(k) => k,
None => return (steps, norm(&item)),
};
steps += 1;
assert!(steps < 100, "no end: {}", norm(&item));
let a = &o.attrs[k];
let args = a.args.as_ref().map(|g| g.stream()).unwrap_or_default();
let mut rest = TokenStream::new();
for (i, t) in o.tts.iter().enumerate() {
if i != a.at && i != a.at + 1 {
rest.extend([t.clone()]);
}
}
let name = a.segments.last().unwrap().to_string();
item = match name.as_str() {
"announce" => crate::announce(args, rest),
"__announce_runner" => crate::run_announce(args, rest),
"derive" => rest,
"emit" => {
let mut out = ts(&format!(
"#[::l::announce::announce(crate = ::l::announce, meta = m::{})]",
args
));
out.extend(rest);
out
}
"late" => {
let o = scan::outer(&rest);
let end = o.attrs_end();
let mut out: TokenStream = o.tts[..end].iter().cloned().collect();
out.extend(ts(&format!(
"#[::l::announce::announce(crate = ::l::announce, meta = m::{})]",
args
)));
out.extend(o.tts[end..].iter().cloned());
out
}
"pass" => rest,
"dup" => {
let n: u32 = args.to_string().trim().parse().unwrap();
if n == 0 {
rest
} else {
let mut out = ts(&format!(
"#[::l::announce::announce(crate = ::l::announce, meta = m::dup{n})] #[dup({m})]",
n = n,
m = n - 1
));
out.extend(rest);
out
}
}
_ => unreachable!(),
};
}
}
const A: &str = "#[::l::announce::announce(crate = ::l::announce, meta = m::a)]";
const B: &str = "#[::l::announce::announce(meta = m::b)]";
const C: &str = "#[::l::announce::announce(meta = m::c)]";
fn trampoline(left: &str) -> String {
let made = left
.matches("derive(::l::announce::__private::Capture)")
.count();
assert_eq!(made, 1, "{}", left);
left.to_string()
}
#[test]
fn expansion_ends_with_one_trampoline() {
let cases: &[(String, &[&str])] = &[
(format!("{} {} #[x] struct S;", A, B), &["m::a", "m::b"]),
(format!("{} {} {} #[x] struct S;", A, B, C), &["m::a", "m::b", "m::c"]),
(format!("{} #[pass] {} trait S {{}}", A, B), &["m::a", "m::b"]),
("#[emit(a)] #[pass] struct S;".to_string(), &["m::a"]),
("#[emit(a)] #[emit(b)] struct S;".to_string(), &["m::a", "m::b"]),
(format!("{} #[emit(two)] {} struct S;", A, B), &["m::a", "m::two", "m::b"]),
("#[dup(4)] struct S;".to_string(), &["m::dup4", "m::dup3", "m::dup2", "m::dup1"]),
(format!("#[derive(Debug)] {} #[derive(Clone)] struct S;", A), &["m::a"]),
(format!("{} struct S;", A), &["m::a"]),
(format!("{} {} struct S;", "#[::l::announce::announce(crate = ::l::announce, meta = m::a, emit_item = false)]", B), &["m::a", "m::b"]),
(format!("{} {} trait S {{}}", A, "#[::l::announce::announce(emit_item = false, meta = m::b)]"), &["m::a", "m::b"]),
];
for (item, metas) in cases {
let (steps, left) = expand(item);
let made = trampoline(&left);
let positions: Vec<usize> = metas
.iter()
.map(|m| made.find(m).unwrap_or_else(|| panic!("{}: {}", m, made)))
.collect();
assert!(
positions.windows(2).all(|w| w[0] < w[1]),
"{}\n{}",
item,
made
);
assert!(!left.contains("compile_error"), "{}: {}", item, left);
assert!(!left.contains("__announce_runner"), "{}: {}", item, left);
assert!(steps <= 4 * metas.len() + 4, "{}: {}", item, steps);
}
}
#[test]
fn the_number_of_expansions_grows_with_the_announcements() {
for n in 1..=8u32 {
let (steps, left) = expand(&format!("#[dup({})] struct S;", n));
trampoline(&left);
assert_eq!(steps, 2 * n as usize + 2, "{}", n);
}
}
#[test]
fn an_announcement_after_the_runner_is_an_error_and_the_trampoline_is_made_once() {
let (_, left) = expand(&format!("{} #[x] #[late(l)] struct S;", A));
assert!(left.starts_with("::core::compile_error!"), "{}", left);
assert_eq!(left.matches("compile_error").count(), 1, "{}", left);
assert_eq!(
left.matches("derive(::l::announce::__private::Capture)")
.count(),
1,
"{}",
left
);
}
}