use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, FnArg, Ident, ItemFn, Pat, ReturnType, Signature, Type};
use crate::{
args::Args,
error::{self, Error},
};
fn pattern_from_arg(arg: &FnArg) -> &Pat {
match arg {
FnArg::Receiver(_) => unreachable!("`pattern_from_arg` should never receive receiver patterns"),
FnArg::Typed(pat_type) => &*pat_type.pat,
}
}
fn is_associated_function(signature: &Signature) -> bool {
fn is_receiver(arg: &FnArg) -> bool {
matches!(arg, FnArg::Receiver(_))
}
signature.inputs.iter().any(is_receiver)
}
pub fn gen_negated_function(func: ItemFn, args: Args) -> TokenStream {
let maybe_name = args.name;
let maybe_docs = args.docs;
let negated_identifier = {
let signature = &func.sig;
let output_type = &signature.output;
if !returns_bool(output_type) {
return error::build_compile_error(func.span(), Error::DoesNotReturnBool);
}
match build_identifier(maybe_name, &func) {
Ok(id) => id,
Err(err) => {
return error::build_compile_error(func.span(), err);
}
}
};
let original_function = func;
let mut new_signature = original_function.sig.clone();
new_signature.ident = Ident::new(&negated_identifier, Span::call_site());
let doc_string = build_docstring(maybe_docs, &original_function);
if is_associated_function(&new_signature) {
generate_associated_fn(original_function, new_signature, doc_string)
} else {
generate_non_associated_fn(original_function, new_signature, doc_string)
}
}
fn generate_associated_fn(
original_function: ItemFn,
new_signature: Signature,
doc_string: String,
) -> TokenStream {
let visibility = &original_function.vis;
let arguments = new_signature.inputs.iter().skip(1).map(pattern_from_arg);
let original_identifier = &original_function.sig.ident;
let tokens = quote! {
#original_function
#[doc = #doc_string]
#visibility #new_signature {
!self.#original_identifier(#(#arguments),*)
}
};
tokens.into()
}
fn generate_non_associated_fn(
original_function: ItemFn,
new_signature: Signature,
doc_string: String,
) -> TokenStream {
let visibility = &original_function.vis;
let arguments = new_signature.inputs.iter().map(pattern_from_arg);
let original_identifier = &original_function.sig.ident;
let tokens = quote! {
#original_function
#[doc = #doc_string]
#visibility #new_signature {
!#original_identifier(#(#arguments),*)
}
};
tokens.into()
}
fn build_docstring(maybe_docs: Option<String>, original_function: &ItemFn) -> String {
let original_identifier = &original_function.sig.ident;
let gen_docs = || {
let ident = original_identifier.to_string();
format!("This is an automatically generated function that negates [`{}`].\nConsult the original function for more information.", ident)
};
maybe_docs.unwrap_or_else(gen_docs)
}
fn build_identifier(
maybe_name: Option<String>,
original_function: &ItemFn,
) -> Result<String, Error> {
if let Some(name) = maybe_name {
Ok(name)
} else {
let original_identifier = &original_function.sig.ident;
negate_identifier(original_identifier)
}
}
fn returns_bool(return_type: &ReturnType) -> bool {
fn type_is_bool(ty: &Type) -> bool {
matches!(ty, Type::Path(type_path) if type_path.to_token_stream().to_string() == "bool")
}
match return_type {
ReturnType::Default => false,
ReturnType::Type(_arrow, ty) => type_is_bool(ty),
}
}
fn get_adjective(identifier: &str) -> Option<&str> {
if !identifier.starts_with("is_") {
None?;
}
identifier.get(3..)
}
fn negate_identifier(ident: &Ident) -> Result<String, Error> {
let identifier = ident.to_string();
let adjective = get_adjective(&identifier).ok_or(Error::InvalidIdentifier)?;
Ok(format!("is_not_{}", adjective))
}