use crate::helper::{Cont, ContList};
use proc_macro2::{Ident, Span, TokenStream};
use std::collections::HashSet;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::visit::Visit;
use syn::visit_mut::VisitMut;
use syn::{Attribute, Item, Result, Token};
use template_quote::{quote, ToTokens};
pub(crate) const QUERY_TAG: &str = "__announce_attrs";
pub(crate) const META_NAME: &str = "attrs";
const RESERVED: &[&str] = &[
"cfg",
"cfg_attr",
"cfg_eval",
"cfg_accessible",
"derive",
"derive_const",
"test",
"bench",
"test_case",
"global_allocator",
"proc_macro",
"proc_macro_derive",
"proc_macro_attribute",
"macro_use",
"macro_export",
"macro_reexport",
"path",
"allow",
"warn",
"deny",
"forbid",
"expect",
"doc",
"deprecated",
"must_use",
"must_not_suspend",
"ignore",
"should_panic",
"automatically_derived",
"diagnostic",
"stable",
"unstable",
"allow_internal_unstable",
"allow_internal_unsafe",
"inline",
"cold",
"track_caller",
"target_feature",
"instruction_set",
"optimize",
"naked",
"no_mangle",
"export_name",
"link_name",
"link_section",
"link_ordinal",
"link",
"linkage",
"used",
"repr",
"non_exhaustive",
"thread_local",
"coverage",
"no_sanitize",
"sanitize",
"cfi_encoding",
"patchable_function_entry",
"ffi_const",
"ffi_pure",
"ffi_returns_twice",
"collapse_debuginfo",
"debugger_visualizer",
"export_stable",
"pointee",
"const_trait",
"marker",
"fundamental",
"lang",
"unsafe",
"define_opaque",
"custom_mir",
"loop_match",
"const_continue",
"crate_name",
"crate_type",
"no_std",
"no_core",
"no_main",
"no_implicit_prelude",
"no_link",
"no_builtins",
"recursion_limit",
"type_length_limit",
"windows_subsystem",
"panic_handler",
"global_asm",
"feature",
"register_tool",
"register_attr",
"plugin",
"prelude_import",
"start",
"default_lib_allocator",
"needs_allocator",
"panic_runtime",
"rustfmt",
"clippy",
"rustc",
"rustdoc",
"miri",
"rust_analyzer",
"announce",
];
fn reserved(name: &str) -> Option<&'static str> {
if RESERVED.contains(&name) {
Some("an attribute that rustc or a standard tool uses")
} else if name.starts_with("rustc_") {
Some("a name reserved for rustc (`rustc_*`)")
} else if name.starts_with("__announce") {
Some("a name reserved for announce (`__announce*`)")
} else {
None
}
}
#[derive(Clone, Debug, Default)]
pub struct OwnedAttrs {
names: Vec<Ident>,
}
impl PartialEq for OwnedAttrs {
fn eq(&self, other: &Self) -> bool {
self.keys() == other.keys()
}
}
impl Eq for OwnedAttrs {}
impl OwnedAttrs {
pub fn new() -> Self {
Self::default()
}
pub fn from_idents(names: impl IntoIterator<Item = Ident>) -> Self {
let mut out = OwnedAttrs::new();
out.extend(names);
out
}
pub fn from_strs(names: &[&str]) -> Self {
Self::from_idents(
names
.iter()
.map(|n| Ident::new(n.strip_prefix("r#").unwrap_or(n), Span::call_site())),
)
}
pub fn extend(&mut self, names: impl IntoIterator<Item = Ident>) {
for n in names {
if !self.contains(&n.unraw().to_string()) {
self.names.push(n);
}
}
}
pub fn names(&self) -> &[Ident] {
&self.names
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn contains(&self, name: &str) -> bool {
self.names.iter().any(|n| n.unraw() == name)
}
fn keys(&self) -> Vec<String> {
self.names.iter().map(|n| n.unraw().to_string()).collect()
}
pub fn owns(&self, attr: &Attribute) -> bool {
let path = attr.path();
path.leading_colon.is_none()
&& path
.segments
.first()
.map_or(false, |s| self.contains(&s.ident.unraw().to_string()))
}
pub fn find(&self, item: &Item) -> Vec<Attribute> {
struct V<'a>(&'a OwnedAttrs, Vec<Attribute>);
impl<'a, 'ast> Visit<'ast> for V<'a> {
fn visit_attribute(&mut self, a: &'ast Attribute) {
if self.0.owns(a) {
self.1.push(a.clone());
}
}
}
let mut v = V(self, Vec::new());
v.visit_item(item);
v.1
}
pub fn strip(&self, item: &mut Item) -> Result<bool> {
if self.is_empty() {
return Ok(false);
}
remove_attributes(item, |a| self.owns(a))
}
pub fn check_names(&self) -> Result<()> {
for n in &self.names {
if let Some(why) = reserved(&n.unraw().to_string()) {
return Err(syn::Error::new(
n.span(),
format!(
"announce: `{}` cannot be an owned attribute name: it is {}",
n, why
),
));
}
}
Ok(())
}
pub fn parse_names(tokens: TokenStream) -> Result<Self> {
let p: Punctuated<Ident, Token![,]> =
syn::parse::Parser::parse2(Punctuated::parse_terminated, tokens)?;
Ok(Self::from_idents(p))
}
}
impl ToTokens for OwnedAttrs {
fn to_tokens(&self, tokens: &mut TokenStream) {
if self.is_empty() {
return;
}
let name = Ident::new(META_NAME, Span::call_site());
let names = &self.names;
tokens.extend(quote! { #name(#(#names),*) });
}
}
const MARK: &str = "__announce_removed_attribute";
pub fn remove_attributes(item: &mut Item, remove: impl FnMut(&Attribute) -> bool) -> Result<bool> {
struct V<F>(F, bool);
impl<F: FnMut(&Attribute) -> bool> VisitMut for V<F> {
fn visit_attribute_mut(&mut self, a: &mut Attribute) {
if (self.0)(a) {
let mark: syn::Path = Ident::new(MARK, Span::call_site()).into();
match &mut a.meta {
syn::Meta::Path(p) => *p = mark,
syn::Meta::List(l) => l.path = mark,
syn::Meta::NameValue(nv) => nv.path = mark,
}
self.1 = true;
}
syn::visit_mut::visit_attribute_mut(self, a);
}
}
let mut v = V(remove, false);
v.visit_item_mut(item);
if !v.1 {
return Ok(false);
}
*item = syn::parse2(drop_marked_attributes(item.to_token_stream()))?;
Ok(true)
}
fn drop_marked_attributes(ts: TokenStream) -> TokenStream {
use proc_macro2::{Delimiter, Group, TokenTree};
let tts: Vec<TokenTree> = ts.into_iter().collect();
let mut out = Vec::with_capacity(tts.len());
let mut i = 0;
while i < tts.len() {
match &tts[i] {
TokenTree::Punct(p) if p.as_char() == '#' => {
let mut j = i + 1;
if matches!(tts.get(j), Some(TokenTree::Punct(b)) if b.as_char() == '!') {
j += 1;
}
if let Some(TokenTree::Group(g)) = tts.get(j) {
let marked = g.delimiter() == Delimiter::Bracket
&& matches!(
g.stream().into_iter().next(),
Some(TokenTree::Ident(id)) if id == MARK
);
if marked {
i = j + 1;
continue;
}
}
out.push(tts[i].clone());
}
TokenTree::Group(g) => {
let mut ng = Group::new(g.delimiter(), drop_marked_attributes(g.stream()));
ng.set_span(g.span());
out.push(TokenTree::Group(ng));
}
t => out.push(t.clone()),
}
i += 1;
}
out.into_iter().collect()
}
#[derive(Clone, Debug)]
pub struct Query {
pub list: ContList,
pub state: TokenStream,
}
impl Parse for Query {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<Token![@]>()?;
let tag: Ident = input.parse()?;
if tag != QUERY_TAG {
return Err(syn::Error::new(
tag.span(),
format!("announce: expected `@{}`", QUERY_TAG),
));
}
let list;
syn::bracketed!(list in input);
let state;
syn::braced!(state in input);
let q = Query {
list: list.parse()?,
state: state.parse()?,
};
if !input.is_empty() {
return Err(input.error("announce: unexpected tokens after the attribute query"));
}
Ok(q)
}
}
impl Query {
pub fn answer(&self, owned: &OwnedAttrs) -> TokenStream {
if let Err(e) = owned.check_names() {
return e.to_compile_error();
}
let mut it = self.list.0.iter();
match it.next() {
Some(Cont::Bang(_)) => {}
_ => {
return quote! {
::core::compile_error! { "announce: the list of the attribute query is empty or does not start with a macro: it must start with the macro that is called (this check) and end with `finish!`. A check before this one did not forward its list (without its own first element)" }
}
}
}
let next = match it.clone().next() {
Some(c @ Cont::Bang(_)) => c,
_ => {
return quote! {
::core::compile_error! { "announce: the attribute query has no macro to call after this check: the list ends with this check, and `finish!` must end it. A check before this one forwarded too little of its list" }
}
}
};
let rest: ContList = it.cloned().collect();
let state = &self.state;
quote! { #next { @__announce_attrs #rest { #state #owned } } }
}
}
#[allow(clippy::large_enum_variant)]
pub enum CheckCall {
Query(Query),
Run(crate::helper::CheckInput),
}
impl Parse for CheckCall {
fn parse(input: ParseStream) -> Result<Self> {
if input.peek(Token![@]) {
Ok(CheckCall::Query(input.parse()?))
} else {
Ok(CheckCall::Run(input.parse()?))
}
}
}
pub(crate) fn owned_from_meta(meta: &crate::helper::Meta) -> Option<OwnedAttrs> {
if !meta.path.is_ident(META_NAME) {
return None;
}
OwnedAttrs::parse_names(meta.args.clone().unwrap_or_default()).ok()
}
pub(crate) fn owned_from_metas(metas: &[crate::helper::Meta]) -> OwnedAttrs {
let mut out = OwnedAttrs::new();
let mut seen = HashSet::new();
for m in metas {
if let Some(o) = owned_from_meta(m) {
for n in o.names {
if seen.insert(n.unraw().to_string()) {
out.names.push(n);
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn s(ts: impl ToTokens) -> String {
ts.to_token_stream()
.to_string()
.split_whitespace()
.collect()
}
fn item(ts: TokenStream) -> Item {
syn::parse2(ts).unwrap()
}
#[test]
fn matches_the_first_path_segment() {
let owned = OwnedAttrs::from_strs(&["my_lib"]);
let attrs: Vec<Attribute> = vec![
syn::parse_quote!(#[my_lib]),
syn::parse_quote!(#[my_lib(skip)]),
syn::parse_quote!(#[my_lib = "x"]),
syn::parse_quote!(#[my_lib::skip]),
syn::parse_quote!(#[my_lib::a::b(1)]),
];
assert!(attrs.iter().all(|a| owned.owns(a)));
let others: Vec<Attribute> = vec![
syn::parse_quote!(#[other]),
syn::parse_quote!(#[my_libs]),
syn::parse_quote!(#[a::my_lib]),
syn::parse_quote!(#[::my_lib::x]),
syn::parse_quote!(#[doc = "my_lib"]),
syn::parse_quote!(#[cfg(my_lib)]),
];
assert!(others.iter().all(|a| !owned.owns(a)));
}
#[test]
fn finds_attributes_in_every_position() {
let owned = OwnedAttrs::from_strs(&["t"]);
let it = item(quote! {
#[t(0)]
pub struct S<#[t(1)] T, #[t(2)] const N: usize> {
#[t(3)] a: T,
b: [u8; N],
}
});
assert_eq!(owned.find(&it).len(), 4);
let it = item(quote! {
#[t(0)]
pub enum E { #[t(1)] A, B(#[t(2)] u8), #[t(3)] C { #[t(4)] x: u8 } }
});
assert_eq!(owned.find(&it).len(), 5);
let it = item(quote! {
#[t(0)]
pub trait Tr {
#[t(1)] type A;
#[t(2)] const C: u8;
#[t(3)] fn f(&self, #[t(4)] x: u8) -> u8 { #[t(5)] let y = x; y }
}
});
assert_eq!(owned.find(&it).len(), 6);
let it = item(quote! {
#[t(0)]
mod m {
#![t(1)]
#[t(2)] pub struct S { #[t(3)] a: u8 }
#[t(4)] fn f() {}
}
});
let found = owned.find(&it);
assert_eq!(found.len(), 5);
assert!(found
.iter()
.any(|a| matches!(a.style, syn::AttrStyle::Inner(_))));
}
#[test]
fn leaves_macro_bodies_alone() {
let owned = OwnedAttrs::from_strs(&["t"]);
let mut it = item(quote! {
mod m { m!(#[t] x); macro_rules! k { () => { #[t] struct Z; } } #[t] struct Y; }
});
assert_eq!(owned.find(&it).len(), 1);
assert!(owned.strip(&mut it).unwrap());
let out = s(&it);
assert!(
out.contains("m!(#[t]x)") && out.contains("#[t]structZ"),
"{}",
out
);
assert!(
out.contains("structY") && !out.contains("#[t]structY"),
"{}",
out
);
}
#[test]
fn strips_and_keeps_everything_else() {
let owned = OwnedAttrs::from_strs(&["my_lib", "other"]);
let mut it = item(quote! {
#[derive(Clone)]
#[my_lib(a)]
pub struct S {
#[other] #[serde(rename = "x")] pub a: u8,
#[my_lib::tag = "y"] pub b: u8,
}
});
assert!(owned.strip(&mut it).unwrap());
assert_eq!(
s(&it),
"#[doc=\"docs\"]#[derive(Clone)]pubstructS{#[serde(rename=\"x\")]puba:u8,pubb:u8,}"
);
assert!(!owned.strip(&mut it).unwrap());
assert!(!OwnedAttrs::new().strip(&mut it).unwrap());
}
#[test]
fn names_are_unique_ordered_and_printed() {
let o = OwnedAttrs::from_strs(&["b", "a", "b", "r#a"]);
assert_eq!(s(&o), "attrs(b,a)");
assert_eq!(s(OwnedAttrs::new()), "");
let back = OwnedAttrs::parse_names(quote! { b, a }).unwrap();
assert_eq!(back, o);
assert!(o.contains("a") && !o.contains("c"));
}
#[test]
fn reserved_names_are_errors() {
for bad in [
"cfg",
"doc",
"derive",
"allow",
"inline",
"repr",
"rustc_foo",
"__announce_x",
"announce",
"clippy",
] {
let e = OwnedAttrs::from_strs(&[bad]).check_names().unwrap_err();
assert!(e.to_string().contains(bad), "{}: {}", bad, e);
}
for ok in ["my_lib", "skip", "sumtype", "serde", "doc_hidden"] {
OwnedAttrs::from_strs(&[ok]).check_names().unwrap();
}
}
#[test]
fn the_query_is_answered_by_calling_the_next_element() {
let q: Query = syn::parse_quote! {
@__announce_attrs [ a::check!, b::check!, ::announce::__private::finish! ] { [1u64; attr (x) { struct S; }] }
};
assert_eq!(q.list.0.len(), 3);
let answered = q.answer(&OwnedAttrs::from_strs(&["a", "c"]));
assert_eq!(
s(&answered),
"b::check!{@__announce_attrs[b::check!,::announce::__private::finish!]{[1u64;attr(x){structS;}]attrs(a,c)}}"
);
assert_eq!(
s(q.answer(&OwnedAttrs::new())),
"b::check!{@__announce_attrs[b::check!,::announce::__private::finish!]{[1u64;attr(x){structS;}]}}"
);
let q2: Query = syn::parse2(syn::parse2::<syn::Macro>(answered).unwrap().tokens).unwrap();
assert_eq!(s(&q2.list), "[b::check!,::announce::__private::finish!]");
assert_eq!(
s(q2.answer(&OwnedAttrs::from_strs(&["b"]))),
"::announce::__private::finish!{@__announce_attrs[::announce::__private::finish!]{[1u64;attr(x){structS;}]attrs(a,c)attrs(b)}}"
);
let q: Query = syn::parse_quote! {
@__announce_attrs [ crate::a!, ::x::b!, self::d!, e!, f::finish! ] { st }
};
assert_eq!(
s(q.answer(&OwnedAttrs::new())),
"::x::b!{@__announce_attrs[::x::b!,self::d!,e!,f::finish!]{st}}"
);
let q: Query = syn::parse_quote! { @__announce_attrs [ a!, a!, f::finish! ] { st } };
assert_eq!(
s(q.answer(&OwnedAttrs::new())),
"a!{@__announce_attrs[a!,f::finish!]{st}}"
);
let q: Query = syn::parse_quote! { @__announce_attrs [ b!, f::finish! ] { st } };
assert_eq!(
s(q.answer(&OwnedAttrs::from_strs(&["a"]))),
"f::finish!{@__announce_attrs[f::finish!]{stattrs(a)}}"
);
}
#[test]
fn a_list_that_does_not_fit_is_an_error_not_a_panic() {
let q: Query = syn::parse_quote! { @__announce_attrs [] { st } };
let out = s(q.answer(&OwnedAttrs::new()));
assert!(
out.contains("compile_error") && out.contains("muststartwiththemacrothatiscalled"),
"{}",
out
);
let q: Query = syn::parse_quote! { @__announce_attrs [ b::check! ] { st } };
let out = s(q.answer(&OwnedAttrs::new()));
assert!(
out.contains("compile_error") && out.contains("nomacrotocallafterthischeck"),
"{}",
out
);
let q: Query = syn::parse_quote! { @__announce_attrs [ #[derive(A)] ] { st } };
assert!(s(q.answer(&OwnedAttrs::new())).contains("compile_error"));
let q: Query = syn::parse_quote! { @__announce_attrs [ b!, #[derive(A)] ] { st } };
assert!(s(q.answer(&OwnedAttrs::new())).contains("compile_error"));
let q: Query = syn::parse_quote! { @__announce_attrs [ b!, f::finish! ] { st } };
let out = s(q.answer(&OwnedAttrs::from_strs(&["doc"])));
assert!(
out.contains("compile_error") && out.contains("cannotbeanownedattributename"),
"{}",
out
);
}
#[test]
fn malformed_queries_are_errors() {
for bad in [
quote! { @other [ f! ] { } },
quote! { @__announce_attrs { } },
quote! { @__announce_attrs [ f! ] { } extra },
quote! { (a), struct S; },
] {
assert!(syn::parse2::<Query>(bad).is_err());
}
}
#[test]
fn a_check_call_is_a_query_or_its_input() {
let q: CheckCall = syn::parse_quote! { @__announce_attrs [ c!, f! ] { } };
assert!(matches!(q, CheckCall::Query(_)));
let r: CheckCall = syn::parse_quote! { (a, b), struct S; };
match r {
CheckCall::Run(i) => assert_eq!(s(i.args), "a,b"),
_ => panic!(),
}
}
}