use std::mem;
use itertools::Itertools;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::ToTokens;
use syn::{
Attribute, FnArg, Ident, ItemFn, Pat, PatType,
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
};
use crate::common::IdentString;
enum LazyPair<T, F> {
First(T, F),
Second(F),
Done,
}
impl<T, F: FnOnce() -> Option<T>> LazyPair<T, F> {
pub fn new(first: T, second: F) -> Self {
Self::First(first, second)
}
}
impl<T, F: FnOnce() -> Option<T>> Iterator for LazyPair<T, F> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
match mem::replace(self, LazyPair::Done) {
LazyPair::First(item, func) => {
*self = LazyPair::Second(func);
Some(item)
}
LazyPair::Second(func) => func(),
LazyPair::Done => None,
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match *self {
Self::First(..) => (1, Some(2)),
Self::Second(..) => (0, Some(1)),
Self::Done => (0, Some(0)),
}
}
}
fn is_args_attr(attr: &Attribute) -> bool {
match attr.meta.require_path_only() {
Ok(path) => path.is_ident("args"),
Err(_) => false,
}
}
fn extract_args_fn_input(
args: impl IntoIterator<Item = FnArg>,
span: Span,
) -> syn::Result<(PatType, Punctuated<FnArg, Comma>)> {
let args = args.into_iter();
let mut args = match args.at_most_one() {
Ok(None) => {
return Err(syn::Error::new(
span,
"#[debate::main] function requires an argument, \
that's the whole point",
));
}
Ok(Some(FnArg::Typed(arg))) => return Ok((arg, Punctuated::new())),
Ok(Some(FnArg::Receiver(_))) => {
return Err(syn::Error::new(
span,
"#[debate::main] function requires an argument \
(`self` doesn't count)",
));
}
Err(args) => args,
};
let mut inputs = Punctuated::new();
let arg = args
.find_map(|arg| {
let arg = match arg {
FnArg::Typed(arg) if arg.attrs.iter().any(is_args_attr) => {
return Some(arg);
}
arg => arg,
};
inputs.push(arg);
None
})
.ok_or_else(|| {
syn::Error::new(
span,
"#[debate::main] function with more than one argument must \
use #[args] to identify the CLI arguments parameter",
)
})?;
inputs.extend(args);
Ok((arg, inputs))
}
fn ident_is_lowercase(ident: &IdentString<'_>) -> bool {
ident.as_str().chars().all(|c| !c.is_uppercase())
}
fn find_best_ident<'a>(
options: impl IntoIterator<Item = IdentString<'a>>,
) -> Option<IdentString<'a>> {
let mut options = options.into_iter();
let first = options.next()?;
Some(match ident_is_lowercase(&first) {
true => first,
false => options
.find(|ident| ident_is_lowercase(ident))
.unwrap_or(first),
})
}
fn find_best_ident_from_pattern_list<'a>(
option: impl IntoIterator<Item = &'a Pat>,
) -> Option<IdentString<'a>> {
find_best_ident(option.into_iter().filter_map(get_useful_ident))
}
fn get_useful_ident(pattern: &Pat) -> Option<IdentString<'_>> {
match pattern {
Pat::Ident(pat) => match pat.subpat {
None => Some(IdentString::new(&pat.ident)),
Some((_, ref subpat)) => {
find_best_ident(LazyPair::new(IdentString::new(&pat.ident), || {
get_useful_ident(subpat)
}))
}
},
Pat::Or(pat) => find_best_ident_from_pattern_list(&pat.cases),
Pat::Paren(inner) => get_useful_ident(&inner.pat),
Pat::Path(path) => path.path.get_ident().map(IdentString::new),
Pat::Reference(pat) => get_useful_ident(&pat.pat),
Pat::Slice(pat) => find_best_ident_from_pattern_list(&pat.elems),
Pat::Struct(pat) => {
find_best_ident_from_pattern_list(pat.fields.iter().map(|field| &*field.pat))
}
Pat::Tuple(pat) => find_best_ident_from_pattern_list(&pat.elems),
Pat::TupleStruct(pat) => find_best_ident_from_pattern_list(&pat.elems),
Pat::Type(typed) => get_useful_ident(&typed.pat),
_ => None,
}
}
enum Mode {
Normal,
Leak,
}
impl Parse for Mode {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident: Option<Ident> = input.parse()?;
match ident {
None => Ok(Mode::Normal),
Some(ident) => match ident == "leak" {
true => Ok(Mode::Leak),
false => Err(syn::Error::new(ident.span(), "unrecognized token")),
},
}
}
}
pub fn decorate_fn_main(attrs: TokenStream2, function: TokenStream2) -> syn::Result<TokenStream2> {
let mode: Mode = syn::parse2(attrs)?;
let mut function: ItemFn = syn::parse2(function)?;
let inputs_span = function.sig.paren_token.span.span();
let (arg, updated_inputs) = extract_args_fn_input(function.sig.inputs, inputs_span)?;
function.sig.inputs = updated_inputs;
if let Some(weird) = arg.attrs.iter().find(|attr| !is_args_attr(attr)) {
return Err(syn::Error::new(
weird.span(),
"unrecgonzied attribute on #[debate::main] CLI arguments parameter",
));
}
let storage_identifier = {
let original_ident = get_useful_ident(&arg.pat)
.ok_or_else(|| syn::Error::new(arg.pat.span(), "there aren't any variables here"))?
.raw();
let mut ident = original_ident.clone();
ident.set_span(Span::mixed_site().located_at(original_ident.span()));
ident
};
let user_pattern = &arg.pat;
let user_type = &arg.ty;
let body_prefix = [
match mode {
Mode::Normal => parse_quote! {
let #storage_identifier =
::debate::arguments::LoadedArguments::from_env();
},
Mode::Leak => parse_quote! {
let #storage_identifier: &'static ::debate::arguments::LoadedArguments =
::std::boxed::Box::leak(
::std::boxed::Box::new(
::debate::arguments::LoadedArguments::from_env()
)
);
},
},
parse_quote! {
let #user_pattern: #user_type =
::debate::arguments::LoadedArguments::parse(&#storage_identifier);
},
];
function.block.stmts = itertools::chain(body_prefix, function.block.stmts).collect();
Ok(function.into_token_stream())
}