Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion soroban-sdk-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use syn::{
parse_macro_input, parse_str, spanned::Spanned, Data, DeriveInput, Error, Expr, Fields,
ItemImpl, ItemStruct, LitStr, Path, Type, Visibility,
};
use syn_ext::HasFnsItem;
use syn_ext::{impl_remove_fns_without_blocks, HasFnsItem};

use soroban_spec_rust::{generate_from_wasm, GenerateFromFileError};

Expand Down Expand Up @@ -262,6 +262,8 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
#[#crate_path::contractspecfn(name = #ty_str)]
#[#crate_path::contractimpl_trait_check]
#[#crate_path::contractimpl_remove_fns_without_blocks]
#imp
#derived_ok
};
Expand All @@ -282,6 +284,27 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
}
}

#[proc_macro_attribute]
pub fn contractimpl_remove_fns_without_blocks(
_metadata: TokenStream,
input: TokenStream,
) -> TokenStream {
let imp = parse_macro_input!(input as ItemImpl);
let imp = impl_remove_fns_without_blocks(&imp);
quote!(#imp).into()
}

#[proc_macro_attribute]
pub fn contractimpl_trait_check(_metadata: TokenStream, input: TokenStream) -> TokenStream {
let imp = parse_macro_input!(input as ItemImpl);
let trait_check = syn_ext::impl_generate_trait_check(&imp);
quote! {
#imp
#trait_check
}
.into()
}

#[derive(Debug, FromMeta)]
struct MetadataArgs {
key: String,
Expand Down
145 changes: 142 additions & 3 deletions soroban-sdk-macros/src/syn_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use std::collections::HashMap;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Comma,
AngleBracketedGenericArguments, Attribute, GenericArgument, Path, PathArguments, PathSegment,
ReturnType, Token, TypePath,
token::{Brace, Comma},
AngleBracketedGenericArguments, Attribute, Block, GenericArgument, Path, PathArguments,
PathSegment, ReturnType, Signature, Token, TypePath,
};
use syn::{
spanned::Spanned, token::And, Error, FnArg, Ident, ImplItem, ImplItemFn, ItemImpl, ItemTrait,
Expand All @@ -22,6 +22,9 @@ pub fn impl_pub_methods(imp: &ItemImpl) -> Vec<ImplItemFn> {
.iter()
.filter_map(|i| match i {
ImplItem::Fn(m) => Some(m.clone()),
ImplItem::Verbatim(tokens) => syn::parse2::<FnWithoutBlock>(tokens.clone())
.ok()
.map(Into::into),
_ => None,
})
.filter(|m| imp.trait_.is_some() || matches!(m.vis, Visibility::Public(_)))
Expand Down Expand Up @@ -316,3 +319,139 @@ fn flatten_associated_items_in_impl_fns(imp: &mut ItemImpl) {
pub fn ty_to_safe_ident_str(ty: &Type) -> String {
quote!(#ty).to_string().replace(' ', "").replace(':', "_")
}

/// Check if the function has a block that contains only a semicolon
/// (similar to the inherent crate's inherit_default_implementation)
pub fn impl_remove_fns_without_blocks(imp: &ItemImpl) -> ItemImpl {
let mut imp = imp.clone();
imp.items.retain(|item| match item {
ImplItem::Verbatim(tokens) => syn::parse2::<FnWithoutBlock>(tokens.clone()).is_err(),
_ => true,
});
imp
}

/// Generate a trait implementation check for functions without blocks.
/// This creates a const _: () = {} block that implements the trait with
/// unimplemented!() bodies for block-less functions to ensure they match
/// the trait signatures.
pub fn impl_generate_trait_check(imp: &ItemImpl) -> TokenStream {
let Some((_, trait_path, _)) = &imp.trait_ else {
return quote!();
};

let mut trait_items = Vec::new();

for item in &imp.items {
match item {
ImplItem::Fn(f) => {
// Include functions with blocks, but change their bodies to unimplemented!()
let ImplItemFn {
attrs,
vis,
defaultness,
sig,
..
} = f;
trait_items.push(quote! {
#(#attrs)*
#[allow(unused_parameters)]
#vis
#defaultness
#sig {
unimplemented!()
}
});
}
ImplItem::Verbatim(tokens) => {
if let Ok(fn_without_block) = syn::parse2::<FnWithoutBlock>(tokens.clone()) {
let FnWithoutBlock {
attrs,
vis,
defaultness,
sig,
..
} = fn_without_block;
trait_items.push(quote! {
#(#attrs)*
#[allow(unused_parameters)]
#vis
#defaultness
#sig {
unimplemented!()
}
});
} else {
// For verbatim that are not functions, quote as-is
trait_items.push(quote! { #tokens });
}
}
_ => {
// For all other impl items (types, constants, etc.), quote as-is
trait_items.push(quote! { #item });
}
}
}

if trait_items.is_empty() {
return quote!();
}

quote! {
const _: () = {
struct TraitCheckType;

impl #trait_path for TraitCheckType {
#(#trait_items)*
}
};
}
}

pub struct FnWithoutBlock {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub defaultness: Option<Token![default]>,
pub sig: Signature,
pub semi_token: Token![;],
}

impl Parse for FnWithoutBlock {
fn parse(input: ParseStream) -> Result<Self, Error> {
Ok(Self {
attrs: input.call(Attribute::parse_outer)?,
vis: input.parse()?,
defaultness: input.parse()?,
sig: input.parse()?,
semi_token: input.parse()?,
})
}
}

impl From<FnWithoutBlock> for ImplItemFn {
fn from(fn_without_block: FnWithoutBlock) -> Self {
ImplItemFn {
attrs: fn_without_block.attrs,
vis: fn_without_block.vis,
defaultness: fn_without_block.defaultness,
sig: fn_without_block.sig,
block: Block {
brace_token: Brace::default(),
stmts: vec![],
},
}
}
}

impl From<ImplItemFn> for FnWithoutBlock {
fn from(impl_item_fn: ImplItemFn) -> Self {
let span = impl_item_fn.span();
FnWithoutBlock {
attrs: impl_item_fn.attrs,
vis: impl_item_fn.vis,
defaultness: impl_item_fn.defaultness,
sig: impl_item_fn.sig,
semi_token: Token![;](span),
}
}
}
97 changes: 97 additions & 0 deletions soroban-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,14 @@ pub use soroban_sdk_macros::contract;
/// Functions that are publicly accessible in the implementation are invocable
/// by other contracts, or directly by transactions, when deployed.
///
/// All trait functions visiible in the impl block are invocable. Trait
/// default functions are only invocable if specified without a block. See the
/// example below.
///
/// ### Examples
///
/// #### Defining a function
///
/// Define a contract with one function, `hello`, and call it from within a test
/// using the generated client.
///
Expand Down Expand Up @@ -399,8 +405,99 @@ pub use soroban_sdk_macros::contract;
/// # #[cfg(not(feature = "testutils"))]
/// # fn main() { }
/// ```
///
/// #### Defining a function using a trait
///
/// Define a contract with one function, `hello`, that is defined by a trait.
///
/// ```
/// use soroban_sdk::{contract, contractimpl, vec, symbol_short, BytesN, Env, Symbol, Vec};
///
/// pub trait HelloTrait {
/// fn hello(env: Env, to: Symbol) -> Vec<Symbol>;
/// }
///
/// #[contract]
/// pub struct HelloContract;
///
/// #[contractimpl]
/// impl HelloTrait for HelloContract {
/// fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
/// vec![&env, symbol_short!("Hello"), to]
/// }
/// }
///
/// #[test]
/// fn test() {
/// # }
/// # #[cfg(feature = "testutils")]
/// # fn main() {
/// let env = Env::default();
/// let contract_id = env.register(HelloContract, ());
/// let client = HelloContractClient::new(&env, &contract_id);
///
/// let words = client.hello(&symbol_short!("Dev"));
///
/// assert_eq!(words, vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),]);
/// }
/// # #[cfg(not(feature = "testutils"))]
/// # fn main() { }
/// ```
///
/// #### Defining a function using a trait with a default implementation
///
/// Define a contract with one function, `hello`, that is defined by a trait, and has a default
/// implementation in the trait that the contract will make invocable.
///
/// ```
/// use soroban_sdk::{contract, contractimpl, vec, symbol_short, BytesN, Env, Symbol, Vec};
///
/// pub trait HelloTrait {
/// fn hello(env: Env, to: Symbol) -> Vec<Symbol> {
/// vec![&env, symbol_short!("Hello"), to]
/// }
/// }
///
/// #[contract]
/// pub struct HelloContract;
///
/// #[contractimpl]
/// impl HelloTrait for HelloContract {
/// // To make a trait default function invocable it must be specified
/// // in the impl block with no code block.
/// fn hello(env: Env, to: Symbol) -> Vec<Symbol>;
/// }
///
/// #[test]
/// fn test() {
/// # }
/// # #[cfg(feature = "testutils")]
/// # fn main() {
/// let env = Env::default();
/// let contract_id = env.register(HelloContract, ());
/// let client = HelloContractClient::new(&env, &contract_id);
///
/// let words = client.hello(&symbol_short!("Dev"));
///
/// assert_eq!(words, vec![&env, symbol_short!("Hello"), symbol_short!("Dev"),]);
/// }
/// # #[cfg(not(feature = "testutils"))]
/// # fn main() { }
/// ```
pub use soroban_sdk_macros::contractimpl;

/// Removes functions without blocks from impl blocks that use functions without blocks, terminated
/// with a semi-colon, as a way to signal that a default trait function exists and should be
/// exported.
///
/// This macro is used internally and is not intended to be used directly by contracts.
#[doc(hidden)]
pub use soroban_sdk_macros::contractimpl_remove_fns_without_blocks;

/// This macro is used internally and is not intended to be used directly by contracts.
#[doc(hidden)]
pub use soroban_sdk_macros::contractimpl_trait_check;

/// Adds a serialized SCMetaEntry::SCMetaV0 to the WASM contracts custom section
/// under the section name 'contractmetav0'. Contract developers can use this to
/// append metadata to their contract.
Expand Down
17 changes: 17 additions & 0 deletions tests-expanded/test_account_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,23 @@ impl CustomAccountInterface for Contract {
Ok(())
}
}
const _: () = {
struct TraitCheckType;
impl CustomAccountInterface for TraitCheckType {
type Error = Error;
type Signature = ();
#[allow(non_snake_case)]
#[allow(unused_parameters)]
fn __check_auth(
_env: Env,
_signature_payload: Hash<32>,
_signatures: Self::Signature,
_auth_contexts: Vec<Context>,
) -> Result<(), Error> {
::core::panicking::panic("not implemented")
}
}
};
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(non_snake_case)]
Expand Down
17 changes: 17 additions & 0 deletions tests-expanded/test_account_wasm32v1-none.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ impl CustomAccountInterface for Contract {
Ok(())
}
}
const _: () = {
struct TraitCheckType;
impl CustomAccountInterface for TraitCheckType {
type Error = Error;
type Signature = ();
#[allow(non_snake_case)]
#[allow(unused_parameters)]
fn __check_auth(
_env: Env,
_signature_payload: Hash<32>,
_signatures: Self::Signature,
_auth_contexts: Vec<Context>,
) -> Result<(), Error> {
::core::panicking::panic("not implemented")
}
}
};
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(non_snake_case)]
Expand Down
Loading
Loading