pub mod enumeration;
pub mod value;
use std::{borrow::Cow, fmt::Display, sync::OnceLock};
use darling::{
FromAttributes as _,
util::{Override, SpannedValue},
};
use heck::{ToKebabCase as _, ToShoutySnakeCase, ToTitleCase};
use itertools::Itertools as _;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::ToTokens;
use regex::Regex;
use syn::{Attribute, Expr, Field, Ident, Type, spanned::Spanned as _};
macro_rules! regex {
($pattern:literal) => {{
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new($pattern).expect("failed to compile regex"))
}};
}
#[derive(Clone, Eq)]
pub struct IdentString<'a> {
raw: &'a Ident,
string: String,
}
impl<'a> IdentString<'a> {
#[must_use]
pub fn new(ident: &'a Ident) -> Self {
Self {
string: ident.to_string(),
raw: ident,
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
self.string.as_str()
}
#[inline]
#[must_use]
pub fn raw(&self) -> &'a Ident {
self.raw
}
#[inline]
#[must_use]
pub fn as_spanned_str(&self) -> SpannedValue<&str> {
SpannedValue::new(self.string.as_str(), self.raw.span())
}
}
impl ToTokens for IdentString<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
self.raw.to_tokens(tokens);
}
}
impl PartialEq<str> for IdentString<'_> {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<IdentString<'_>> for str {
fn eq(&self, other: &IdentString<'_>) -> bool {
self == other.as_str()
}
}
impl PartialEq<IdentString<'_>> for IdentString<'_> {
fn eq(&self, other: &IdentString<'_>) -> bool {
self.as_str() == other.as_str()
}
}
impl std::hash::Hash for IdentString<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
pub enum FlattenOr<F, T> {
Flatten(F),
Normal(T),
}
#[derive(darling::FromAttributes)]
#[darling(attributes(debate))]
struct RawParsedFieldAttr {
long: Option<SpannedValue<Override<String>>>,
short: Option<SpannedValue<Override<char>>>,
default: Option<SpannedValue<Override<Expr>>>,
placeholder: Option<SpannedValue<String>>,
invert: Option<SpannedValue<Override<String>>>,
#[darling(rename = "override")]
r#override: Option<SpannedValue<()>>,
flatten: Option<SpannedValue<()>>,
}
macro_rules! reject_some {
($message:literal; $($candidate:expr $(,)?)+) => {
$(
if let Some(ref value) = $candidate {
Err(syn::Error::new(value.span(), $message))
} else
)+ { Ok(()) }
};
}
pub enum FieldDefault {
Trait,
Expr(Expr),
}
impl FieldDefault {
#[inline]
#[must_use]
pub fn new(default: Option<SpannedValue<Override<Expr>>>) -> Option<Self> {
default.map(|default| match default.into_inner() {
Override::Explicit(default) => Self::Expr(default),
Override::Inherit => Self::Trait,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FlagTags<Long, Short> {
Long(Long),
Short(Short),
LongShort { long: Long, short: Short },
}
impl<'a> FlagTags<&'a str, char> {
#[inline]
#[must_use]
pub fn long(&self) -> Option<&'a str> {
match *self {
FlagTags::Long(long) | FlagTags::LongShort { long, .. } => Some(long),
FlagTags::Short(_) => None,
}
}
#[inline]
#[must_use]
pub fn short(&self) -> Option<char> {
match *self {
FlagTags::Short(short) | FlagTags::LongShort { short, .. } => Some(short),
FlagTags::Long(_) => None,
}
}
}
impl FlagTags<SpannedValue<String>, SpannedValue<char>> {
#[inline]
#[must_use]
pub fn long(&self) -> Option<SpannedValue<&str>> {
match *self {
FlagTags::Long(ref long) | FlagTags::LongShort { ref long, .. } => {
Some(SpannedValue::new(long.as_str(), long.span()))
}
FlagTags::Short(_) => None,
}
}
#[inline]
#[must_use]
pub fn short(&self) -> Option<SpannedValue<char>> {
match *self {
FlagTags::Short(short) | FlagTags::LongShort { short, .. } => Some(short),
FlagTags::Long(_) => None,
}
}
#[inline]
#[must_use]
pub fn simplify(&self) -> FlagTags<&'_ str, char> {
match self {
FlagTags::Long(long) => FlagTags::Long(long.as_str()),
FlagTags::Short(short) => FlagTags::Short(**short),
FlagTags::LongShort { long, short } => FlagTags::LongShort {
long: long.as_str(),
short: **short,
},
}
}
}
pub struct Description {
pub succinct: String,
pub full: String,
}
impl Description {
pub const fn empty() -> Self {
Self {
succinct: String::new(),
full: String::new(),
}
}
}
pub struct PositionalFieldInfo<'a> {
pub ident: IdentString<'a>,
pub docs: Description,
pub placeholder: SpannedValue<String>,
pub ty: &'a Type,
pub default: Option<FieldDefault>,
}
pub struct FlagFieldInfo<'a> {
pub ident: IdentString<'a>,
pub docs: Description,
pub placeholder: SpannedValue<String>,
pub ty: &'a Type,
pub default: Option<FieldDefault>,
pub tags: FlagTags<SpannedValue<String>, SpannedValue<char>>,
pub invert: Option<SpannedValue<String>>,
pub overridable: bool,
}
pub struct FlattenFieldInfo<'a> {
pub ident: IdentString<'a>,
pub docs: Description,
pub placeholder: SpannedValue<String>,
pub ty: &'a Type,
pub title: SpannedValue<String>,
}
pub fn compute_docs(attrs: &[Attribute]) -> syn::Result<Description> {
let body: String = attrs
.iter()
.filter_map(|attr| match attr.meta {
syn::Meta::NameValue(ref meta) => Some(meta),
_ => None,
})
.filter(|meta| meta.path.is_ident("doc"))
.map(|meta| match meta.value {
Expr::Lit(ref lit) => match lit.lit {
syn::Lit::Str(ref lit) => Ok(lit.value()),
_ => Err(syn::Error::new(meta.span(), "malformed #[doc] attribute")),
},
Expr::Macro(ref expr) => Err(syn::Error::new(
expr.span(),
"macro #[doc] attributes aren't supported",
)),
_ => Err(syn::Error::new(meta.span(), "malformed #[doc] attribute")),
})
.map_ok(|mut doc| {
doc.push('\n');
doc
})
.try_collect()?;
let leading_whitespace = regex!("^[\t\n ]*\n");
let paragraph_separator = regex!("[\t ]*\n[*\t\n ]*\n");
let body = body.trim_end();
let body = leading_whitespace.replace(body, "");
let mut paragraphs = paragraph_separator.split(&body).map(|paragraph| {
let (unfilled, _) = textwrap::unfill(paragraph);
textwrap::fill(&unfilled, 80)
});
let Some(first) = paragraphs.next() else {
return Ok(Description::empty());
};
let short = first.clone();
let long = paragraphs.fold(first, |mut body, paragraph| {
body.reserve(paragraph.len() + 2);
body.push_str("\n\n");
body.push_str(¶graph);
body
});
Ok(Description {
succinct: short,
full: long,
})
}
pub enum ParsedFieldInfo<'a> {
Positional(PositionalFieldInfo<'a>),
Flag(FlagFieldInfo<'a>),
Flatten(FlattenFieldInfo<'a>),
}
impl<'a> ParsedFieldInfo<'a> {
pub fn from_field(field: &'a Field) -> syn::Result<Self> {
let parsed = RawParsedFieldAttr::from_attributes(&field.attrs)?;
let docs = compute_docs(&field.attrs)?;
let ty = &field.ty;
let ident = IdentString::new(field.ident.as_ref().ok_or_else(|| {
syn::Error::new(
field.span(),
"can't use anonymous fields in debate derives. This should \
have been detected already, it's probably a bug in `debate`.",
)
})?);
if parsed.flatten.is_some() {
reject_some!(
"`flatten` is incompatible with other attributes";
parsed.long, parsed.short, parsed.default, parsed.placeholder,
parsed.invert, parsed.r#override,
)?;
let name = ident.as_str();
let group_name = SpannedValue::new(
match name.ends_with("command") {
true => Cow::Owned(format!("{name}s")),
false => Cow::Borrowed(name),
}
.to_title_case(),
ident.span(),
);
let placeholder = SpannedValue::new(name.to_shouty_snake_case(), ident.span());
return Ok(Self::Flatten(FlattenFieldInfo {
ident,
ty,
title: group_name,
placeholder,
docs,
}));
}
let placeholder = compute_placeholder(parsed.placeholder, &ident)?;
let default = FieldDefault::new(parsed.default);
if let Some(tags) = compute_tags(parsed.long, parsed.short, &ident)? {
let invert = parsed
.invert
.map(|invert| {
let long = tags.long().ok_or_else(|| {
syn::Error::new(
invert.span(),
"#[debate(invert)] requires either a --long tag \
(to prepend with --no-long) or a manual name \
(such as invert=\"quiet\"",
)
})?;
compute_invert(unpack_spanned_override(invert), long)
})
.transpose()?;
Ok(Self::Flag(FlagFieldInfo {
ident,
placeholder,
ty,
default,
docs,
invert,
overridable: parsed.r#override.is_some(),
tags,
}))
} else {
reject_some!(
"`invert` can only be used with flags, not positionals";
parsed.invert
)?;
reject_some!(
"`override` can only be used with flags, not positionals";
parsed.r#override
)?;
Ok(Self::Positional(PositionalFieldInfo {
ident,
placeholder,
ty,
default,
docs,
}))
}
}
pub fn get_positional(
&self,
) -> Option<FlattenOr<&FlattenFieldInfo<'a>, &PositionalFieldInfo<'a>>> {
match self {
ParsedFieldInfo::Positional(info) => Some(FlattenOr::Normal(info)),
ParsedFieldInfo::Flatten(info) => Some(FlattenOr::Flatten(info)),
ParsedFieldInfo::Flag(_) => None,
}
}
}
#[inline]
#[must_use]
fn unpack_spanned_override<T>(value: SpannedValue<Override<T>>) -> Option<SpannedValue<T>> {
let span = value.span();
match value.into_inner() {
Override::Inherit => None,
Override::Explicit(value) => Some(SpannedValue::new(value, span)),
}
}
fn derive_from_field_name<T>(
user_value: Option<SpannedValue<T>>,
field_name: SpannedValue<&str>,
from_field_name: impl FnOnce(&str) -> T,
check: impl FnOnce(&T) -> Result<(), &'static str>,
) -> syn::Result<SpannedValue<T>> {
let value = user_value
.unwrap_or_else(|| SpannedValue::new(from_field_name(&field_name), field_name.span()));
match check(&value) {
Ok(()) => Ok(value),
Err(message) => Err(syn::Error::new(value.span(), message)),
}
}
macro_rules! checks {
($($check:expr => $message:literal),+ $(,)?) => {
$(
if $check { Err($message) } else
)+
{ Ok(()) }
}
}
#[inline]
fn compute_long(
long: Option<SpannedValue<String>>,
field_name: &IdentString<'_>,
) -> syn::Result<SpannedValue<String>> {
derive_from_field_name(
long,
field_name.as_spanned_str(),
|field| field.to_kebab_case(),
|long| {
checks! {
long.starts_with("--") =>
"long parameters don't need to start with --; this is \
handled automatically",
long.starts_with('-') =>
"long parameters don't start with '-'",
!long.starts_with(|c: char| c.is_alphabetic()) =>
"long parameters should start with something alphabetic. \
This might be relaxed later.",
long.contains('=') =>
"long parameters must not include an '=', as it is the \
argument separator",
long.contains(|c: char| c.is_whitespace()) =>
"long parameters shouldn't include whitespace",
}
},
)
}
#[inline]
fn compute_short(
short: Option<SpannedValue<char>>,
field_name: &IdentString<'_>,
) -> syn::Result<SpannedValue<char>> {
derive_from_field_name(
short,
field_name.as_spanned_str(),
|field| {
field
.chars()
.next()
.expect("Identifiers can't be empty")
.to_ascii_lowercase()
},
|short| {
checks! {
*short == '-' => "short parameter must not be '-'",
!short.is_ascii_graphic() => "short parameter should be an ascii printable",
}
},
)
}
pub fn compute_tags(
long: Option<SpannedValue<Override<String>>>,
short: Option<SpannedValue<Override<char>>>,
ident: &IdentString<'_>,
) -> syn::Result<Option<FlagTags<SpannedValue<String>, SpannedValue<char>>>> {
let long = long
.map(|long| compute_long(unpack_spanned_override(long), ident))
.transpose()?;
let short = short
.map(|short| compute_short(unpack_spanned_override(short), ident))
.transpose()?;
Ok(match (long, short) {
(None, None) => None,
(Some(long), None) => Some(FlagTags::Long(long)),
(None, Some(short)) => Some(FlagTags::Short(short)),
(Some(long), Some(short)) => Some(FlagTags::LongShort { long, short }),
})
}
#[inline]
fn compute_placeholder(
placeholder: Option<SpannedValue<String>>,
field_name: &IdentString<'_>,
) -> syn::Result<SpannedValue<String>> {
derive_from_field_name(
placeholder,
field_name.as_spanned_str(),
|field| field.to_shouty_snake_case(),
|placeholder| {
checks! {
placeholder.contains(|c: char| c.is_whitespace()) => "placeholder shouldn't include whitespace",
}
},
)
}
#[inline]
fn compute_invert(
invert: Option<SpannedValue<String>>,
long: SpannedValue<&str>,
) -> syn::Result<SpannedValue<String>> {
derive_from_field_name(
invert,
long,
|long| {
let underscore = long.contains('_');
let dash = long.contains('-');
let upper = long.contains(|c: char| c.is_uppercase());
let lower = long.contains(|c: char| c.is_lowercase());
let word = match (upper, lower) {
(false, _) => "no",
(true, true) => "No",
(true, false) => "NO",
};
let mark = match (dash, underscore) {
(true, _) => "-",
(false, true) => "_",
(false, false) if lower && upper => "",
(false, false) => "-",
};
format!("{word}{mark}{long}")
},
|_| Ok(()),
)
}
#[derive(darling::FromAttributes, Debug)]
#[darling(attributes(debate))]
pub struct RawParsedTypeAttr {
help: Option<SpannedValue<()>>,
}
impl RawParsedTypeAttr {
pub fn help_option(&self) -> Option<HelpFlag<'_>> {
self.help.as_ref().map(|help| HelpFlag {
tags: FlagTags::LongShort {
long: "help",
short: 'h',
},
span: help.span(),
})
}
}
pub struct HelpFlag<'a> {
pub tags: FlagTags<&'a str, char>,
pub span: Span,
}
pub fn error_pair(
origin1: Span,
message1: impl Display,
origin2: Span,
message2: impl Display,
) -> syn::Error {
let mut error = syn::Error::new(origin1, message1);
error.combine(syn::Error::new(origin2, message2));
error
}