Skip to content

Commit f7c4477

Browse files
Copy trait function docs to overridden functions that lack their own
When using #[contractimpl(contracttrait)], overridden trait functions that lack their own doc comments now inherit docs from the trait function definition. This ensures contract specs are fully documented when using well-documented traits. Closes #1653
1 parent 54b94fa commit f7c4477

File tree

6 files changed

+119
-7
lines changed

6 files changed

+119
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

soroban-sdk-macros/src/derive_contractimpl_trait_default_fns_not_overridden.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
attribute::is_attr_doc,
23
default_crate_path,
34
derive_args::derive_args_impl,
45
derive_client::derive_client_impl,
@@ -9,7 +10,7 @@ use crate::{
910
use darling::{ast::NestedMeta, Error, FromMeta};
1011
use proc_macro2::{Ident, TokenStream as TokenStream2};
1112
use quote::quote;
12-
use std::collections::HashSet;
13+
use std::collections::{HashMap, HashSet};
1314
use syn::{LitStr, Path, Type};
1415

1516
// See soroban-sdk/docs/contracttrait.md for documentation on how this works.
@@ -20,8 +21,10 @@ struct Args {
2021
crate_path: Path,
2122
trait_ident: Path,
2223
trait_default_fns: Vec<LitStr>,
24+
trait_all_fns: Vec<LitStr>,
2325
impl_ident: Ident,
2426
impl_fns: Vec<LitStr>,
27+
impl_fns_with_docs: Vec<LitStr>,
2528
client_name: String,
2629
args_name: String,
2730
spec_name: Type,
@@ -84,5 +87,33 @@ fn derive(args: &Args) -> Result<TokenStream2, Error> {
8487
fns.iter().map(|f| &f.ident),
8588
));
8689

90+
// Generate spec for overridden functions, inheriting trait docs for
91+
// functions that lack their own doc comments.
92+
let trait_all_fns = syn_ext::strs_to_fns(&args.trait_all_fns)?;
93+
let trait_fn_docs: HashMap<String, Vec<syn::Attribute>> = trait_all_fns
94+
.into_iter()
95+
.map(|f| (f.ident.to_string(), f.attrs))
96+
.collect();
97+
98+
let impl_fns_parsed = syn_ext::strs_to_fns(&args.impl_fns_with_docs)?;
99+
let impl_fns_for_spec: Vec<syn_ext::Fn> = impl_fns_parsed
100+
.into_iter()
101+
.map(|f| {
102+
let has_docs = f.attrs.iter().any(|a| is_attr_doc(a));
103+
if !has_docs {
104+
if let Some(trait_attrs) = trait_fn_docs.get(&f.ident.to_string()) {
105+
return syn_ext::Fn {
106+
ident: f.ident,
107+
attrs: trait_attrs.clone(),
108+
inputs: f.inputs,
109+
output: f.output,
110+
};
111+
}
112+
}
113+
f
114+
})
115+
.collect();
116+
output.extend(derive_fns_spec(&args.spec_name, &impl_fns_for_spec, spec_export));
117+
87118
Ok(output)
88119
}

soroban-sdk-macros/src/derive_contractimpl_trait_macro.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ fn derive(args: &Args, input: &ItemTrait) -> Result<TokenStream2, Error> {
5656
_ => None,
5757
});
5858

59+
// Serialize all trait functions' docs+sigs (including non-default ones) so
60+
// that overridden functions can inherit trait docs when they lack their own.
61+
let all_fns = input.items.iter().filter_map(|i| match i {
62+
TraitItem::Fn(TraitItemFn { sig, attrs, .. }) => {
63+
let doc_attrs: Vec<_> = attrs.iter().filter(|a| is_attr_doc(a)).collect();
64+
Some(quote!(#(#doc_attrs)* #sig).to_token_stream().to_string())
65+
}
66+
_ => None,
67+
});
68+
5969
let macro_ident = macro_ident(&input.ident);
6070

6171
let output = quote! {
@@ -67,15 +77,18 @@ fn derive(args: &Args, input: &ItemTrait) -> Result<TokenStream2, Error> {
6777
$trait_ident:path,
6878
$impl_ident:ty,
6979
$impl_fns:expr,
80+
$impl_fns_with_docs:expr,
7081
$client_name:literal,
7182
$args_name:literal,
7283
$spec_name:literal $(,)?
7384
) => {
7485
#path::contractimpl_trait_default_fns_not_overridden!(
7586
trait_ident = $trait_ident,
7687
trait_default_fns = [#(#fns),*],
88+
trait_all_fns = [#(#all_fns),*],
7789
impl_ident = $impl_ident,
7890
impl_fns = $impl_fns,
91+
impl_fns_with_docs = $impl_fns_with_docs,
7992
client_name = $client_name,
8093
args_name = $args_name,
8194
spec_name = $spec_name,
@@ -99,11 +112,23 @@ pub fn generate_call_to_contractimpl_for_trait(
99112
spec_ident: &str,
100113
) -> TokenStream2 {
101114
let impl_fn_idents = pub_methods.iter().map(|f| f.sig.ident.to_string());
115+
// Serialize impl functions with their doc attrs so the macro bridge can
116+
// generate spec entries, falling back to trait docs when the impl function
117+
// lacks its own.
118+
let impl_fn_strs: Vec<String> = pub_methods
119+
.iter()
120+
.map(|f| {
121+
let doc_attrs: Vec<_> = f.attrs.iter().filter(|a| is_attr_doc(a)).collect();
122+
let sig = &f.sig;
123+
quote!(#(#doc_attrs)* #sig).to_token_stream().to_string()
124+
})
125+
.collect();
102126
quote! {
103127
#trait_ident!(
104128
#trait_ident,
105129
#impl_ident,
106130
[#(#impl_fn_idents),*],
131+
[#(#impl_fn_strs),*],
107132
#client_ident,
108133
#args_ident,
109134
#spec_ident,

soroban-sdk-macros/src/lib.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,24 @@ pub fn contractimpl(metadata: TokenStream, input: TokenStream) -> TokenStream {
259259

260260
match derived {
261261
Ok(derived_ok) => {
262-
let mut output = quote! {
263-
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
264-
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
265-
#[#crate_path::contractspecfn(name = #ty_str)]
266-
#imp
267-
#derived_ok
262+
// When contracttrait is true, spec generation for overridden
263+
// functions is handled by the macro bridge (so that trait docs can
264+
// be used as fallback), so contractspecfn is not applied here.
265+
let mut output = if args.contracttrait {
266+
quote! {
267+
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
268+
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
269+
#imp
270+
#derived_ok
271+
}
272+
} else {
273+
quote! {
274+
#[#crate_path::contractargs(name = #args_ident, impl_only = true)]
275+
#[#crate_path::contractclient(crate_path = #crate_path_str, name = #client_ident, impl_only = true)]
276+
#[#crate_path::contractspecfn(name = #ty_str)]
277+
#imp
278+
#derived_ok
279+
}
268280
};
269281

270282
// See soroban-sdk/docs/contracttrait.md for documentation on how

tests/contracttrait_impl_partial/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ test_contracttrait_trait = {path = "../contracttrait_trait"}
1818
[dev-dependencies]
1919
soroban-sdk = {path = "../../soroban-sdk", features = ["testutils"]}
2020
test_contracttrait_trait = {path = "../contracttrait_trait"}
21+
stellar-xdr.workspace = true

tests/contracttrait_impl_partial/src/lib.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ impl AllTypes for Contract {
2020
v
2121
}
2222

23+
/// Custom env param docs.
2324
fn test_env_param(_env: &Env) -> u32 {
2425
100
2526
}
@@ -37,6 +38,47 @@ mod test {
3738
use super::*;
3839
use soroban_sdk::{map, symbol_short, testutils::Address as _, vec, Env};
3940

41+
#[test]
42+
fn test_spec_docs_inherited_from_trait() {
43+
use stellar_xdr::curr as stellar_xdr;
44+
use stellar_xdr::{Limits, ReadXdr, ScSpecEntry};
45+
46+
// An overridden function without its own docs should inherit trait docs.
47+
let entry = ScSpecEntry::from_xdr(Contract::spec_xdr_test_u32(), Limits::none()).unwrap();
48+
let ScSpecEntry::FunctionV0(func) = entry else {
49+
panic!("expected FunctionV0");
50+
};
51+
assert_eq!(
52+
func.doc.to_utf8_string().unwrap(),
53+
"Test u32 values.\nReturns the input unchanged."
54+
);
55+
56+
// A non-overridden default function should also have trait docs.
57+
let entry = ScSpecEntry::from_xdr(Contract::spec_xdr_test_i32(), Limits::none()).unwrap();
58+
let ScSpecEntry::FunctionV0(func) = entry else {
59+
panic!("expected FunctionV0");
60+
};
61+
assert_eq!(func.doc.to_utf8_string().unwrap(), "Test i32 values.");
62+
63+
// An overridden function without docs and a trait function without docs
64+
// should have empty docs.
65+
let entry =
66+
ScSpecEntry::from_xdr(Contract::spec_xdr_test_string(), Limits::none()).unwrap();
67+
let ScSpecEntry::FunctionV0(func) = entry else {
68+
panic!("expected FunctionV0");
69+
};
70+
assert_eq!(func.doc.to_utf8_string().unwrap(), "");
71+
72+
// An overridden function with its own docs should use its own docs,
73+
// not the trait docs.
74+
let entry =
75+
ScSpecEntry::from_xdr(Contract::spec_xdr_test_env_param(), Limits::none()).unwrap();
76+
let ScSpecEntry::FunctionV0(func) = entry else {
77+
panic!("expected FunctionV0");
78+
};
79+
assert_eq!(func.doc.to_utf8_string().unwrap(), "Custom env param docs.");
80+
}
81+
4082
#[test]
4183
fn test_partial_override() {
4284
let e = Env::default();

0 commit comments

Comments
 (0)