use itertools::Itertools;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::collections::{HashMap, HashSet};
use syn::{
spanned::Spanned, Attribute, Ident, Meta, MetaList, NestedMeta, Result, Signature, UseTree,
};
use syn_ext::{
ext::{AttributeExt as SynAttributeExt, *},
types::PunctuatedNestedMeta,
};
pub(crate) const ALL_ALLOWED_NAMES: &[&str] = &[
"pymethod",
"pyclassmethod",
"pystaticmethod",
"pygetset",
"pyfunction",
"pyclass",
"pyexception",
"pystruct_sequence",
"pyattr",
"pyslot",
"extend_class",
"pymember",
];
#[derive(Clone)]
struct NurseryItem {
attr_name: Ident,
py_names: Vec<String>,
cfgs: Vec<Attribute>,
tokens: TokenStream,
sort_order: usize,
}
#[derive(Default)]
pub(crate) struct ItemNursery(Vec<NurseryItem>);
pub(crate) struct ValidatedItemNursery(ItemNursery);
impl ItemNursery {
pub fn add_item(
&mut self,
attr_name: Ident,
py_names: Vec<String>,
cfgs: Vec<Attribute>,
tokens: TokenStream,
sort_order: usize,
) -> Result<()> {
self.0.push(NurseryItem {
attr_name,
py_names,
cfgs,
tokens,
sort_order,
});
Ok(())
}
pub fn validate(self) -> Result<ValidatedItemNursery> {
let mut by_name: HashSet<(String, Vec<Attribute>)> = HashSet::new();
for item in &self.0 {
for py_name in &item.py_names {
let inserted = by_name.insert((py_name.clone(), item.cfgs.clone()));
if !inserted {
return Err(syn::Error::new(
item.attr_name.span(),
format!("Duplicated #[py*] attribute found for {:?}", &item.py_names),
));
}
}
}
Ok(ValidatedItemNursery(self))
}
}
impl ToTokens for ValidatedItemNursery {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut sorted = self.0 .0.clone();
sorted.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
tokens.extend(sorted.iter().map(|item| {
let cfgs = &item.cfgs;
let tokens = &item.tokens;
quote! {
#( #cfgs )*
{
#tokens
}
}
}))
}
}
#[derive(Clone)]
pub(crate) struct ContentItemInner<T> {
pub index: usize,
pub attr_name: T,
}
pub(crate) trait ContentItem {
type AttrName: std::str::FromStr + std::fmt::Display;
fn inner(&self) -> &ContentItemInner<Self::AttrName>;
fn index(&self) -> usize {
self.inner().index
}
fn attr_name(&self) -> &Self::AttrName {
&self.inner().attr_name
}
fn new_syn_error(&self, span: Span, message: &str) -> syn::Error {
syn::Error::new(span, format!("#[{}] {}", self.attr_name(), message))
}
}
pub(crate) struct ItemMetaInner {
pub item_ident: Ident,
pub meta_ident: Ident,
pub meta_map: HashMap<String, (usize, Meta)>,
}
impl ItemMetaInner {
pub fn from_nested<I>(
item_ident: Ident,
meta_ident: Ident,
nested: I,
allowed_names: &[&'static str],
) -> Result<Self>
where
I: std::iter::Iterator<Item = NestedMeta>,
{
let (meta_map, lits) = nested.into_unique_map_and_lits(|path| {
if let Some(ident) = path.get_ident() {
let name = ident.to_string();
if allowed_names.contains(&name.as_str()) {
Ok(Some(name))
} else {
Err(err_span!(
ident,
"#[{meta_ident}({name})] is not one of allowed attributes [{}]",
allowed_names.iter().format(", ")
))
}
} else {
Ok(None)
}
})?;
if !lits.is_empty() {
bail_span!(meta_ident, "#[{meta_ident}(..)] cannot contain literal")
}
Ok(Self {
item_ident,
meta_ident,
meta_map,
})
}
pub fn item_name(&self) -> String {
self.item_ident.to_string()
}
pub fn meta_name(&self) -> String {
self.meta_ident.to_string()
}
pub fn _optional_str(&self, key: &str) -> Result<Option<String>> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
let Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) = meta
else {
bail_span!(
meta,
"#[{}({} = ...)] must exist as a string",
self.meta_name(),
key
)
};
Some(lit.value())
} else {
None
};
Ok(value)
}
pub fn _has_key(&self, key: &str) -> Result<bool> {
Ok(matches!(self.meta_map.get(key), Some((_, _))))
}
pub fn _bool(&self, key: &str) -> Result<bool> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Bool(lit),
..
}) => lit.value,
Meta::Path(_) => true,
_ => bail_span!(meta, "#[{}({})] is expected", self.meta_name(), key),
}
} else {
false
};
Ok(value)
}
pub fn _optional_list(
&self,
key: &str,
) -> Result<Option<impl std::iter::Iterator<Item = &'_ NestedMeta>>> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
let Meta::List(syn::MetaList {
path: _, nested, ..
}) = meta
else {
bail_span!(meta, "#[{}({}(...))] must be a list", self.meta_name(), key)
};
Some(nested.into_iter())
} else {
None
};
Ok(value)
}
}
pub(crate) trait ItemMeta: Sized {
const ALLOWED_NAMES: &'static [&'static str];
fn from_attr(item_ident: Ident, attr: &Attribute) -> Result<Self> {
let (meta_ident, nested) = attr.ident_and_promoted_nested()?;
Self::from_nested(item_ident, meta_ident.clone(), nested.into_iter())
}
fn from_nested<I>(item_ident: Ident, meta_ident: Ident, nested: I) -> Result<Self>
where
I: std::iter::Iterator<Item = NestedMeta>,
{
Ok(Self::from_inner(ItemMetaInner::from_nested(
item_ident,
meta_ident,
nested,
Self::ALLOWED_NAMES,
)?))
}
fn from_inner(inner: ItemMetaInner) -> Self;
fn inner(&self) -> &ItemMetaInner;
fn simple_name(&self) -> Result<String> {
let inner = self.inner();
Ok(inner
._optional_str("name")?
.unwrap_or_else(|| inner.item_name()))
}
fn optional_name(&self) -> Option<String> {
self.inner()._optional_str("name").ok().flatten()
}
fn new_meta_error(&self, msg: &str) -> syn::Error {
let inner = self.inner();
err_span!(inner.meta_ident, "#[{}] {}", inner.meta_name(), msg)
}
}
pub(crate) struct SimpleItemMeta(pub ItemMetaInner);
impl ItemMeta for SimpleItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
pub(crate) struct ModuleItemMeta(pub ItemMetaInner);
impl ItemMeta for ModuleItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "with", "sub"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
impl ModuleItemMeta {
pub fn sub(&self) -> Result<bool> {
self.inner()._bool("sub")
}
pub fn with(&self) -> Result<Vec<&syn::Path>> {
let mut withs = Vec::new();
let Some(nested) = self.inner()._optional_list("with")? else {
return Ok(withs);
};
for meta in nested {
let NestedMeta::Meta(Meta::Path(path)) = meta else {
bail_span!(meta, "#[pymodule(with(...))] arguments should be paths")
};
withs.push(path);
}
Ok(withs)
}
}
pub(crate) struct AttrItemMeta(pub ItemMetaInner);
impl ItemMeta for AttrItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "once"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
pub(crate) struct ClassItemMeta(ItemMetaInner);
impl ItemMeta for ClassItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &[
"module",
"name",
"base",
"metaclass",
"unhashable",
"ctx",
"impl",
"traverse",
];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
impl ClassItemMeta {
pub fn class_name(&self) -> Result<String> {
const KEY: &str = "name";
let inner = self.inner();
if let Some((_, meta)) = inner.meta_map.get(KEY) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => return Ok(lit.value()),
Meta::Path(_) => return Ok(inner.item_name()),
_ => {}
}
}
bail_span!(
inner.meta_ident,
"#[{attr_name}(name = ...)] must exist as a string. Try \
#[{attr_name}(name)] to use rust type name.",
attr_name = inner.meta_name()
)
}
pub fn ctx_name(&self) -> Result<Option<String>> {
self.inner()._optional_str("ctx")
}
pub fn base(&self) -> Result<Option<String>> {
self.inner()._optional_str("base")
}
pub fn unhashable(&self) -> Result<bool> {
self.inner()._bool("unhashable")
}
pub fn metaclass(&self) -> Result<Option<String>> {
self.inner()._optional_str("metaclass")
}
pub fn module(&self) -> Result<Option<String>> {
const KEY: &str = "module";
let inner = self.inner();
let value = if let Some((_, meta)) = inner.meta_map.get(KEY) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => Ok(Some(lit.value())),
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Bool(lit),
..
}) => if lit.value {
Err(lit.span())
} else {
Ok(None)
}
other => Err(other.span()),
}
} else {
Err(inner.item_ident.span())
}.map_err(|span| syn::Error::new(
span,
format!(
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module = false)] for built-in types.",
attr_name=inner.meta_name()
),
))?;
Ok(value)
}
pub fn impl_attrs(&self) -> Result<Option<String>> {
self.inner()._optional_str("impl")
}
}
pub(crate) struct ExceptionItemMeta(ClassItemMeta);
impl ItemMeta for ExceptionItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "base", "unhashable", "ctx", "impl"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(ClassItemMeta(inner))
}
fn inner(&self) -> &ItemMetaInner {
&self.0 .0
}
}
impl ExceptionItemMeta {
pub fn class_name(&self) -> Result<String> {
const KEY: &str = "name";
let inner = self.inner();
if let Some((_, meta)) = inner.meta_map.get(KEY) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => return Ok(lit.value()),
Meta::Path(_) => {
return Ok({
let type_name = inner.item_name();
let Some(py_name) = type_name.as_str().strip_prefix("Py") else {
bail_span!(
inner.item_ident,
"#[pyexception] expects its underlying type to be named `Py` prefixed"
)
};
py_name.to_string()
})
}
_ => {}
}
}
bail_span!(
inner.meta_ident,
"#[{attr_name}(name = ...)] must exist as a string. Try \
#[{attr_name}(name)] to use rust type name.",
attr_name = inner.meta_name()
)
}
pub fn has_impl(&self) -> Result<bool> {
self.inner()._bool("impl")
}
}
impl std::ops::Deref for ExceptionItemMeta {
type Target = ClassItemMeta;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub(crate) trait AttributeExt: SynAttributeExt {
fn promoted_nested(&self) -> Result<PunctuatedNestedMeta>;
fn ident_and_promoted_nested(&self) -> Result<(&Ident, PunctuatedNestedMeta)>;
fn try_remove_name(&mut self, name: &str) -> Result<Option<syn::NestedMeta>>;
fn fill_nested_meta<F>(&mut self, name: &str, new_item: F) -> Result<()>
where
F: Fn() -> NestedMeta;
}
impl AttributeExt for Attribute {
fn promoted_nested(&self) -> Result<PunctuatedNestedMeta> {
let list = self.promoted_list().map_err(|mut e| {
let name = self.get_ident().unwrap().to_string();
e.combine(err_span!(
self,
"#[{name} = \"...\"] cannot be a name/value, you probably meant \
#[{name}(name = \"...\")]",
));
e
})?;
Ok(list.nested)
}
fn ident_and_promoted_nested(&self) -> Result<(&Ident, PunctuatedNestedMeta)> {
Ok((self.get_ident().unwrap(), self.promoted_nested()?))
}
fn try_remove_name(&mut self, item_name: &str) -> Result<Option<syn::NestedMeta>> {
self.try_meta_mut(|meta| {
let nested = match meta {
Meta::List(MetaList { ref mut nested, .. }) => Ok(nested),
other => Err(syn::Error::new(
other.span(),
format!(
"#[{name}(...)] doesn't contain '{item}' to remove",
name = other.get_ident().unwrap(),
item = item_name
),
)),
}?;
let mut found = None;
for (i, item) in nested.iter().enumerate() {
let ident = if let Some(ident) = item.get_ident() {
ident
} else {
continue;
};
if *ident != item_name {
continue;
}
if found.is_some() {
return Err(syn::Error::new(
item.span(),
format!("#[py..({item_name}...)] must be unique but found multiple times"),
));
}
found = Some(i);
}
Ok(found.map(|idx| nested.remove(idx).into_value()))
})
}
fn fill_nested_meta<F>(&mut self, name: &str, new_item: F) -> Result<()>
where
F: Fn() -> NestedMeta,
{
self.try_meta_mut(|meta| {
let list = meta.promote_to_list(Default::default())?;
let has_name = list
.nested
.iter()
.any(|nested_meta| nested_meta.get_path().map_or(false, |p| p.is_ident(name)));
if !has_name {
list.nested.push(new_item())
}
Ok(())
})
}
}
pub(crate) fn pyclass_ident_and_attrs(item: &syn::Item) -> Result<(&Ident, &[Attribute])> {
use syn::Item::*;
Ok(match item {
Struct(syn::ItemStruct { ident, attrs, .. }) => (ident, attrs),
Enum(syn::ItemEnum { ident, attrs, .. }) => (ident, attrs),
Use(item_use) => (
iter_use_idents(item_use, |ident, _is_unique| Ok(ident))?
.into_iter()
.exactly_one()
.map_err(|_| {
err_span!(
item_use,
"#[pyclass] can only be on single name use statement",
)
})?,
&item_use.attrs,
),
other => {
bail_span!(
other,
"#[pyclass] can only be on a struct, enum or use declaration",
)
}
})
}
pub(crate) fn pyexception_ident_and_attrs(item: &syn::Item) -> Result<(&Ident, &[Attribute])> {
use syn::Item::*;
Ok(match item {
Struct(syn::ItemStruct { ident, attrs, .. }) => (ident, attrs),
Enum(syn::ItemEnum { ident, attrs, .. }) => (ident, attrs),
other => {
bail_span!(other, "#[pyexception] can only be on a struct or enum",)
}
})
}
pub(crate) trait ErrorVec: Sized {
fn into_error(self) -> Option<syn::Error>;
fn into_result(self) -> Result<()> {
if let Some(error) = self.into_error() {
Err(error)
} else {
Ok(())
}
}
fn ok_or_push<T>(&mut self, r: Result<T>) -> Option<T>;
}
impl ErrorVec for Vec<syn::Error> {
fn into_error(self) -> Option<syn::Error> {
let mut iter = self.into_iter();
if let Some(mut first) = iter.next() {
for err in iter {
first.combine(err);
}
Some(first)
} else {
None
}
}
fn ok_or_push<T>(&mut self, r: Result<T>) -> Option<T> {
match r {
Ok(v) => Some(v),
Err(e) => {
self.push(e);
None
}
}
}
}
macro_rules! iter_chain {
($($it:expr),*$(,)?) => {
::std::iter::empty()
$(.chain(::std::iter::once($it)))*
};
}
pub(crate) fn iter_use_idents<'a, F, R: 'a>(item_use: &'a syn::ItemUse, mut f: F) -> Result<Vec<R>>
where
F: FnMut(&'a syn::Ident, bool) -> Result<R>,
{
let mut result = Vec::new();
match &item_use.tree {
UseTree::Name(name) => result.push(f(&name.ident, true)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, true)?),
UseTree::Path(path) => match &*path.tree {
UseTree::Name(name) => result.push(f(&name.ident, true)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, true)?),
other => iter_use_tree_idents(other, &mut result, &mut f)?,
},
other => iter_use_tree_idents(other, &mut result, &mut f)?,
}
Ok(result)
}
fn iter_use_tree_idents<'a, F, R: 'a>(
tree: &'a syn::UseTree,
result: &mut Vec<R>,
f: &mut F,
) -> Result<()>
where
F: FnMut(&'a syn::Ident, bool) -> Result<R>,
{
match tree {
UseTree::Name(name) => result.push(f(&name.ident, false)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, false)?),
UseTree::Path(path) => iter_use_tree_idents(&path.tree, result, f)?,
UseTree::Group(syn::UseGroup { items, .. }) => {
for subtree in items {
iter_use_tree_idents(subtree, result, f)?;
}
}
UseTree::Glob(glob) => {
bail_span!(glob, "#[py*] doesn't allow '*'")
}
}
Ok(())
}
pub(crate) fn text_signature(sig: &Signature, name: &str) -> String {
let signature = func_sig(sig);
if signature.starts_with("$self") {
format!("{name}({signature})")
} else {
format!("{}({}, {})", name, "$module", signature)
}
}
fn func_sig(sig: &Signature) -> String {
sig.inputs
.iter()
.filter_map(|arg| {
use syn::FnArg::*;
let arg = match arg {
Typed(typed) => typed,
Receiver(_) => return Some("$self".to_owned()),
};
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
return Some("*args, **kwargs".to_owned());
}
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
return None;
}
let ident = match arg.pat.as_ref() {
syn::Pat::Ident(p) => p.ident.to_string(),
other => quote!(#other).to_string(),
};
if ident == "zelf" {
return Some("$self".to_owned());
}
if ident == "vm" {
unreachable!("type &VirtualMachine(`{}`) must be filtered already", ty);
}
Some(ident)
})
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn format_doc(sig: &str, doc: &str) -> String {
format!("{sig}\n--\n\n{doc}")
}