1use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
2use proc_macro2::TokenStream;
3use quote::quote;
4use syn::{
5 Attribute, Error, LitBool, LitStr, Path, Result, Token, meta::ParseNestedMeta, parse::Parse,
6 punctuated::Punctuated,
7};
8
9const DUPLICATE_ERROR: &str = "duplicate attribute";
10const UNKNOWN_ERROR: &str = "unknown `sol` attribute";
11
12pub fn mk_doc(s: impl quote::ToTokens) -> TokenStream {
14 quote!(#[doc = #s])
15}
16
17pub fn is_doc(attr: &Attribute) -> bool {
19 attr.path().is_ident("doc")
20}
21
22pub fn is_derive(attr: &Attribute) -> bool {
24 attr.path().is_ident("derive")
25}
26
27pub fn docs(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
29 attrs.iter().filter(|a| is_doc(a))
30}
31
32pub fn docs_str(attrs: &[Attribute]) -> String {
34 let mut doc = String::new();
35 for attr in docs(attrs) {
36 let syn::Meta::NameValue(syn::MetaNameValue {
37 value: syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }),
38 ..
39 }) = &attr.meta
40 else {
41 continue;
42 };
43
44 let value = s.value();
45 if !value.is_empty() {
46 if !doc.is_empty() {
47 doc.push('\n');
48 }
49 doc.push_str(&value);
50 }
51 }
52 doc
53}
54
55pub fn derives(attrs: &[Attribute]) -> impl Iterator<Item = &Attribute> {
57 attrs.iter().filter(|a| is_derive(a))
58}
59
60pub fn derives_mapped(attrs: &[Attribute]) -> impl Iterator<Item = Path> + '_ {
63 derives(attrs).flat_map(parse_derives)
64}
65
66pub fn parse_derives(attr: &Attribute) -> Punctuated<Path, Token![,]> {
68 attr.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated).unwrap_or_default()
69}
70
71#[derive(Clone, Debug, Default, PartialEq, Eq)]
80pub struct SolAttrs {
81 pub rpc: Option<bool>,
83 pub abi: Option<bool>,
85 pub all_derives: Option<bool>,
87 pub extra_derives: Option<Vec<Path>>,
89 pub extra_methods: Option<bool>,
91 pub docs: Option<bool>,
93
94 pub alloy_sol_types: Option<Path>,
96 pub alloy_contract: Option<Path>,
98
99 pub rename: Option<LitStr>,
102 pub rename_all: Option<CasingStyle>,
105
106 pub bytecode: Option<LitStr>,
108 pub deployed_bytecode: Option<LitStr>,
110
111 pub type_check: Option<LitStr>,
113
114 pub ignore_unlinked: Option<bool>,
117}
118
119impl SolAttrs {
120 pub fn parse(attrs: &[Attribute]) -> Result<(Self, Vec<Attribute>)> {
122 let mut this = Self::default();
123 let mut others = Vec::with_capacity(attrs.len());
124 for attr in attrs {
125 if !attr.path().is_ident("sol") {
126 others.push(attr.clone());
127 continue;
128 }
129
130 attr.meta.require_list()?.parse_nested_meta(|meta| {
131 let path = meta.path.get_ident().ok_or_else(|| meta.error("expected ident"))?;
132 let s = path.to_string();
133
134 macro_rules! match_ {
135 ($($l:ident => $e:expr),* $(,)?) => {
136 match s.as_str() {
137 $(
138 stringify!($l) => if this.$l.is_some() {
139 return Err(meta.error(DUPLICATE_ERROR))
140 } else {
141 this.$l = Some($e);
142 },
143 )*
144 _ => return Err(meta.error(UNKNOWN_ERROR)),
145 }
146 };
147 }
148
149 let bool = || {
151 if let Ok(input) = meta.value() {
152 input.parse::<LitBool>().map(|lit| lit.value)
153 } else {
154 Ok(true)
155 }
156 };
157
158 let path = || meta.value()?.parse::<Path>();
160
161 let lit = || {
163 let value = meta.value()?;
164 let span = value.span();
165 let macro_string::MacroString(value) =
166 value.parse::<macro_string::MacroString>()?;
167 Ok::<_, syn::Error>(LitStr::new(&value, span))
168 };
169
170 let bytes = || {
172 let lit = lit()?;
173 if let Err(e) = hex::check(lit.value()) {
174 let msg = format!("invalid hex value: {e}");
175 return Err(Error::new(lit.span(), msg));
176 }
177 Ok(lit)
178 };
179
180 fn list<T>(
182 meta: &ParseNestedMeta<'_>,
183 parser: fn(syn::parse::ParseStream<'_>) -> Result<T>,
184 ) -> Result<Vec<T>> {
185 let content;
186 syn::parenthesized!(content in meta.input);
187 Ok(content.parse_terminated(parser, Token![,])?.into_iter().collect())
188 }
189
190 match_! {
191 rpc => bool()?,
192 abi => bool()?,
193 all_derives => bool()?,
194 extra_derives => list(&meta, Path::parse)?,
195 extra_methods => bool()?,
196 docs => bool()?,
197
198 alloy_sol_types => path()?,
199 alloy_contract => path()?,
200
201 rename => lit()?,
202 rename_all => CasingStyle::from_lit(&lit()?)?,
203
204 bytecode => bytes()?,
205 deployed_bytecode => bytes()?,
206
207 type_check => lit()?,
208 ignore_unlinked => bool()?,
209 };
210 Ok(())
211 })?;
212 }
213 Ok((this, others))
214 }
215
216 pub fn merge(&mut self, other: &SolAttrs) {
220 fn merge_opt<T: Clone>(a: &mut Option<T>, b: &Option<T>) {
221 if let Some(b) = b {
222 *a = Some(b.clone());
223 }
224 }
225 fn merge_vec<T: Clone>(a: &mut Option<Vec<T>>, b: &Option<Vec<T>>) {
226 if let Some(b) = b {
227 a.get_or_insert_default().extend(b.iter().cloned());
228 }
229 }
230 let (a, b) = (self, other);
231 merge_opt(&mut a.rpc, &b.rpc);
232 merge_opt(&mut a.abi, &b.abi);
233 merge_opt(&mut a.all_derives, &b.all_derives);
234 merge_vec(&mut a.extra_derives, &b.extra_derives);
235 merge_opt(&mut a.extra_methods, &b.extra_methods);
236 merge_opt(&mut a.docs, &b.docs);
237 merge_opt(&mut a.alloy_sol_types, &b.alloy_sol_types);
238 merge_opt(&mut a.alloy_contract, &b.alloy_contract);
239 merge_opt(&mut a.rename, &b.rename);
240 merge_opt(&mut a.rename_all, &b.rename_all);
241 merge_opt(&mut a.bytecode, &b.bytecode);
242 merge_opt(&mut a.deployed_bytecode, &b.deployed_bytecode);
243 merge_opt(&mut a.type_check, &b.type_check);
244 merge_opt(&mut a.ignore_unlinked, &b.ignore_unlinked);
245 }
246}
247
248pub trait ContainsSolAttrs {
251 fn attrs(&self) -> &[Attribute];
253
254 fn split_attrs(&self) -> syn::Result<(SolAttrs, Vec<Attribute>)> {
256 SolAttrs::parse(self.attrs())
257 }
258}
259
260impl ContainsSolAttrs for syn_solidity::File {
261 fn attrs(&self) -> &[Attribute] {
262 &self.attrs
263 }
264}
265
266impl ContainsSolAttrs for syn_solidity::ItemContract {
267 fn attrs(&self) -> &[Attribute] {
268 &self.attrs
269 }
270}
271
272impl ContainsSolAttrs for syn_solidity::ItemEnum {
273 fn attrs(&self) -> &[Attribute] {
274 &self.attrs
275 }
276}
277
278impl ContainsSolAttrs for syn_solidity::ItemError {
279 fn attrs(&self) -> &[Attribute] {
280 &self.attrs
281 }
282}
283
284impl ContainsSolAttrs for syn_solidity::ItemEvent {
285 fn attrs(&self) -> &[Attribute] {
286 &self.attrs
287 }
288}
289
290impl ContainsSolAttrs for syn_solidity::ItemFunction {
291 fn attrs(&self) -> &[Attribute] {
292 &self.attrs
293 }
294}
295
296impl ContainsSolAttrs for syn_solidity::ItemStruct {
297 fn attrs(&self) -> &[Attribute] {
298 &self.attrs
299 }
300}
301
302impl ContainsSolAttrs for syn_solidity::ItemUdt {
303 fn attrs(&self) -> &[Attribute] {
304 &self.attrs
305 }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum CasingStyle {
311 Camel,
314 Kebab,
316 Pascal,
319 ScreamingSnake,
322 Snake,
325 Lower,
327 Upper,
329 Verbatim,
331}
332
333impl CasingStyle {
334 fn from_lit(name: &LitStr) -> Result<Self> {
335 let normalized = name.value().to_upper_camel_case().to_lowercase();
336 let s = match normalized.as_ref() {
337 "camel" | "camelcase" => Self::Camel,
338 "kebab" | "kebabcase" => Self::Kebab,
339 "pascal" | "pascalcase" => Self::Pascal,
340 "screamingsnake" | "screamingsnakecase" => Self::ScreamingSnake,
341 "snake" | "snakecase" => Self::Snake,
342 "lower" | "lowercase" => Self::Lower,
343 "upper" | "uppercase" => Self::Upper,
344 "verbatim" | "verbatimcase" => Self::Verbatim,
345 s => return Err(Error::new(name.span(), format!("unsupported casing: {s}"))),
346 };
347 Ok(s)
348 }
349
350 #[allow(dead_code)]
352 pub fn apply(self, s: &str) -> String {
353 match self {
354 Self::Pascal => s.to_upper_camel_case(),
355 Self::Kebab => s.to_kebab_case(),
356 Self::Camel => s.to_lower_camel_case(),
357 Self::ScreamingSnake => s.to_shouty_snake_case(),
358 Self::Snake => s.to_snake_case(),
359 Self::Lower => s.to_snake_case().replace('_', ""),
360 Self::Upper => s.to_shouty_snake_case().replace('_', ""),
361 Self::Verbatim => s.to_owned(),
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use syn::parse_quote;
370
371 macro_rules! test_sol_attrs {
372 ($($(#[$attr:meta])* $group:ident { $($t:tt)* })+) => {$(
373 #[test]
374 $(#[$attr])*
375 fn $group() {
376 test_sol_attrs! { $($t)* }
377 }
378 )+};
379
380 ($( $(#[$attr:meta])* => $expected:expr ),+ $(,)?) => {$(
381 run_test(
382 &[$(stringify!(#[$attr])),*],
383 $expected
384 );
385 )+};
386 }
387
388 macro_rules! sol_attrs {
389 ($($id:ident : $e:expr),* $(,)?) => {
390 SolAttrs {
391 $($id: Some($e),)*
392 ..Default::default()
393 }
394 };
395 }
396
397 struct OuterAttribute(Vec<Attribute>);
398
399 impl syn::parse::Parse for OuterAttribute {
400 fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
401 input.call(Attribute::parse_outer).map(Self)
402 }
403 }
404
405 fn run_test(
406 attrs_s: &'static [&'static str],
407 expected: std::result::Result<SolAttrs, &'static str>,
408 ) {
409 let attrs: Vec<Attribute> =
410 attrs_s.iter().flat_map(|s| syn::parse_str::<OuterAttribute>(s).unwrap().0).collect();
411 match (SolAttrs::parse(&attrs), expected) {
412 (Ok((actual, _)), Ok(expected)) => assert_eq!(actual, expected, "{attrs_s:?}"),
413 (Err(actual), Err(expected)) => {
414 let actual = actual.to_string();
415 if !actual.contains(expected) {
416 assert_eq!(actual, expected, "{attrs_s:?}")
417 }
418 }
419 (a, b) => panic!("assertion failed: `{a:?} != {b:?}`: {attrs_s:?}"),
420 }
421 }
422
423 test_sol_attrs! {
424 top_level {
425 #[cfg] => Ok(SolAttrs::default()),
426 #[cfg()] => Ok(SolAttrs::default()),
427 #[cfg = ""] => Ok(SolAttrs::default()),
428 #[derive()] #[sol()] => Ok(SolAttrs::default()),
429 #[sol()] => Ok(SolAttrs::default()),
430 #[sol()] #[sol()] => Ok(SolAttrs::default()),
431 #[sol = ""] => Err("expected `(`"),
432 #[sol] => Err("expected attribute arguments in parentheses: `sol(...)`"),
433
434 #[sol(() = "")] => Err("unexpected token in nested attribute, expected ident"),
435 #[sol(? = "")] => Err("unexpected token in nested attribute, expected ident"),
436 #[sol(::a)] => Err("expected ident"),
437 #[sol(::a = "")] => Err("expected ident"),
438 #[sol(a::b = "")] => Err("expected ident"),
439 }
440
441 extra {
442 #[sol(all_derives)] => Ok(sol_attrs! { all_derives: true }),
443 #[sol(all_derives = true)] => Ok(sol_attrs! { all_derives: true }),
444 #[sol(all_derives = false)] => Ok(sol_attrs! { all_derives: false }),
445 #[sol(all_derives = "false")] => Err("expected boolean literal"),
446 #[sol(all_derives)] #[sol(all_derives)] => Err(DUPLICATE_ERROR),
447
448 #[sol(extra_derives(Single, module::Double))] => Ok(sol_attrs! { extra_derives: vec![
449 parse_quote!(Single),
450 parse_quote!(module::Double),
451 ] }),
452
453 #[sol(extra_methods)] => Ok(sol_attrs! { extra_methods: true }),
454 #[sol(extra_methods = true)] => Ok(sol_attrs! { extra_methods: true }),
455 #[sol(extra_methods = false)] => Ok(sol_attrs! { extra_methods: false }),
456
457 #[sol(docs)] => Ok(sol_attrs! { docs: true }),
458 #[sol(docs = true)] => Ok(sol_attrs! { docs: true }),
459 #[sol(docs = false)] => Ok(sol_attrs! { docs: false }),
460
461 #[sol(abi)] => Ok(sol_attrs! { abi: true }),
462 #[sol(abi = true)] => Ok(sol_attrs! { abi: true }),
463 #[sol(abi = false)] => Ok(sol_attrs! { abi: false }),
464
465 #[sol(rpc)] => Ok(sol_attrs! { rpc: true }),
466 #[sol(rpc = true)] => Ok(sol_attrs! { rpc: true }),
467 #[sol(rpc = false)] => Ok(sol_attrs! { rpc: false }),
468
469 #[sol(alloy_sol_types)] => Err("expected `=`"),
470 #[sol(alloy_sol_types = alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy_core::sol_types) }),
471 #[sol(alloy_sol_types = ::alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy_core::sol_types) }),
472 #[sol(alloy_sol_types = alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy::sol_types) }),
473 #[sol(alloy_sol_types = ::alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy::sol_types) }),
474
475 #[sol(alloy_contract)] => Err("expected `=`"),
476 #[sol(alloy_contract = alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(alloy::contract) }),
477 #[sol(alloy_contract = ::alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(::alloy::contract) }),
478 }
479
480 rename {
481 #[sol(rename = "foo")] => Ok(sol_attrs! { rename: parse_quote!("foo") }),
482
483 #[sol(rename_all = "foo")] => Err("unsupported casing: foo"),
484 #[sol(rename_all = "camelcase")] => Ok(sol_attrs! { rename_all: CasingStyle::Camel }),
485 #[sol(rename_all = "camelCase")] #[sol(rename_all = "PascalCase")] => Err(DUPLICATE_ERROR),
486 }
487
488 bytecode {
489 #[sol(deployed_bytecode = "0x1234")] => Ok(sol_attrs! { deployed_bytecode: parse_quote!("0x1234") }),
490 #[sol(bytecode = "0x1234")] => Ok(sol_attrs! { bytecode: parse_quote!("0x1234") }),
491 #[sol(bytecode = "1234")] => Ok(sol_attrs! { bytecode: parse_quote!("1234") }),
492 #[sol(bytecode = "0x123xyz")] => Err("invalid hex value: "),
493 #[sol(bytecode = "12 34")] => Err("invalid hex value: "),
494 #[sol(bytecode = "xyz")] => Err("invalid hex value: "),
495 #[sol(bytecode = "123")] => Err("invalid hex value: "),
496 }
497
498 type_check {
499 #[sol(type_check = "my_function")] => Ok(sol_attrs! { type_check: parse_quote!("my_function") }),
500 #[sol(type_check = "my_function1")] #[sol(type_check = "my_function2")] => Err(DUPLICATE_ERROR),
501 }
502
503 #[cfg_attr(miri, ignore = "env not available")]
504 inner_macro {
505 #[sol(rename = env!("CARGO_PKG_NAME"))] => Ok(sol_attrs! { rename: parse_quote!("alloy-sol-macro-input") }),
506 }
507
508 ignore_unlinked {
509 #[sol(ignore_unlinked)] => Ok(sol_attrs! { ignore_unlinked: true }),
510 #[sol(ignore_unlinked = true)] => Ok(sol_attrs! { ignore_unlinked: true }),
511 #[sol(ignore_unlinked = false)] => Ok(sol_attrs! { ignore_unlinked: false }),
512 }
513 }
514}