use std::collections::HashSet;
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::ext::IdentExt;
use syn::parse::{ParseStream, Parser};
use syn::{
Attribute, Error, GenericArgument, GenericParam, Generics, Ident, ItemTrait, LitStr, Meta,
Path, PathArguments, Result, Token, Type, parse_quote,
};
use crate::sealed::{self, SealedType};
use crate::util::{argument, lifetimes, mentions, name_of, ours, rename, render, snake_case};
const ATTRS: &str = "attrs";
const CRATE: &str = "crate";
const MATCH_ANY: &str = "match_any";
const NAME: &str = "name";
const NO_BRIDGE: &str = "no_bridge";
const SKIP: &str = "skip";
const OWNED: &str = "owned";
const REF: &str = "ref";
const MUT: &str = "mut";
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Kind {
Owned,
Shared,
Unique,
}
impl Kind {
fn suffix(self) -> &'static str {
match self {
Kind::Owned => "",
Kind::Shared => "Ref",
Kind::Unique => "Mut",
}
}
fn macro_suffix(self) -> &'static str {
match self {
Kind::Owned => "",
Kind::Shared => "_ref",
Kind::Unique => "_mut",
}
}
}
pub(crate) struct Enumeration {
pub(crate) ident: Ident,
pub(crate) match_any: Option<Ident>,
pub(crate) no_bridge: bool,
pub(crate) attrs: Vec<Attribute>,
}
pub(crate) struct Input {
pub(crate) item: ItemTrait,
pub(crate) variants: Vec<Variant>,
pub(crate) enum_params: Option<TokenStream>,
pub(crate) enum_args: Option<TokenStream>,
pub(crate) owned: Option<Enumeration>,
pub(crate) shared: Option<Enumeration>,
pub(crate) unique: Option<Enumeration>,
pub(crate) krate: Path,
}
pub(crate) struct Variant {
pub(crate) ident: Ident,
pub(crate) ty: Type,
pub(crate) impl_params: Option<TokenStream>,
pub(crate) enum_args: Option<TokenStream>,
}
impl Input {
pub(crate) fn parse(args: TokenStream, item: ItemTrait) -> Result<Self> {
let args = parse_args(args)?;
repeated_attribute(&item)?;
let entries = sealed_types(&item)?
.into_iter()
.map(|entry| in_traits_terms(entry, &item))
.collect::<Result<Vec<_>>>()?;
let shared_params = enum_parameters(&item, &entries);
let variants = entries
.iter()
.map(|entry| variant(entry, &item, &shared_params))
.collect::<Result<Vec<_>>>()?;
let declarations = shared_params.iter().map(|param| quote!(#param));
let arguments = shared_params.iter().map(argument);
duplicate_variants(&variants)?;
let enum_ident = args
.grouped
.name
.clone()
.unwrap_or_else(|| format_ident!("Any{}", item.ident));
duplicate_conversions(&variants, &enum_ident)?;
let owned = args.resolve(Kind::Owned, &item);
let shared = args.resolve(Kind::Shared, &item);
let unique = args.resolve(Kind::Unique, &item);
if owned.is_none() && shared.is_none() && unique.is_none() {
return Err(Error::new(
Span::call_site(),
"every enum was skipped, which leaves `#[enumerate]` nothing to generate",
));
}
let wants_macro = [&owned, &shared, &unique]
.into_iter()
.flatten()
.any(|enumeration| enumeration.match_any.is_some());
if wants_macro {
dispatchable(&item, &entries, &shared_params)?;
}
Ok(Input {
enum_params: (!shared_params.is_empty()).then(|| quote!(<#(#declarations),*>)),
enum_args: (!shared_params.is_empty()).then(|| quote!(<#(#arguments),*>)),
owned,
shared,
unique,
item,
variants,
krate: args.krate.unwrap_or_else(|| parse_quote!(::closed_trait)),
})
}
}
#[derive(Default, Clone)]
struct Options {
skip: bool,
name: Option<Ident>,
match_any: Option<Option<Ident>>,
no_bridge: Option<Span>,
attrs: Option<Vec<Attribute>>,
}
impl Options {
fn over(&self, grouped: &Options) -> Options {
Options {
skip: self.skip,
name: self.name.clone(),
match_any: self.match_any.clone().or_else(|| grouped.match_any.clone()),
no_bridge: self.no_bridge.or(grouped.no_bridge),
attrs: self.attrs.clone(),
}
}
}
#[derive(Default)]
struct Args {
grouped: Options,
owned: Options,
shared: Options,
unique: Options,
krate: Option<Path>,
}
impl Args {
fn specific(&self, kind: Kind) -> &Options {
match kind {
Kind::Owned => &self.owned,
Kind::Shared => &self.shared,
Kind::Unique => &self.unique,
}
}
fn resolve(&self, kind: Kind, item: &ItemTrait) -> Option<Enumeration> {
let options = self.specific(kind).over(&self.grouped);
if options.skip {
return None;
}
let ident = options.name.clone().unwrap_or_else(|| {
let base = self
.grouped
.name
.clone()
.unwrap_or_else(|| format_ident!("Any{}", item.ident));
format_ident!("{base}{}", kind.suffix(), span = base.span())
});
let match_any = options.match_any.map(|_| {
if let Some(Some(named)) = &self.specific(kind).match_any {
return named.clone();
}
let base = self
.grouped
.match_any
.clone()
.flatten()
.unwrap_or_else(|| format_ident!("match_any_{}", snake_case(&item.ident)));
format_ident!("{base}{}", kind.macro_suffix(), span = base.span())
});
Some(Enumeration {
ident,
match_any,
no_bridge: options.no_bridge.is_some(),
attrs: options.attrs.unwrap_or_default(),
})
}
}
fn parse_args(args: TokenStream) -> Result<Args> {
let mut parsed = Args::default();
if args.is_empty() {
return Ok(parsed);
}
let mut groups = HashSet::new();
let parser = |stream: ParseStream| -> Result<()> {
while !stream.is_empty() {
let key = Ident::parse_any(stream)?;
let name = key.to_string();
match name.as_str() {
OWNED | REF | MUT => {
let inner;
syn::parenthesized!(inner in stream);
if !groups.insert(name.clone()) {
return Err(Error::new_spanned(
&key,
format!("duplicate `{name}(..)` group"),
));
}
let target = match name.as_str() {
OWNED => &mut parsed.owned,
REF => &mut parsed.shared,
_ => &mut parsed.unique,
};
parse_options(&inner, target, true)?;
}
CRATE => {
stream.parse::<Token![=]>()?;
if !stream.peek(LitStr) {
return Err(stream.error(format!(
r#"expected a string, as in `{CRATE} = "::my_reexport"`"#
)));
}
let literal = stream.parse::<LitStr>()?;
let value = literal.parse::<Path>().map_err(|_| {
Error::new_spanned(&literal, "expected a path to the `closed-trait` crate")
})?;
if parsed.krate.replace(value).is_some() {
return Err(duplicate(&key));
}
}
_ => option(&key, stream, &mut parsed.grouped, false)?,
}
if stream.is_empty() {
break;
}
stream.parse::<Token![,]>()?;
}
Ok(())
};
parser.parse2(args)?;
if let Some(span) = parsed.shared.no_bridge {
return Err(Error::new(
span,
format!(
"`{NO_BRIDGE}` has nothing to leave out here: no conversion is written on the \
borrowing enum's shared form.\nWrite it on `{OWNED}` to drop `as_ref` and \
`as_mut` from the owned enum, or on `{MUT}` to drop the reborrowing `as_ref`"
),
));
}
Ok(parsed)
}
fn parse_options(stream: ParseStream, options: &mut Options, in_group: bool) -> Result<()> {
while !stream.is_empty() {
let key = Ident::parse_any(stream)?;
option(&key, stream, options, in_group)?;
if stream.is_empty() {
break;
}
stream.parse::<Token![,]>()?;
}
Ok(())
}
fn duplicate(key: &Ident) -> Error {
Error::new_spanned(key, format!("duplicate `{key}` option"))
}
fn option(key: &Ident, stream: ParseStream, options: &mut Options, in_group: bool) -> Result<()> {
match key.to_string().as_str() {
SKIP if in_group => {
if options.skip {
return Err(duplicate(key));
}
options.skip = true;
}
MATCH_ANY => {
let named = if stream.peek(syn::token::Paren) {
let inner;
syn::parenthesized!(inner in stream);
if !inner.peek(LitStr) {
return Err(inner.error(format!(
r#"expected a string, as in `{MATCH_ANY}("match_shape")`"#
)));
}
let named = inner.parse::<LitStr>()?.parse::<Ident>()?;
if inner.peek(Token![,]) {
inner.parse::<Token![,]>()?;
}
if !inner.is_empty() {
return Err(inner.error(format!(
"`{MATCH_ANY}` takes one name, which every enum extends: \
`{OWNED}({MATCH_ANY}(..))` names the macro for one of them"
)));
}
Some(named)
} else {
None
};
if options.match_any.replace(named).is_some() {
return Err(duplicate(key));
}
}
NO_BRIDGE => {
if options.no_bridge.replace(key.span()).is_some() {
return Err(duplicate(key));
}
}
NAME => {
stream.parse::<Token![=]>()?;
if !stream.peek(LitStr) {
return Err(
stream.error(format!(r#"expected a string, as in `{NAME} = "Shapes"`"#))
);
}
let value = stream.parse::<LitStr>()?.parse::<Ident>()?;
if options.name.replace(value).is_some() {
return Err(duplicate(key));
}
}
ATTRS if !in_group => {
return Err(Error::new_spanned(
key,
format!(
r#"`{ATTRS}` applies to one enum at a time, as in `{OWNED}({ATTRS} = "..")`"#
),
));
}
ATTRS => {
stream.parse::<Token![=]>()?;
if !stream.peek(LitStr) {
return Err(stream.error(format!(
r##"expected a string of attributes, as in `{ATTRS} = "#[derive(Debug)]"`"##
)));
}
let literal = stream.parse::<LitStr>()?;
let attrs = literal.parse_with(Attribute::parse_outer)?;
if let Some(doc) = attrs.iter().find(|attr| attr.path().is_ident("doc")) {
return Err(Error::new_spanned(
doc,
format!(
"`{ATTRS}` cannot document the enum: its documentation is generated \
and is the same for every sealed trait"
),
));
}
if options.attrs.replace(attrs).is_some() {
return Err(duplicate(key));
}
}
unknown => {
let where_ = if in_group {
format!("expected `{SKIP}`, `{NAME}`, `{MATCH_ANY}`, `{NO_BRIDGE}` or `{ATTRS}`")
} else {
format!(
"expected `{OWNED}`, `{REF}`, `{MUT}`, `{NAME}`, `{MATCH_ANY}`, \
`{NO_BRIDGE}` or `{CRATE}`"
)
};
return Err(Error::new_spanned(
key,
format!("unknown option `{unknown}`, {where_}"),
));
}
}
Ok(())
}
fn repeated_attribute(item: &ItemTrait) -> Result<()> {
match item.attrs.iter().find(|attr| ours(attr, "enumerate")) {
Some(attr) => Err(Error::new_spanned(
attr,
format!(
"`#[{}]` is written twice, and each one generates the enums.\nWrite it once, \
above the `#[sealed(..)]` it reads",
render(attr.path()),
),
)),
None => Ok(()),
}
}
fn sealed_types(item: &ItemTrait) -> Result<Vec<SealedType>> {
let candidates: Vec<_> = item
.attrs
.iter()
.filter(|attr| {
attr.path()
.segments
.last()
.is_some_and(|segment| segment.ident == "sealed")
})
.collect();
let mut seen = HashSet::new();
for attr in &candidates {
let path = render(attr.path());
if !seen.insert(path.clone()) {
return Err(Error::new_spanned(
attr,
format!(
"`#[{path}(..)]` is written twice, and a trait is sealed to one list.\nWrite \
one attribute listing every permitted type"
),
));
}
}
let bare = candidates
.iter()
.find(|attr| attr.path().is_ident("sealed"));
let chosen = match (bare, candidates.as_slice()) {
(Some(attr), _) => Some(*attr),
(None, [only]) => Some(*only),
(None, several) => several
.iter()
.copied()
.find(|attr| parse_sealed(attr).is_ok()),
};
let attr = chosen.ok_or_else(|| {
Error::new_spanned(
&item.ident,
"`#[enumerate]` needs a `#[sealed(..)]` attribute written below it, \
to know which types the enum should hold",
)
})?;
match parse_sealed(attr)? {
types if types.is_empty() => Err(Error::new_spanned(
attr,
"`#[enumerate]` needs at least one type to make an enum from, and this \
`#[sealed(..)]` lists none",
)),
types => Ok(types),
}
}
fn parse_sealed(attr: &Attribute) -> Result<Vec<SealedType>> {
let tokens = match &attr.meta {
Meta::Path(_) => TokenStream::new(),
meta => meta.require_list()?.tokens.clone(),
};
Ok(sealed::Args::parse(tokens)?.types)
}
fn enum_parameters(item: &ItemTrait, entries: &[SealedType]) -> Vec<GenericParam> {
item.generics
.params
.iter()
.filter(|param| entries.iter().any(|entry| uses(&entry.ty, param)))
.cloned()
.collect()
}
fn bounded(param: &GenericParam, entry: &SealedType) -> GenericParam {
let name = name_of(param);
let bound = entry
.binder
.iter()
.flat_map(|binder| binder.params.iter())
.find(|bound| name_of(bound) == name);
match (param.clone(), bound) {
(GenericParam::Type(mut param), Some(GenericParam::Type(bound))) => {
let known: Vec<String> = param.bounds.iter().map(render).collect();
let added = bound
.bounds
.iter()
.filter(|bound| !known.contains(&render(bound)))
.cloned()
.collect::<Vec<_>>();
param.bounds.extend(added);
GenericParam::Type(param)
}
(GenericParam::Lifetime(mut param), Some(GenericParam::Lifetime(bound))) => {
let known: Vec<String> = param.bounds.iter().map(render).collect();
let added = bound
.bounds
.iter()
.filter(|bound| !known.contains(&render(bound)))
.cloned()
.collect::<Vec<_>>();
param.bounds.extend(added);
GenericParam::Lifetime(param)
}
(param, _) => param,
}
}
fn uses(ty: &impl ToTokens, param: &GenericParam) -> bool {
match param {
GenericParam::Lifetime(param) => lifetimes(ty).contains(¶m.lifetime.ident),
GenericParam::Type(param) => mentions(ty, ¶m.ident.to_string()),
GenericParam::Const(param) => mentions(ty, ¶m.ident.to_string()),
}
}
fn in_traits_terms(entry: SealedType, item: &ItemTrait) -> Result<SealedType> {
let Some(binder) = &entry.binder else {
return Ok(entry);
};
let arguments = entry
.instantiation
.as_ref()
.map(instantiation_arguments)
.unwrap_or_default();
let renames: Vec<(String, TokenStream)> = item
.generics
.params
.iter()
.zip(arguments)
.filter_map(|(param, given)| {
let given = render(&given);
let bound = binder
.params
.iter()
.find(|bound| name_of_argument(bound) == given)?;
Some((name_of_argument(bound), argument(param)))
})
.collect();
for bound in &binder.params {
let name = name_of_argument(bound);
let used = match bound {
GenericParam::Lifetime(bound) => lifetimes(&entry.ty)
.iter()
.any(|found| found == &bound.lifetime.ident),
other => mentions(&entry.ty, &name_of(other)),
};
if !used || renames.iter().any(|(from, _)| from == &name) {
continue;
}
let remedy = match item.generics.params.is_empty() {
true => format!(
"Declare `{name}` on `{}` itself, or remove `#[enumerate]`",
item.ident
),
false => format!(
"Write `: {}<..>` with `{name}` where that parameter goes, or remove \
`#[enumerate]`",
item.ident,
),
};
return Err(Error::new_spanned(
&entry.ty,
format!(
"`#[enumerate]` cannot hold `{}`: `{name}` is bound by the `for<..>` and the \
instantiation does not say which of `{}`'s parameters it stands for, so the \
generated enum could not be named in its supertrait bound.\n{remedy}",
render(&entry.ty),
item.ident,
),
));
}
let instantiation = match &entry.instantiation {
Some(path) => Some(syn::parse2(rename(path, &renames))?),
None => None,
};
let params = binder
.params
.iter()
.map(|param| syn::parse2::<GenericParam>(rename(param, &renames)))
.collect::<Result<_>>()?;
let binder = Generics {
params,
..binder.clone()
};
Ok(SealedType {
ty: syn::parse2(rename(&entry.ty, &renames))?,
binder: Some(binder),
instantiation,
..entry
})
}
fn instantiation_arguments(path: &Path) -> Vec<GenericArgument> {
match path.segments.last().map(|segment| &segment.arguments) {
Some(PathArguments::AngleBracketed(arguments)) => arguments.args.iter().cloned().collect(),
_ => Vec::new(),
}
}
fn variant(entry: &SealedType, item: &ItemTrait, shared: &[GenericParam]) -> Result<Variant> {
let ty = &entry.ty;
let declared: Vec<GenericParam> = item
.generics
.params
.iter()
.filter(|param| {
uses(ty, param)
|| entry
.instantiation
.as_ref()
.is_some_and(|path| uses(path, param))
})
.map(|param| bounded(param, entry))
.collect();
let free: Vec<_> = lifetimes(ty)
.into_iter()
.filter(|name| {
!item
.generics
.lifetimes()
.any(|param| ¶m.lifetime.ident == name)
})
.collect();
if let Some(lifetime) = free.first() {
return Err(Error::new_spanned(
ty,
format!(
"`#[enumerate]` cannot hold `{}`: `'{}` is not a parameter of `{}`, so the \
generated enum could not be named in its supertrait bound.\nDeclare `'{}` on \
`{}` itself, or remove `#[enumerate]`",
render(ty),
lifetime,
item.ident,
lifetime,
item.ident,
),
));
}
let arguments = enum_arguments(entry, item, shared, &declared)?;
let declarations = declared.iter().map(|param| quote!(#param));
let ident = match &entry.alias {
Some(alias) => alias.clone(),
None => {
let Type::Path(path) = ty else {
return Err(Error::new_spanned(
ty,
"`#[enumerate]` needs each sealed type to be a path so the variant can be \
named after it, or an explicit `as Name`",
));
};
path.path
.segments
.last()
.map(|segment| segment.ident.clone())
.ok_or_else(|| {
Error::new_spanned(ty, "expected a path with at least one segment")
})?
}
};
Ok(Variant {
ident,
ty: ty.clone(),
impl_params: (!declared.is_empty()).then(|| quote!(<#(#declarations),*>)),
enum_args: arguments,
})
}
fn enum_arguments(
entry: &SealedType,
item: &ItemTrait,
shared: &[GenericParam],
declared: &[GenericParam],
) -> Result<Option<TokenStream>> {
if shared.is_empty() {
return Ok(None);
}
let fixed = entry.instantiation.as_ref().map(|path| {
let arguments = match path.segments.last().map(|segment| &segment.arguments) {
Some(PathArguments::AngleBracketed(arguments)) => {
arguments.args.iter().cloned().collect::<Vec<_>>()
}
_ => Vec::new(),
};
item.generics
.params
.iter()
.zip(arguments)
.map(|(param, argument)| (name_of(param), quote!(#argument)))
.collect::<Vec<_>>()
});
let mut arguments = Vec::new();
for param in shared {
let name = name_of(param);
let annotated = match &fixed {
Some(fixed) => fixed
.iter()
.find(|(fixed, _)| fixed == &name)
.map(|(_, tokens)| tokens.clone()),
None => None,
};
let chosen = match annotated {
Some(annotated) => annotated,
None if declared.iter().any(|known| name_of(known) == name) => argument(param),
None => return Err(sealed::needs_instantiation(&entry.ty, item)),
};
arguments.push(chosen);
}
Ok(Some(quote!(<#(#arguments),*>)))
}
fn name_of_argument(param: &GenericParam) -> String {
match param {
GenericParam::Lifetime(param) => format!("'{}", param.lifetime.ident),
other => name_of(other),
}
}
fn dispatchable(item: &ItemTrait, entries: &[SealedType], shared: &[GenericParam]) -> Result<()> {
if let Some(unused) = item
.generics
.params
.iter()
.find(|param| !shared.iter().any(|used| name_of(used) == name_of(param)))
{
return Err(Error::new_spanned(
unused,
format!(
"`#[enumerate({MATCH_ANY})]` needs every parameter of `{}` to appear in the \
sealed types, and `{}` appears in none of them, so the match has nothing to \
name.\nUse it in a sealed type, or drop the option",
item.ident,
name_of(unused),
),
));
}
let wanted = render(&trait_bound(item, shared));
let pinned = entries.iter().find_map(|entry| {
let instantiation = entry.instantiation.as_ref()?;
(render(instantiation) != wanted).then_some((entry, instantiation))
});
if let Some((entry, instantiation)) = pinned {
return Err(Error::new_spanned(
&entry.ty,
format!(
"`#[enumerate({MATCH_ANY})]` cannot match `{ty}`: it implements `{had}`, not \
`{want}`, so it is not a variant of every `{want}`.\nDrop the `{MATCH_ANY}` \
option, or make `{ty}` generic over the same parameters as `{trait_}`",
ty = render(&entry.ty),
had = render(instantiation),
want = wanted,
trait_ = item.ident,
),
));
}
Ok(())
}
pub(crate) fn trait_bound(item: &ItemTrait, shared: &[GenericParam]) -> TokenStream {
let ident = &item.ident;
let arguments = shared.iter().map(argument);
let arguments = (!shared.is_empty()).then(|| quote!(<#(#arguments),*>));
quote!(#ident #arguments)
}
fn duplicate_conversions(variants: &[Variant], enum_ident: &Ident) -> Result<()> {
let key = |variant: &Variant| {
let ty = &variant.ty;
let args = &variant.enum_args;
render("e!(#ty #args))
};
for (index, variant) in variants.iter().enumerate() {
if variants[..index]
.iter()
.any(|earlier| key(earlier) == key(variant))
{
let ty = render(&variant.ty);
let args = variant.enum_args.as_ref().map(render).unwrap_or_default();
return Err(Error::new_spanned(
&variant.ty,
format!(
"`{ty}` is listed twice for the same `{enum_ident}{args}`, so `into_enum` \
would have two answers.\nEntries may share a type only when they pin \
different arguments, which needs the enum to stay generic: some entry has \
to name the parameter rather than fixing it"
),
));
}
}
Ok(())
}
fn duplicate_variants(variants: &[Variant]) -> Result<()> {
for (index, variant) in variants.iter().enumerate() {
if let Some(earlier) = variants[..index]
.iter()
.find(|earlier| earlier.ident == variant.ident)
{
return Err(Error::new_spanned(
&variant.ty,
format!(
"`{}` and `{}` would both become the `{}` variant, since a variant is named \
after the type's last path segment.\nGive one an explicit name: \
`{} as SomeName`",
render(&earlier.ty),
render(&variant.ty),
variant.ident,
render(&variant.ty),
),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn resolved(attr: TokenStream) -> [Option<Enumeration>; 3] {
let args = parse_args(attr).expect("the attribute parses");
let item: ItemTrait = parse_quote!(
pub trait Shape {}
);
[
args.resolve(Kind::Owned, &item),
args.resolve(Kind::Shared, &item),
args.resolve(Kind::Unique, &item),
]
}
fn names(attr: TokenStream) -> Vec<Option<String>> {
resolved(attr)
.iter()
.map(|kind| kind.as_ref().map(|kind| kind.ident.to_string()))
.collect()
}
fn macros(attr: TokenStream) -> Vec<Option<String>> {
resolved(attr)
.iter()
.map(|kind| {
kind.as_ref()
.and_then(|kind| kind.match_any.as_ref())
.map(|name| name.to_string())
})
.collect()
}
fn bridges(attr: TokenStream) -> Vec<Option<bool>> {
resolved(attr)
.iter()
.map(|kind| kind.as_ref().map(|kind| !kind.no_bridge))
.collect()
}
#[test]
fn bare_gives_all_three_and_no_macros() {
assert_eq!(
names(quote!()),
vec![
Some("AnyShape".to_owned()),
Some("AnyShapeRef".to_owned()),
Some("AnyShapeMut".to_owned()),
]
);
assert_eq!(macros(quote!()), vec![None, None, None]);
assert_eq!(bridges(quote!()), vec![Some(true); 3]);
}
#[test]
fn a_grouped_name_is_a_base_each_kind_extends() {
assert_eq!(
names(quote!(name = "Shapes")),
vec![
Some("Shapes".to_owned()),
Some("ShapesRef".to_owned()),
Some("ShapesMut".to_owned()),
]
);
}
#[test]
fn a_specific_name_is_the_name_itself() {
assert_eq!(
names(quote!(name = "Shapes", ref(name = "View"))),
vec![
Some("Shapes".to_owned()),
Some("View".to_owned()),
Some("ShapesMut".to_owned()),
]
);
}
#[test]
fn a_grouped_macro_name_is_a_base_too() {
assert_eq!(
macros(quote!(match_any)),
vec![
Some("match_any_shape".to_owned()),
Some("match_any_shape_ref".to_owned()),
Some("match_any_shape_mut".to_owned()),
]
);
assert_eq!(
macros(quote!(match_any("walk"))),
vec![
Some("walk".to_owned()),
Some("walk_ref".to_owned()),
Some("walk_mut".to_owned()),
]
);
}
#[test]
fn a_specific_macro_name_overrides_just_that_one() {
assert_eq!(
macros(quote!(match_any, mut(match_any("walk")))),
vec![
Some("match_any_shape".to_owned()),
Some("match_any_shape_ref".to_owned()),
Some("walk".to_owned()),
]
);
}
#[test]
fn a_macro_asked_for_in_one_group_only_reaches_that_one() {
assert_eq!(
macros(quote!(ref(match_any))),
vec![None, Some("match_any_shape_ref".to_owned()), None]
);
}
#[test]
fn skip_drops_only_its_own_kind() {
assert_eq!(
names(quote!(ref(skip))),
vec![
Some("AnyShape".to_owned()),
None,
Some("AnyShapeMut".to_owned()),
]
);
assert_eq!(
names(quote!(owned(skip), mut(skip))),
vec![None, Some("AnyShapeRef".to_owned()), None]
);
}
#[test]
fn no_bridge_applies_to_the_enum_it_is_written_on() {
assert_eq!(bridges(quote!(no_bridge)), vec![Some(false); 3]);
assert_eq!(
bridges(quote!(owned(no_bridge))),
vec![Some(false), Some(true), Some(true)]
);
assert_eq!(
bridges(quote!(mut(no_bridge))),
vec![Some(true), Some(true), Some(false)]
);
}
#[test]
fn no_bridge_on_ref_is_refused() {
assert!(refused(quote!(ref(no_bridge))).contains("nothing to leave out"));
}
#[test]
fn a_grouped_no_bridge_cannot_be_undone_by_a_group() {
assert_eq!(
bridges(quote!(no_bridge, ref(name = "View"))),
vec![Some(false); 3]
);
}
#[test]
fn attrs_reach_only_the_group_they_are_written_in() {
let resolved = resolved(quote!(ref(attrs = "#[derive(Debug)]")));
assert!(resolved[0].as_ref().expect("owned").attrs.is_empty());
assert_eq!(resolved[1].as_ref().expect("ref").attrs.len(), 1);
assert!(resolved[2].as_ref().expect("mut").attrs.is_empty());
}
fn refused(attr: TokenStream) -> String {
match parse_args(attr) {
Err(error) => error.to_string(),
Ok(_) => panic!("expected the attribute to be refused"),
}
}
#[test]
fn every_option_forbids_duplicates() {
let cases = [
(quote!(name = "A", name = "B"), "name"),
(quote!(crate = "::a", crate = "::b"), "crate"),
(quote!(match_any("a"), match_any("b")), "match_any"),
(quote!(no_bridge, no_bridge), "no_bridge"),
(quote!(owned(skip, skip)), "skip"),
(
quote!(owned(
attrs = "#[derive(Debug)]",
attrs = "#[non_exhaustive]"
)),
"attrs",
),
];
for (attr, option) in cases {
let message = refused(attr);
assert!(
message.contains(&format!("duplicate `{option}`")),
"`{option}` written twice gave {message}"
);
}
}
#[test]
fn every_group_forbids_duplicates() {
for group in ["owned", "ref", "mut"] {
let attr: TokenStream = format!("{group}(no_bridge), {group}(name = \"A\")")
.parse()
.expect("the options parse");
let message = refused(attr);
assert!(
message.contains(&format!("duplicate `{group}(..)` group")),
"`{group}` written twice gave {message}"
);
}
}
#[test]
fn a_second_enumerate_attribute_is_refused() {
let refused = |item: ItemTrait| match Input::parse(TokenStream::new(), item) {
Err(error) => error.to_string(),
Ok(_) => panic!("expected the attribute to be refused"),
};
let message = refused(parse_quote!(
#[enumerate]
#[sealed(Square)]
pub trait Shape {}
));
assert!(message.contains("is written twice"), "{message}");
let message = refused(parse_quote!(
#[closed_trait::enumerate]
#[sealed(Square)]
pub trait Shape {}
));
assert!(message.contains("is written twice"), "{message}");
}
#[test]
fn another_crates_enumerate_is_left_alone() {
let item: ItemTrait = parse_quote!(
#[other::enumerate]
#[sealed(Square)]
pub trait Shape {}
);
assert!(Input::parse(TokenStream::new(), item).is_ok());
}
#[test]
fn a_trailing_comma_is_accepted_everywhere() {
assert_eq!(
names(quote!(name = "Shapes", ref(name = "View",),)),
names(quote!(name = "Shapes", ref(name = "View")))
);
assert_eq!(
macros(quote!(match_any("walk",),)),
macros(quote!(match_any("walk")))
);
assert!(parse_args(quote!(owned(skip,),)).is_ok());
}
#[test]
fn match_any_takes_one_name() {
let message = refused(quote!(match_any("one", "two")));
assert!(message.contains("takes one name"), "{message}");
}
#[test]
fn bare_attrs_is_refused() {
assert!(refused(quote!(attrs = "#[derive(Debug)]")).contains("one enum at a time"));
}
#[test]
fn skip_is_refused_outside_a_group() {
assert!(refused(quote!(skip)).contains("unknown option `skip`"));
}
#[test]
fn a_bare_name_is_refused() {
assert!(refused(quote!(name = Shapes)).contains(r#"`name = "Shapes"`"#));
assert!(refused(quote!(ref(name = View))).contains(r#"`name = "Shapes"`"#));
assert!(refused(quote!(match_any(walk))).contains(r#"`match_any("match_shape")`"#));
}
}