Skip to content

Commit 9e1c6c6

Browse files
committed
Propagate sol! derive attrs to non-interface JSON ABI items
1 parent 1ff6a56 commit 9e1c6c6

File tree

4 files changed

+269
-74
lines changed

4 files changed

+269
-74
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"components": [
6+
{
7+
"internalType": "uint256",
8+
"name": "value",
9+
"type": "uint256"
10+
},
11+
{
12+
"internalType": "enum GlobalEnum",
13+
"name": "enum_",
14+
"type": "uint8"
15+
}
16+
],
17+
"internalType": "struct GlobalStruct",
18+
"name": "payload",
19+
"type": "tuple"
20+
}
21+
],
22+
"name": "GlobalError",
23+
"type": "error"
24+
},
25+
{
26+
"anonymous": false,
27+
"inputs": [
28+
{
29+
"components": [
30+
{
31+
"internalType": "uint256",
32+
"name": "value",
33+
"type": "uint256"
34+
},
35+
{
36+
"internalType": "enum GlobalEnum",
37+
"name": "enum_",
38+
"type": "uint8"
39+
}
40+
],
41+
"indexed": false,
42+
"internalType": "struct GlobalStruct",
43+
"name": "payload",
44+
"type": "tuple"
45+
},
46+
{
47+
"indexed": false,
48+
"internalType": "GlobalUDT",
49+
"name": "amount",
50+
"type": "uint256"
51+
}
52+
],
53+
"name": "GlobalEvent",
54+
"type": "event"
55+
},
56+
{
57+
"inputs": [
58+
{
59+
"components": [
60+
{
61+
"internalType": "enum GlobalEnum",
62+
"name": "kind",
63+
"type": "uint8"
64+
},
65+
{
66+
"internalType": "uint256",
67+
"name": "count",
68+
"type": "uint256"
69+
}
70+
],
71+
"internalType": "struct Interface.InterfaceStruct",
72+
"name": "data",
73+
"type": "tuple"
74+
}
75+
],
76+
"name": "emitGlobalEvent",
77+
"outputs": [],
78+
"stateMutability": "nonpayable",
79+
"type": "function"
80+
},
81+
{
82+
"inputs": [],
83+
"name": "triggerError",
84+
"outputs": [],
85+
"stateMutability": "pure",
86+
"type": "function"
87+
}
88+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
library Interface {
2+
struct InterfaceStruct {
3+
GlobalEnum kind;
4+
uint256 count;
5+
}
6+
}
7+
8+
interface ContractUsingGlobals {
9+
error GlobalError(GlobalStruct payload);
10+
11+
event GlobalEvent(GlobalStruct payload, GlobalUDT amount);
12+
13+
function emitGlobalEvent(Interface.InterfaceStruct memory data) external;
14+
function triggerError() external pure;
15+
}
16+
type GlobalEnum is uint8;
17+
type GlobalUDT is uint256;
18+
struct GlobalStruct {
19+
uint256 value;
20+
GlobalEnum enum_;
21+
}
22+

crates/sol-macro-input/src/json.rs

Lines changed: 104 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,22 @@ impl SolInput {
1818

1919
let mut abi = abi.ok_or_else(|| syn::Error::new(name.span(), "ABI not found in JSON"))?;
2020
let sol = abi_to_sol(&name, &mut abi);
21-
let mut all_tokens = tokens_for_sol(&name, &sol)?.into_iter();
21+
let all_tokens = tokens_for_sol(&name, &sol)?;
22+
let mut ast: ast::File = syn::parse2(all_tokens).map_err(|e| {
23+
let msg = format!(
24+
"failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\
25+
This is a bug. We would appreciate a bug report: \
26+
https://github.com/alloy-rs/core/issues/new/choose"
27+
);
28+
syn::Error::new(name.span(), msg)
29+
})?;
2230

2331
let (inner_attrs, attrs) = attrs
2432
.into_iter()
2533
.partition::<Vec<_>, _>(|attr| matches!(attr.style, AttrStyle::Inner(_)));
2634

2735
let (derives, sol_derives) = extract_derive_attrs(&attrs);
2836

29-
let mut library_tokens_iter = all_tokens
30-
.by_ref()
31-
.take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "interface"))
32-
.skip_while(|tt| matches!(tt, TokenTree::Ident(id) if id == "library"))
33-
.peekable();
34-
35-
let library_tokens = library_tokens_iter.by_ref();
36-
37-
let mut libraries = Vec::new();
38-
39-
while library_tokens.peek().is_some() {
40-
let sol_library_tokens: TokenStream = std::iter::once(TokenTree::Ident(id("library")))
41-
.chain(
42-
library_tokens
43-
.take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "library")),
44-
)
45-
.collect();
46-
47-
let tokens = quote! {
48-
#(#derives)*
49-
#(#sol_derives)*
50-
#sol_library_tokens
51-
};
52-
53-
libraries.push(tokens);
54-
}
55-
let sol_interface_tokens: TokenStream =
56-
std::iter::once(TokenTree::Ident(id("interface"))).chain(all_tokens).collect();
5737
let bytecode = bytecode.map(|bytes| {
5838
let s = bytes.to_string();
5939
quote!(bytecode = #s,)
@@ -63,44 +43,34 @@ impl SolInput {
6343
quote!(deployed_bytecode = #s)
6444
});
6545

66-
let attrs_iter = attrs.iter();
67-
let doc_str = format!(
68-
"\n\n\
69-
Generated by the following Solidity interface...
70-
```solidity
71-
{sol}
72-
```
73-
74-
...which was generated by the following JSON ABI:
75-
```json
76-
{json_s}
77-
```",
78-
json_s = serde_json::to_string_pretty(&abi).unwrap()
79-
);
80-
let tokens = quote! {
81-
#(#inner_attrs)*
82-
#(#libraries)*
83-
84-
#(#attrs_iter)*
85-
#[doc = #doc_str]
86-
#[sol(#bytecode #deployed_bytecode)]
87-
#sol_interface_tokens
46+
let ctx = ApplyAttrsCtx {
47+
derives: &derives,
48+
sol_derives: &sol_derives,
49+
interface_attrs: &attrs,
50+
bytecode: bytecode.as_ref(),
51+
deployed_bytecode: deployed_bytecode.as_ref(),
52+
sol: &sol,
53+
abi: &abi,
8854
};
89-
90-
let ast: ast::File = syn::parse2(tokens).map_err(|e| {
91-
let msg = format!(
92-
"failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\
93-
This is a bug. We would appreciate a bug report: \
94-
https://github.com/alloy-rs/core/issues/new/choose"
95-
);
96-
syn::Error::new(name.span(), msg)
97-
})?;
55+
apply_attrs_to_items(&mut ast.items, &ctx);
56+
ast.attrs.extend(inner_attrs);
9857

9958
let kind = SolInputKind::Sol(ast);
10059
Ok(SolInput { attrs, path, kind })
10160
}
10261
}
10362

63+
/// Shared context for applying user attributes to ABI-derived items.
64+
struct ApplyAttrsCtx<'a> {
65+
derives: &'a [&'a syn::Attribute],
66+
sol_derives: &'a [&'a syn::Attribute],
67+
interface_attrs: &'a [syn::Attribute],
68+
bytecode: Option<&'a TokenStream>,
69+
deployed_bytecode: Option<&'a TokenStream>,
70+
sol: &'a str,
71+
abi: &'a JsonAbi,
72+
}
73+
10474
// doesn't parse Json
10575

10676
fn abi_to_sol(name: &Ident, abi: &mut JsonAbi) -> String {
@@ -153,18 +123,87 @@ fn extract_derive_attrs(attrs: &[syn::Attribute]) -> (Vec<&syn::Attribute>, Vec<
153123
})
154124
}
155125

156-
#[inline]
157-
#[track_caller]
158-
fn id(s: impl AsRef<str>) -> Ident {
159-
// Ident::new panics on Rust keywords and `r#` prefixes
160-
syn::parse_str(s.as_ref()).unwrap()
126+
/// Applies derive/`sol` attributes to ABI-derived items.
127+
///
128+
/// - Non-interface contracts, structs, enums, and UDVTs get the user-specified derive and `sol`
129+
/// attributes cloned onto them.
130+
/// - The single interface gets the outer attributes, a generated doc (including the original
131+
/// Solidity/JSON ABI), and the `#[sol(bytecode = ..., deployed_bytecode = ...)]` attribute.
132+
fn apply_attrs_to_items(items: &mut [ast::Item], ctx: &ApplyAttrsCtx<'_>) {
133+
for item in items {
134+
match item {
135+
ast::Item::Contract(contract) if contract.kind.is_interface() => {
136+
apply_interface_attrs(contract, ctx);
137+
}
138+
ast::Item::Contract(contract) => {
139+
extend_attrs(&mut contract.attrs, ctx.derives, ctx.sol_derives);
140+
}
141+
ast::Item::Struct(strukt) => {
142+
extend_attrs(&mut strukt.attrs, ctx.derives, ctx.sol_derives);
143+
}
144+
ast::Item::Udt(udt) => {
145+
extend_attrs(&mut udt.attrs, ctx.derives, ctx.sol_derives);
146+
}
147+
// Globals from `to_sol` are only structs, UDVTs; enums are flattened to `uint8`,
148+
// while errors/functions/events,etc are emitted in the interface.
149+
_ => debug_assert!(false, "unexpected global item type"),
150+
}
151+
}
152+
}
153+
154+
/// Merge user outer attrs with generated docs/metadata for the sole interface.
155+
fn apply_interface_attrs(contract: &mut ast::ItemContract, ctx: &ApplyAttrsCtx<'_>) {
156+
let bytecode = ctx.bytecode;
157+
let deployed_bytecode = ctx.deployed_bytecode;
158+
let doc_str = format!(
159+
"\n\n\
160+
Generated by the following Solidity interface...\
161+
```solidity\
162+
{sol}\
163+
```\
164+
\n\
165+
...which was generated by the following JSON ABI:\
166+
```json\
167+
{json_s}\
168+
```",
169+
sol = ctx.sol,
170+
json_s = serde_json::to_string_pretty(ctx.abi).unwrap(),
171+
);
172+
let doc_attr: syn::Attribute = syn::parse_quote!(#[doc = #doc_str]);
173+
let sol_attr: syn::Attribute = syn::parse_quote!(#[sol(#bytecode #deployed_bytecode)]);
174+
175+
let mut merged = ctx.interface_attrs.to_vec();
176+
merged.push(doc_attr);
177+
merged.push(sol_attr);
178+
contract.attrs = merged;
179+
}
180+
181+
/// Clone user-specified `derive`/`sol(...)` attributes onto the given item.
182+
/// Used for globals (structs/UDVTs) and non-interface contracts emitted by `to_sol`.
183+
fn extend_attrs(
184+
attrs: &mut Vec<syn::Attribute>,
185+
derives: &[&syn::Attribute],
186+
sol_derives: &[&syn::Attribute],
187+
) {
188+
attrs.reserve(derives.len() + sol_derives.len());
189+
for attr in derives {
190+
attrs.push((*attr).clone());
191+
}
192+
for attr in sol_derives {
193+
attrs.push((*attr).clone());
194+
}
161195
}
162196

163197
#[cfg(test)]
164198
mod tests {
165199
use super::*;
166200
use std::path::{Path, PathBuf};
167201

202+
fn id(s: impl AsRef<str>) -> Ident {
203+
// Ident::new panics on Rust keywords and `r#` prefixes
204+
syn::parse_str(s.as_ref()).unwrap()
205+
}
206+
168207
#[test]
169208
#[cfg_attr(miri, ignore = "no fs")]
170209
fn abi() {

0 commit comments

Comments
 (0)