use std::cmp::Reverse;
use std::collections::HashSet;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Attribute, Data, DataEnum, DeriveInput, Error, ExprLit, Lit, Meta, MetaNameValue};
pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput {
ident,
data: Data::Enum(DataEnum { variants, .. }),
..
} = input
else {
return Err(Error::new(
input.ident.span(),
"only named fields are supported",
));
};
let mut parsed = Vec::new();
let mut common_prefix_match_arms = quote!();
let mut name_match_arms =
quote!(Self::Ruff => "Ruff-specific rules", Self::Numpy => "NumPy-specific rules", );
let mut url_match_arms = quote!(Self::Ruff => None, Self::Numpy => None, );
let mut all_prefixes = HashSet::new();
for variant in variants {
let mut first_chars = HashSet::new();
let prefixes: Result<Vec<_>, _> = variant
.attrs
.iter()
.filter(|attr| attr.path().is_ident("prefix"))
.map(|attr| {
let Meta::NameValue(MetaNameValue {
value:
syn::Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}),
..
}) = &attr.meta
else {
return Err(Error::new(
attr.span(),
r#"expected attribute to be in the form of [#prefix = "..."]"#,
));
};
let str = lit.value();
match str.chars().next() {
None => {
return Err(Error::new(
lit.span(),
"expected prefix string to be non-empty",
));
}
Some(c) => {
if !first_chars.insert(c) {
return Err(Error::new(
lit.span(),
format!(
"this variant already has another prefix \
starting with the character '{c}'"
),
));
}
}
}
if !all_prefixes.insert(str.clone()) {
return Err(Error::new(
lit.span(),
"prefix has already been defined before",
));
}
Ok(str)
})
.collect();
let prefixes = prefixes?;
if prefixes.is_empty() {
return Err(Error::new(
variant.span(),
r#"Missing #[prefix = "..."] attribute"#,
));
}
let Some(doc_attr) = variant
.attrs
.iter()
.find(|attr| attr.path().is_ident("doc"))
else {
return Err(Error::new(variant.span(), "expected a doc comment"));
};
let variant_ident = variant.ident;
if variant_ident != "Ruff" && variant_ident != "Numpy" {
let (name, url) = parse_doc_attr(doc_attr)?;
name_match_arms.extend(quote! {Self::#variant_ident => #name,});
url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),});
}
for lit in &prefixes {
parsed.push((
lit.clone(),
variant_ident.clone(),
match prefixes.len() {
1 => ParseStrategy::SinglePrefix,
_ => ParseStrategy::MultiplePrefixes,
},
));
}
if let [prefix] = &prefixes[..] {
common_prefix_match_arms.extend(quote! { Self::#variant_ident => #prefix, });
} else {
common_prefix_match_arms.extend(quote! { Self::#variant_ident => "", });
}
}
parsed.sort_by_key(|(prefix, ..)| Reverse(prefix.len()));
let mut if_statements = quote!();
for (prefix, field, strategy) in parsed {
let ret_str = match strategy {
ParseStrategy::SinglePrefix => quote!(rest),
ParseStrategy::MultiplePrefixes => quote!(code),
};
if_statements.extend(quote! {if let Some(rest) = code.strip_prefix(#prefix) {
return Some((#ident::#field, #ret_str));
}});
}
Ok(quote! {
#[automatically_derived]
impl crate::registry::RuleNamespace for #ident {
fn parse_code(code: &str) -> Option<(Self, &str)> {
#if_statements
None
}
fn common_prefix(&self) -> &'static str {
match self { #common_prefix_match_arms }
}
fn name(&self) -> &'static str {
match self { #name_match_arms }
}
fn url(&self) -> Option<&'static str> {
match self { #url_match_arms }
}
}
})
}
fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
let Meta::NameValue(MetaNameValue {
value:
syn::Expr::Lit(ExprLit {
lit: Lit::Str(doc_lit),
..
}),
..
}) = &doc_attr.meta
else {
return Err(Error::new(
doc_attr.span(),
r#"expected doc attribute to be in the form of #[doc = "..."]"#,
));
};
parse_markdown_link(doc_lit.value().trim())
.map(|(name, url)| (name.to_string(), url.to_string()))
.ok_or_else(|| {
Error::new(
doc_lit.span(),
"expected doc comment to be in the form of \
`/// [name](https://example.com/)`",
)
})
}
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
}
enum ParseStrategy {
SinglePrefix,
MultiplePrefixes,
}