use crate::export;
use convert_case::{Case, Casing};
use proc_macro_error::abort;
use proc_macro2::Span;
use quote::ToTokens;
use std::collections::BTreeMap;
use syn::{
FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemFn, ItemImpl, Lifetime, Pat, Path,
PathArguments, PathSegment, ReturnType, Signature, Token, Type, TypeImplTrait, TypeParamBound,
TypePath, TypeReference, TypeTuple, WhereClause, punctuated::Punctuated, spanned::Spanned,
};
pub(crate) fn impl_type_refs(item_impl_type: &Type) -> (&TypePath, &PathArguments, &Ident) {
let path = if let Type::Path(type_path) = item_impl_type {
type_path
} else {
abort!(
item_impl_type,
"failed to parse impl type: {}",
item_impl_type.to_token_stream()
)
};
let segment = path.path.segments.last().unwrap();
let args = &segment.arguments;
let ident = &segment.ident;
(path, args, ident)
}
pub(crate) fn impl_constraints(item_impl: &ItemImpl) -> (Generics, Option<WhereClause>) {
let mut generics = item_impl.generics.clone();
let where_clause = generics.where_clause.take();
(generics, where_clause)
}
fn extract_params(handler_signature: &Signature) -> impl Iterator<Item = (&Ident, &Type)> {
handler_signature.inputs.iter().filter_map(|arg| {
if let FnArg::Typed(arg) = arg {
let arg_ident = if let Pat::Ident(arg_ident) = arg.pat.as_ref() {
&arg_ident.ident
} else {
abort!(arg.span(), "unnamed arguments are not supported");
};
return Some((arg_ident, arg.ty.as_ref()));
}
None
})
}
pub(crate) fn result_type(handler_signature: &Signature) -> Type {
match &handler_signature.output {
ReturnType::Type(_, ty) => *ty.to_owned(),
ReturnType::Default => Type::Tuple(TypeTuple {
paren_token: Default::default(),
elems: Default::default(),
}),
}
}
pub(crate) fn unwrap_result_type(
signature: &Signature,
unwrap_result: bool,
) -> (Type, Option<Type>) {
let result_type = result_type(signature);
if unwrap_result {
if let Type::Path(tp) = &result_type
&& let Some((ok, err)) = extract_result_types(tp)
{
(ok.clone(), Some(err.clone()))
} else {
abort!(
result_type.span(),
"`unwrap_result` can be applied to methods returns result only"
)
}
} else {
(result_type, None)
}
}
pub(crate) struct InvocationExport {
pub span: Span,
pub route: String,
pub unwrap_result: bool,
pub export: bool,
#[cfg(feature = "ethexe")]
pub payable: bool,
pub overrides: Option<Path>,
pub entry_id: Option<u16>,
pub scale: bool,
#[cfg(feature = "ethexe")]
pub ethabi: bool,
}
pub(crate) fn invocation_export(fn_impl: &ImplItemFn) -> Option<InvocationExport> {
export::parse_export_args(&fn_impl.attrs).map(|(args, span)| {
let ident = &fn_impl.sig.ident;
let unwrap_result = args.unwrap_result();
let scale = args.scale();
#[cfg(feature = "ethexe")]
let payable = args.payable();
#[cfg(feature = "ethexe")]
let ethabi = args.ethabi();
let route = args.route().map_or_else(
|| ident.to_string().to_case(Case::Pascal),
|route| route.to_case(Case::Pascal),
);
InvocationExport {
span,
route,
unwrap_result,
export: true,
#[cfg(feature = "ethexe")]
payable,
overrides: args.overrides().cloned(),
entry_id: args.entry_id(),
scale,
#[cfg(feature = "ethexe")]
ethabi,
}
})
}
pub(crate) fn invocation_export_or_default(fn_impl: &ImplItemFn) -> InvocationExport {
invocation_export(fn_impl).unwrap_or_else(|| {
let ident = &fn_impl.sig.ident;
InvocationExport {
span: ident.span(),
route: ident.to_string().to_case(Case::Pascal),
unwrap_result: false,
export: false,
#[cfg(feature = "ethexe")]
payable: false,
overrides: None,
entry_id: None,
scale: true,
#[cfg(feature = "ethexe")]
ethabi: true,
}
})
}
pub(crate) fn discover_invocation_targets<'a>(
item_impl: &'a ItemImpl,
filter: impl Fn(&ImplItemFn) -> bool,
sails_path: &'a Path,
) -> Vec<FnBuilder<'a>> {
let mut routes = BTreeMap::<String, String>::new();
let vec: Vec<FnBuilder<'a>> = item_impl
.items
.iter()
.filter_map(|item| {
if let ImplItem::Fn(fn_item) = item
&& filter(fn_item)
{
let ie = invocation_export_or_default(fn_item);
let entry_id = routes.len() as u16;
if ie.overrides.is_none()
&& let Some(duplicate) =
routes.insert(ie.route.clone(), fn_item.sig.ident.to_string())
{
abort!(
ie.span,
"`export` attribute conflicts with one already assigned to '{}'",
duplicate
);
}
let fn_builder = FnBuilder::new(ie, entry_id, fn_item, sails_path);
return Some(fn_builder);
}
None
})
.collect();
vec
}
pub(crate) fn replace_any_lifetime_with_static(ty: Type) -> Type {
match ty {
Type::Reference(r) => {
if r.lifetime.is_some() {
Type::Reference(TypeReference {
and_token: r.and_token,
lifetime: Some(Lifetime::new("'static", Span::call_site())),
mutability: r.mutability,
elem: r.elem,
})
} else {
Type::Reference(r)
}
}
Type::Path(p) => Type::Path(TypePath {
path: replace_lifetime_with_static_in_path(p.path),
qself: p.qself,
}),
_ => ty,
}
}
fn replace_lifetime_with_static_in_path(path: Path) -> Path {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
for s in path.segments {
segments.push(PathSegment {
ident: s.ident,
arguments: replace_lifetime_with_static_in_path_args(s.arguments),
});
}
Path {
leading_colon: path.leading_colon,
segments,
}
}
fn replace_lifetime_with_static_in_path_args(path_args: PathArguments) -> PathArguments {
if let PathArguments::AngleBracketed(mut type_args) = path_args {
type_args.args.iter_mut().for_each(|a| match a {
GenericArgument::Lifetime(lifetime) => {
*lifetime = Lifetime::new("'static", Span::call_site());
}
GenericArgument::Type(ty) => *ty = replace_any_lifetime_with_static(ty.clone()),
_ => {}
});
PathArguments::AngleBracketed(type_args)
} else {
path_args
}
}
pub(crate) fn remove_lifetimes(path: &Path) -> Path {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
for s in &path.segments {
segments.push(PathSegment {
ident: s.ident.clone(),
arguments: PathArguments::None,
});
}
Path {
leading_colon: path.leading_colon,
segments,
}
}
pub(crate) fn extract_reply_type_with_value(ty: &Type) -> Option<&Type> {
match ty {
Type::Path(tp) => extract_reply_result_type(tp),
Type::ImplTrait(imp) => extract_reply_result_type_from_impl_into(imp),
_ => None,
}
}
fn extract_reply_result_type(tp: &TypePath) -> Option<&Type> {
if let Some(last) = tp.path.segments.last() {
if last.ident != "CommandReply" {
return None;
}
if let PathArguments::AngleBracketed(args) = &last.arguments
&& args.args.len() == 1
&& let Some(GenericArgument::Type(ty)) = args.args.first()
{
return Some(ty);
}
}
None
}
fn extract_reply_result_type_from_impl_into(tit: &TypeImplTrait) -> Option<&Type> {
if let Some(TypeParamBound::Trait(tr)) = tit.bounds.first()
&& let Some(last) = tr.path.segments.last()
{
if last.ident != "Into" {
return None;
}
if let PathArguments::AngleBracketed(args) = &last.arguments
&& args.args.len() == 1
&& let Some(GenericArgument::Type(Type::Path(tp))) = args.args.first()
{
return extract_reply_result_type(tp);
}
}
None
}
pub(crate) fn extract_result_types(tp: &TypePath) -> Option<(&Type, &Type)> {
if let Some(last) = tp.path.segments.last() {
if last.ident != "Result" {
return None;
}
if let PathArguments::AngleBracketed(args) = &last.arguments
&& args.args.len() == 2
&& let Some(GenericArgument::Type(ok_ty)) = args.args.first()
&& let Some(GenericArgument::Type(err_ty)) = args.args.last()
{
return Some((ok_ty, err_ty));
}
}
None
}
#[derive(Clone)]
pub(crate) struct FnBuilder<'a> {
pub route: String,
pub entry_id: u16,
pub export: bool,
#[cfg(feature = "ethexe")]
pub payable: bool,
pub overrides: Option<Path>,
pub override_entry_id: Option<u16>,
pub impl_fn: &'a ImplItemFn,
pub ident: &'a Ident,
pub params_struct_ident: Ident,
params_idents: Vec<&'a Ident>,
params_types: Vec<&'a Type>,
pub result_type: Type,
pub error_type: Option<Type>,
pub sails_path: &'a Path,
pub scale: bool,
#[cfg(feature = "ethexe")]
pub ethabi: bool,
}
impl<'a> FnBuilder<'a> {
pub(crate) fn new(
ie: InvocationExport,
entry_id: u16,
impl_fn: &'a ImplItemFn,
sails_path: &'a Path,
) -> Self {
let InvocationExport {
route,
unwrap_result,
export,
#[cfg(feature = "ethexe")]
payable,
overrides,
entry_id: override_entry_id,
scale,
#[cfg(feature = "ethexe")]
ethabi,
..
} = ie;
let signature = &impl_fn.sig;
let ident = &signature.ident;
let params_struct_ident = if overrides.is_some() {
let ident_pascal = ident.to_string().to_case(Case::Pascal);
Ident::new(&format!("__{ident_pascal}Params"), Span::call_site())
} else {
Ident::new(&format!("__{route}Params"), Span::call_site())
};
let (params_idents, params_types): (Vec<_>, Vec<_>) = extract_params(signature).unzip();
let (result_type, error_type) = unwrap_result_type(signature, unwrap_result);
Self {
route,
entry_id,
export,
#[cfg(feature = "ethexe")]
payable,
overrides,
override_entry_id,
impl_fn,
ident,
params_struct_ident,
params_idents,
params_types,
result_type,
error_type,
sails_path,
scale,
#[cfg(feature = "ethexe")]
ethabi,
}
}
pub(crate) fn is_async(&self) -> bool {
self.impl_fn.sig.asyncness.is_some()
}
pub(crate) fn is_query(&self) -> bool {
self.impl_fn
.sig
.receiver()
.is_none_or(|r| r.mutability.is_none())
}
pub(crate) fn has_scale_codec(&self) -> bool {
self.scale
}
#[cfg(feature = "ethexe")]
pub(crate) fn has_ethabi_codec(&self) -> bool {
self.ethabi
}
#[cfg(not(feature = "ethexe"))]
#[allow(dead_code)]
pub(crate) fn has_ethabi_codec(&self) -> bool {
false
}
pub(crate) fn result_type_with_value(&self) -> (&Type, bool) {
let result_type = &self.result_type;
let (result_type, reply_with_value) = extract_reply_type_with_value(result_type)
.map_or_else(|| (result_type, false), |ty| (ty, true));
if reply_with_value && self.is_query() {
abort!(
self.result_type.span(),
"using `CommandReply` type in a query is not allowed"
);
}
(result_type, reply_with_value)
}
pub(crate) fn params(&self) -> impl Iterator<Item = (&&Ident, &&Type)> {
self.params_idents.iter().zip(self.params_types.iter())
}
pub(crate) fn params_idents(&self) -> &[&Ident] {
self.params_idents.as_slice()
}
pub(crate) fn params_types(&self) -> &[&Type] {
self.params_types.as_slice()
}
#[cfg(feature = "ethexe")]
pub(crate) fn route_camel_case(&self) -> String {
use convert_case::{Boundary, Case, Casing};
self.route
.set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper])
.to_case(Case::Camel)
}
#[cfg(feature = "ethexe")]
pub(crate) fn payable_check(&self) -> proc_macro2::TokenStream {
if !self.payable {
let sails_path = self.sails_path;
let msg = format!("'{}' accepts no value", self.ident);
quote::quote! {
#[cfg(target_arch = "wasm32")]
if #sails_path::gstd::msg::value() > 0 {
core::panic!(#msg);
}
}
} else {
quote::quote!()
}
}
}
#[cfg(feature = "ethexe")]
pub mod validation {
use proc_macro_error::abort;
use proc_macro2::Span;
const SOL_KEYWORDS: &[&str] = &[
"abi",
"abstract",
"addmod",
"address",
"after",
"alias",
"anonymous",
"apply",
"as",
"assembly",
"assert",
"auto",
"block",
"blockhash",
"bool",
"break",
"byte",
"bytes",
"calldata",
"case",
"catch",
"constant",
"constructor",
"continue",
"contract",
"copyof",
"days",
"default",
"define",
"delete",
"do",
"ecrecover",
"else",
"emit",
"enum",
"ether",
"event",
"external",
"false",
"final",
"fixed",
"for",
"function",
"gasleft",
"gwei",
"hex",
"hours",
"if",
"immutable",
"implements",
"import",
"in",
"indexed",
"inline",
"int",
"interface",
"internal",
"is",
"keccak256",
"let",
"library",
"macro",
"mapping",
"match",
"memory",
"minutes",
"modifier",
"msg",
"mulmod",
"mutable",
"new",
"null",
"of",
"override",
"partial",
"payable",
"pragma",
"private",
"promise",
"public",
"pure",
"reference",
"relocatable",
"require",
"return",
"returns",
"revert",
"ripemd160",
"sealed",
"seconds",
"selfdestruct",
"sha256",
"sizeof",
"static",
"storage",
"string",
"struct",
"super",
"supports",
"switch",
"this",
"throw",
"true",
"try",
"tx",
"type",
"typedef",
"typeof",
"ufixed",
"uint",
"unchecked",
"unicode",
"using",
"var",
"view",
"virtual",
"weeks",
"wei",
"while",
"years",
];
fn is_reserved(s: &str) -> bool {
let s = s.to_ascii_lowercase();
if SOL_KEYWORDS.binary_search(&s.as_str()).is_ok() {
return true;
}
if let Some(num) = s.strip_prefix("bytes").and_then(|x| x.parse::<u8>().ok()) {
return (1..=32).contains(&num);
}
if let Some(rest) = s.strip_prefix("uint").or_else(|| s.strip_prefix("int"))
&& let Ok(n) = rest.parse::<u16>()
{
return n == 8 || (16..=256).contains(&n) && n % 8 == 0;
}
if let Some(rest) = s.strip_prefix("ufixed").or_else(|| s.strip_prefix("fixed"))
&& let Some((m_str, n_str)) = rest.split_once('x')
&& let (Ok(m), Ok(n)) = (m_str.parse::<u16>(), n_str.parse::<u8>())
&& (8..=256).contains(&m)
&& m % 8 == 0
&& n <= 80
{
return true;
}
false
}
pub fn validate_identifier(name: &str, span: Span, type_of_ident: &str) {
if is_reserved(name) {
abort!(
span,
"The name '{}' cannot be used for a {} because it is a reserved keyword in Solidity.",
name,
type_of_ident;
help = "Please rename this item to avoid compilation errors in the generated Solidity contract."
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_reserved_works() {
assert!(is_reserved("address"));
assert!(is_reserved("contract"));
assert!(is_reserved("function"));
assert!(!is_reserved("myfunction"));
assert!(is_reserved("bytes1"));
assert!(is_reserved("bytes16"));
assert!(is_reserved("bytes32"));
assert!(!is_reserved("bytes0"));
assert!(!is_reserved("bytes33"));
assert!(!is_reserved("sbytes1"));
assert!(is_reserved("uint8"));
assert!(is_reserved("int8"));
assert!(is_reserved("uint16"));
assert!(is_reserved("int24"));
assert!(is_reserved("uint256"));
assert!(is_reserved("int"));
assert!(is_reserved("uint"));
assert!(!is_reserved("uint9"));
assert!(!is_reserved("int17"));
assert!(!is_reserved("uint257"));
assert!(!is_reserved("uint249"));
assert!(is_reserved("fixed128x18"));
assert!(is_reserved("ufixed256x80"));
assert!(is_reserved("fixed8x1"));
assert!(is_reserved("ufixed256x0"));
assert!(is_reserved("fixed"));
assert!(is_reserved("ufixed"));
assert!(!is_reserved("fixed128x81")); assert!(!is_reserved("fixed264x18")); assert!(!is_reserved("fixed129x18")); assert!(!is_reserved("fixed4x1")); assert!(!is_reserved("fixed128"));
assert!(!is_reserved("fixed128xN"));
}
}
}