use {
crate::core::{
Result,
constants::attributes,
},
proc_macro2::TokenStream,
syn::{
Attribute,
Expr,
ExprLit,
Lit,
Meta,
parse::Parse,
spanned::Spanned,
},
};
pub(crate) fn attr_matches(
attr: &Attribute,
name: &str,
) -> bool {
let path = attr.path();
path.is_ident(name) || path.segments.last().is_some_and(|seg| seg.ident == name)
}
pub fn find_attribute(
attrs: &[Attribute],
name: &str,
) -> Option<usize> {
attrs.iter().position(|attr| attr_matches(attr, name))
}
pub fn has_attribute(
attrs: &[Attribute],
name: &str,
) -> bool {
attrs.iter().any(|attr| attr_matches(attr, name))
}
pub fn count_attributes(
attrs: &[Attribute],
name: &str,
) -> usize {
attrs.iter().filter(|attr| attr_matches(attr, name)).count()
}
pub fn reject_duplicate_attribute(
attrs: &[Attribute],
name: &str,
) -> crate::core::Result<()> {
if has_attribute(attrs, name) {
Err(crate::core::Error::validation(
proc_macro2::Span::call_site(),
format!("#[{name}] can only be used once per item. Remove the duplicate attribute"),
))
} else {
Ok(())
}
}
pub fn should_keep_attribute(attr: &Attribute) -> bool {
!is_doc_attribute(attr)
}
pub fn is_doc_attribute(attr: &Attribute) -> bool {
attributes::DOCUMENT_SPECIFIC_ATTRS.iter().any(|name| attr.path().is_ident(name))
}
pub fn filter_doc_attributes(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
attrs.iter().filter(|attr| should_keep_attribute(attr))
}
pub fn remove_attribute_tokens(
attrs: &mut Vec<Attribute>,
index: usize,
) -> syn::Result<TokenStream> {
let attr = attrs.remove(index);
if let Ok(meta_list) = attr.meta.require_list() {
Ok(meta_list.tokens.clone())
} else {
Ok(TokenStream::new())
}
}
pub fn parse_unique_attribute_value(
attrs: &[syn::Attribute],
name: &str,
) -> Result<Option<String>> {
let mut found = None;
for attr in attrs {
if attr.path().is_ident(name) {
if found.is_some() {
return Err(crate::core::Error::validation(
attr.span(),
format!("Multiple `#[{name}]` attributes found on same item"),
));
}
if let syn::Meta::NameValue(nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s), ..
}) = &nv.value
{
found = Some(s.value());
}
}
}
Ok(found)
}
#[allow(dead_code, reason = "API kept for completeness")]
pub trait AttributeExt {
fn find_and_remove<T: Parse>(
&mut self,
name: &str,
) -> Result<Option<T>>;
fn find_value(
&self,
name: &str,
) -> Result<Option<String>>;
fn find_and_remove_value(
&mut self,
name: &str,
) -> Result<Option<String>>;
fn has_attribute(
&self,
name: &str,
) -> bool;
fn find_value_or_collect(
&self,
name: &str,
errors: &mut crate::core::error_handling::ErrorCollector,
) -> Option<String>;
}
impl AttributeExt for Vec<Attribute> {
fn find_and_remove<T: Parse>(
&mut self,
name: &str,
) -> Result<Option<T>> {
let Some(index) = find_attribute(self, name) else {
return Ok(None);
};
let tokens = remove_attribute_tokens(self, index).map_err(crate::core::Error::Parse)?;
if tokens.is_empty() {
return Ok(None);
}
let parsed = syn::parse2::<T>(tokens).map_err(crate::core::Error::Parse)?;
Ok(Some(parsed))
}
fn find_value(
&self,
name: &str,
) -> Result<Option<String>> {
parse_unique_attribute_value(self, name)
}
fn find_and_remove_value(
&mut self,
name: &str,
) -> Result<Option<String>> {
let Some(index) = find_attribute(self, name) else {
return Ok(None);
};
let attr = self.remove(index);
if let Meta::NameValue(nv) = &attr.meta
&& let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &nv.value
{
return Ok(Some(s.value()));
}
Ok(None)
}
fn has_attribute(
&self,
name: &str,
) -> bool {
has_attribute(self, name)
}
fn find_value_or_collect(
&self,
name: &str,
errors: &mut crate::core::error_handling::ErrorCollector,
) -> Option<String> {
self.find_value(name).unwrap_or_else(|e| {
errors.push(e.into());
None
})
}
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "Tests use panicking operations for brevity and clarity"
)]
mod tests {
use super::*;
#[test]
fn test_has_attr() {
use syn::parse_quote;
let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = "test"])];
assert!(has_attribute(&attrs, "doc"));
assert!(!has_attribute(&attrs, "test"));
}
#[test]
fn test_filter_document_default() {
use syn::parse_quote;
let attrs: Vec<Attribute> =
vec![parse_quote!(#[document_default]), parse_quote!(#[derive(Debug)])];
let filtered: Vec<_> = filter_doc_attributes(&attrs).collect();
assert_eq!(filtered.len(), 1);
assert!(filtered[0].path().is_ident("derive"));
}
#[test]
fn test_filter_document_use() {
use syn::parse_quote;
let attrs: Vec<Attribute> =
vec![parse_quote!(#[document_use = "Of"]), parse_quote!(#[inline])];
let filtered: Vec<_> = filter_doc_attributes(&attrs).collect();
assert_eq!(filtered.len(), 1);
assert!(filtered[0].path().is_ident("inline"));
}
#[test]
fn test_is_doc_attribute() {
use syn::parse_quote;
let document_default: Attribute = parse_quote!(#[document_default]);
let document_use: Attribute = parse_quote!(#[document_use = "Of"]);
let derive: Attribute = parse_quote!(#[derive(Debug)]);
assert!(is_doc_attribute(&document_default));
assert!(is_doc_attribute(&document_use));
assert!(!is_doc_attribute(&derive));
}
#[test]
fn test_should_keep_attribute() {
use syn::parse_quote;
let document_default: Attribute = parse_quote!(#[document_default]);
let derive: Attribute = parse_quote!(#[derive(Debug)]);
assert!(!should_keep_attribute(&document_default));
assert!(should_keep_attribute(&derive));
}
#[test]
fn test_filter_empty() {
let attrs: Vec<Attribute> = vec![];
let filtered: Vec<_> = filter_doc_attributes(&attrs).collect();
assert_eq!(filtered.len(), 0);
}
#[test]
fn test_filter_all_doc_attrs() {
use syn::parse_quote;
let attrs: Vec<Attribute> =
vec![parse_quote!(#[document_default]), parse_quote!(#[document_use = "Of"])];
let filtered: Vec<_> = filter_doc_attributes(&attrs).collect();
assert_eq!(filtered.len(), 0);
}
#[test]
fn test_filter_no_doc_attrs() {
use syn::parse_quote;
let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Debug)]), parse_quote!(#[inline])];
let filtered: Vec<_> = filter_doc_attributes(&attrs).collect();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_remove_attribute_tokens() {
use syn::{
ItemStruct,
LitStr,
parse_quote,
};
let mut item: ItemStruct = parse_quote! {
#[test_attr("hello")]
#[derive(Debug)]
struct Foo;
};
let idx = find_attribute(&item.attrs, "test_attr").unwrap();
let tokens = remove_attribute_tokens(&mut item.attrs, idx).unwrap();
assert_eq!(item.attrs.len(), 1);
assert!(has_attribute(&item.attrs, "derive"));
assert!(!has_attribute(&item.attrs, "test_attr"));
let lit: LitStr = syn::parse2(tokens).unwrap();
assert_eq!(lit.value(), "hello");
}
#[test]
fn test_remove_attribute_tokens_empty() {
use syn::{
ItemStruct,
parse_quote,
};
let mut item: ItemStruct = parse_quote! {
#[test_attr]
struct Foo;
};
let idx = find_attribute(&item.attrs, "test_attr").unwrap();
let tokens = remove_attribute_tokens(&mut item.attrs, idx).unwrap();
assert_eq!(item.attrs.len(), 0);
assert!(tokens.is_empty());
}
#[test]
fn test_parse_unique_attribute_value() {
use syn::parse_quote;
let attrs: Vec<Attribute> = vec![parse_quote!(#[test = "value"])];
let result = parse_unique_attribute_value(&attrs, "test");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some("value".to_string()));
let multi_attrs: Vec<Attribute> =
vec![parse_quote!(#[test = "v1"]), parse_quote!(#[test = "v2"])];
let result = parse_unique_attribute_value(&multi_attrs, "test");
assert!(result.is_err());
let invalid_attrs: Vec<Attribute> = vec![parse_quote!(#[test])];
let result = parse_unique_attribute_value(&invalid_attrs, "test");
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_attribute_ext_find_and_remove() {
use syn::{
ItemStruct,
LitStr,
parse::Parse,
parse_quote,
};
struct TestArgs {
value: LitStr,
}
impl Parse for TestArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(TestArgs {
value: input.parse()?,
})
}
}
let mut item: ItemStruct = parse_quote! {
#[test_attr("hello")]
#[derive(Debug)]
struct Foo;
};
let result = item.attrs.find_and_remove::<TestArgs>("test_attr").unwrap();
assert!(result.is_some());
let args = result.unwrap();
assert_eq!(args.value.value(), "hello");
assert_eq!(item.attrs.len(), 1);
assert!(!item.attrs.has_attribute("test_attr"));
assert!(item.attrs.has_attribute("derive"));
}
#[test]
fn test_attribute_ext_find_and_remove_not_found() {
use syn::{
ItemStruct,
LitStr,
parse::Parse,
parse_quote,
};
struct TestArgs {
_value: LitStr,
}
impl Parse for TestArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(TestArgs {
_value: input.parse()?,
})
}
}
let mut item: ItemStruct = parse_quote! {
#[derive(Debug)]
struct Foo;
};
let original_len = item.attrs.len();
let result = item.attrs.find_and_remove::<TestArgs>("nonexistent").unwrap();
assert!(result.is_none());
assert_eq!(item.attrs.len(), original_len);
}
#[test]
fn test_attribute_ext_find_and_remove_empty_attr() {
use syn::{
ItemStruct,
LitStr,
parse::Parse,
parse_quote,
};
struct TestArgs {
_value: LitStr,
}
impl Parse for TestArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(TestArgs {
_value: input.parse()?,
})
}
}
let mut item: ItemStruct = parse_quote! {
#[test_attr]
struct Foo;
};
let result = item.attrs.find_and_remove::<TestArgs>("test_attr").unwrap();
assert!(result.is_none());
assert_eq!(item.attrs.len(), 0);
}
#[test]
fn test_attribute_ext_find_value() {
use syn::{
ItemStruct,
parse_quote,
};
let item: ItemStruct = parse_quote! {
#[document_use = "SomeType"]
#[derive(Debug)]
struct Foo;
};
let result = item.attrs.find_value("document_use").unwrap();
assert_eq!(result, Some("SomeType".to_string()));
assert_eq!(item.attrs.len(), 2);
}
#[test]
fn test_attribute_ext_find_value_not_found() {
use syn::{
ItemStruct,
parse_quote,
};
let item: ItemStruct = parse_quote! {
#[derive(Debug)]
struct Foo;
};
let result = item.attrs.find_value("document_use").unwrap();
assert_eq!(result, None);
}
#[test]
fn test_attribute_ext_find_value_duplicate_error() {
use syn::{
ItemStruct,
parse_quote,
};
let item: ItemStruct = parse_quote! {
#[test = "v1"]
#[test = "v2"]
struct Foo;
};
let result = item.attrs.find_value("test");
assert!(result.is_err());
}
#[test]
fn test_attribute_ext_find_and_remove_value() {
use syn::{
ItemStruct,
parse_quote,
};
let mut item: ItemStruct = parse_quote! {
#[document_use = "SomeType"]
#[derive(Debug)]
struct Foo;
};
let result = item.attrs.find_and_remove_value("document_use").unwrap();
assert_eq!(result, Some("SomeType".to_string()));
assert_eq!(item.attrs.len(), 1);
assert!(!item.attrs.has_attribute("document_use"));
assert!(item.attrs.has_attribute("derive"));
}
#[test]
fn test_attribute_ext_find_and_remove_value_not_found() {
use syn::{
ItemStruct,
parse_quote,
};
let mut item: ItemStruct = parse_quote! {
#[derive(Debug)]
struct Foo;
};
let original_len = item.attrs.len();
let result = item.attrs.find_and_remove_value("document_use").unwrap();
assert_eq!(result, None);
assert_eq!(item.attrs.len(), original_len);
}
#[test]
fn test_attribute_ext_find_and_remove_value_not_name_value() {
use syn::{
ItemStruct,
parse_quote,
};
let mut item: ItemStruct = parse_quote! {
#[test_attr]
struct Foo;
};
let result = item.attrs.find_and_remove_value("test_attr").unwrap();
assert_eq!(result, None);
assert_eq!(item.attrs.len(), 0);
}
#[test]
fn test_attribute_ext_has_attribute() {
use syn::{
ItemStruct,
parse_quote,
};
let item: ItemStruct = parse_quote! {
#[derive(Debug)]
#[inline]
struct Foo;
};
assert!(item.attrs.has_attribute("derive"));
assert!(item.attrs.has_attribute("inline"));
assert!(!item.attrs.has_attribute("test"));
}
#[test]
fn test_attribute_ext_has_attribute_empty() {
use syn::{
ItemStruct,
parse_quote,
};
let item: ItemStruct = parse_quote! {
struct Foo;
};
assert!(!item.attrs.has_attribute("derive"));
}
}