use std::collections::HashMap;
use crate::parse::{BaseSelector, DeriveSpec, VariantValue};
use crate::reserved::{ReservedKind, is_reserved};
use syn::Result;
pub(crate) fn run(spec: DeriveSpec) -> Result<DeriveSpec> {
check_reserved_names(&spec)?;
check_base_value_compatibility(&spec)?;
check_duplicate_values(&spec)?;
Ok(spec)
}
fn check_reserved_names(spec: &DeriveSpec) -> Result<()> {
for variant in &spec.variants {
if let Some(kind) = is_reserved(&variant.rust_ident.to_string()) {
let category = match kind {
ReservedKind::PythonKeyword => "a Python keyword",
ReservedKind::EnumReservedMember => "an `enum`-reserved member name",
ReservedKind::EnumSpecialMethod => "an `enum` special method name",
};
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` collides with {category}; \
rename the Rust variant (future `#[pyenum(rename = \
\"...\")]` may offer an opt-out path)",
variant.rust_ident
),
));
}
}
Ok(())
}
fn check_base_value_compatibility(spec: &DeriveSpec) -> Result<()> {
let base = spec.base;
for variant in &spec.variants {
match (&variant.value, base) {
(
VariantValue::Str(_),
BaseSelector::IntEnum | BaseSelector::Flag | BaseSelector::IntFlag,
) => {
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` has a string `#[pyenum(value = ...)]` \
but the enum base is `{}`, which requires integer \
values",
variant.rust_ident,
base_display(base),
),
));
}
(VariantValue::Int(_), BaseSelector::StrEnum) => {
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` has an integer discriminant but the \
enum base is `StrEnum`, which requires string \
values (use `#[pyenum(value = \"...\")]` or omit \
the discriminant for auto-lowercased names)",
variant.rust_ident,
),
));
}
_ => {}
}
}
Ok(())
}
fn check_duplicate_values(spec: &DeriveSpec) -> Result<()> {
let mut seen_ints: HashMap<i64, String> = HashMap::new();
let mut seen_strs: HashMap<String, String> = HashMap::new();
for variant in &spec.variants {
let variant_name = variant.rust_ident.to_string();
match &variant.value {
VariantValue::Int(v) => {
if let Some(prev) = seen_ints.get(v) {
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` has discriminant `{}`, which was \
already used by `{}`; Python would make the \
second variant an alias of the first and \
break Rust-side round-trip identity",
variant_name, v, prev,
),
));
}
seen_ints.insert(*v, variant_name);
}
VariantValue::Str(s) => {
let normalized = s.clone();
if let Some(prev) = seen_strs.get(&normalized) {
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` has value `{:?}`, which was \
already used by `{}`; Python would make the \
second variant an alias of the first and \
break Rust-side round-trip identity",
variant_name, s, prev,
),
));
}
seen_strs.insert(normalized, variant_name);
}
VariantValue::Auto => {
if spec.base == BaseSelector::StrEnum {
let lowered = variant_name.to_lowercase();
if let Some(prev) = seen_strs.get(&lowered) {
return Err(syn::Error::new(
variant.rust_ident.span(),
format!(
"variant `{}` auto-lowercases to `{:?}`, \
which was already used by `{}`; Python \
would make the second variant an alias \
of the first and break Rust-side \
round-trip identity (add an explicit \
`#[pyenum(value = \"...\")]` to \
disambiguate)",
variant_name, lowered, prev,
),
));
}
seen_strs.insert(lowered, variant_name);
}
}
}
}
Ok(())
}
fn base_display(base: BaseSelector) -> &'static str {
match base {
BaseSelector::Enum => "Enum",
BaseSelector::IntEnum => "IntEnum",
BaseSelector::StrEnum => "StrEnum",
BaseSelector::Flag => "Flag",
BaseSelector::IntFlag => "IntFlag",
}
}