1use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
2use proc_macro2::TokenStream;
3use quote::quote;
4use syn::{
5 meta::ParseNestedMeta, parse::Parse, punctuated::Punctuated, Attribute, Error, LitBool, LitStr,
6 Path, Result, Token,
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(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
115impl SolAttrs {
116 pub fn parse(attrs: &[Attribute]) -> Result<(Self, Vec<Attribute>)> {
118 let mut this = Self::default();
119 let mut others = Vec::with_capacity(attrs.len());
120 for attr in attrs {
121 if !attr.path().is_ident("sol") {
122 others.push(attr.clone());
123 continue;
124 }
125
126 attr.meta.require_list()?.parse_nested_meta(|meta| {
127 let path = meta.path.get_ident().ok_or_else(|| meta.error("expected ident"))?;
128 let s = path.to_string();
129
130 macro_rules! match_ {
131 ($($l:ident => $e:expr),* $(,)?) => {
132 match s.as_str() {
133 $(
134 stringify!($l) => if this.$l.is_some() {
135 return Err(meta.error(DUPLICATE_ERROR))
136 } else {
137 this.$l = Some($e);
138 },
139 )*
140 _ => return Err(meta.error(UNKNOWN_ERROR)),
141 }
142 };
143 }
144
145 let bool = || {
147 if let Ok(input) = meta.value() {
148 input.parse::<LitBool>().map(|lit| lit.value)
149 } else {
150 Ok(true)
151 }
152 };
153
154 let path = || meta.value()?.parse::<Path>();
156
157 let lit = || {
159 let value = meta.value()?;
160 let span = value.span();
161 let macro_string::MacroString(value) =
162 value.parse::<macro_string::MacroString>()?;
163 Ok::<_, syn::Error>(LitStr::new(&value, span))
164 };
165
166 let bytes = || {
168 let lit = lit()?;
169 if let Err(e) = hex::check(lit.value()) {
170 let msg = format!("invalid hex value: {e}");
171 return Err(Error::new(lit.span(), msg));
172 }
173 Ok(lit)
174 };
175
176 fn list<T>(
178 meta: &ParseNestedMeta<'_>,
179 parser: fn(syn::parse::ParseStream<'_>) -> Result<T>,
180 ) -> Result<Vec<T>> {
181 let content;
182 syn::parenthesized!(content in meta.input);
183 Ok(content.parse_terminated(parser, Token![,])?.into_iter().collect())
184 }
185
186 match_! {
187 rpc => bool()?,
188 abi => bool()?,
189 all_derives => bool()?,
190 extra_derives => list(&meta, Path::parse)?,
191 extra_methods => bool()?,
192 docs => bool()?,
193
194 alloy_sol_types => path()?,
195 alloy_contract => path()?,
196
197 rename => lit()?,
198 rename_all => CasingStyle::from_lit(&lit()?)?,
199
200 bytecode => bytes()?,
201 deployed_bytecode => bytes()?,
202
203 type_check => lit()?,
204 };
205 Ok(())
206 })?;
207 }
208 Ok((this, others))
209 }
210}
211
212pub trait ContainsSolAttrs {
215 fn attrs(&self) -> &[Attribute];
217
218 fn split_attrs(&self) -> syn::Result<(SolAttrs, Vec<Attribute>)> {
220 SolAttrs::parse(self.attrs())
221 }
222}
223
224impl ContainsSolAttrs for syn_solidity::File {
225 fn attrs(&self) -> &[Attribute] {
226 &self.attrs
227 }
228}
229
230impl ContainsSolAttrs for syn_solidity::ItemContract {
231 fn attrs(&self) -> &[Attribute] {
232 &self.attrs
233 }
234}
235
236impl ContainsSolAttrs for syn_solidity::ItemEnum {
237 fn attrs(&self) -> &[Attribute] {
238 &self.attrs
239 }
240}
241
242impl ContainsSolAttrs for syn_solidity::ItemError {
243 fn attrs(&self) -> &[Attribute] {
244 &self.attrs
245 }
246}
247
248impl ContainsSolAttrs for syn_solidity::ItemEvent {
249 fn attrs(&self) -> &[Attribute] {
250 &self.attrs
251 }
252}
253
254impl ContainsSolAttrs for syn_solidity::ItemFunction {
255 fn attrs(&self) -> &[Attribute] {
256 &self.attrs
257 }
258}
259
260impl ContainsSolAttrs for syn_solidity::ItemStruct {
261 fn attrs(&self) -> &[Attribute] {
262 &self.attrs
263 }
264}
265
266impl ContainsSolAttrs for syn_solidity::ItemUdt {
267 fn attrs(&self) -> &[Attribute] {
268 &self.attrs
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum CasingStyle {
275 Camel,
278 Kebab,
280 Pascal,
283 ScreamingSnake,
286 Snake,
289 Lower,
291 Upper,
293 Verbatim,
295}
296
297impl CasingStyle {
298 fn from_lit(name: &LitStr) -> Result<Self> {
299 let normalized = name.value().to_upper_camel_case().to_lowercase();
300 let s = match normalized.as_ref() {
301 "camel" | "camelcase" => Self::Camel,
302 "kebab" | "kebabcase" => Self::Kebab,
303 "pascal" | "pascalcase" => Self::Pascal,
304 "screamingsnake" | "screamingsnakecase" => Self::ScreamingSnake,
305 "snake" | "snakecase" => Self::Snake,
306 "lower" | "lowercase" => Self::Lower,
307 "upper" | "uppercase" => Self::Upper,
308 "verbatim" | "verbatimcase" => Self::Verbatim,
309 s => return Err(Error::new(name.span(), format!("unsupported casing: {s}"))),
310 };
311 Ok(s)
312 }
313
314 #[allow(dead_code)]
316 pub fn apply(self, s: &str) -> String {
317 match self {
318 Self::Pascal => s.to_upper_camel_case(),
319 Self::Kebab => s.to_kebab_case(),
320 Self::Camel => s.to_lower_camel_case(),
321 Self::ScreamingSnake => s.to_shouty_snake_case(),
322 Self::Snake => s.to_snake_case(),
323 Self::Lower => s.to_snake_case().replace('_', ""),
324 Self::Upper => s.to_shouty_snake_case().replace('_', ""),
325 Self::Verbatim => s.to_owned(),
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use syn::parse_quote;
334
335 macro_rules! test_sol_attrs {
336 ($($(#[$attr:meta])* $group:ident { $($t:tt)* })+) => {$(
337 #[test]
338 $(#[$attr])*
339 fn $group() {
340 test_sol_attrs! { $($t)* }
341 }
342 )+};
343
344 ($( $(#[$attr:meta])* => $expected:expr ),+ $(,)?) => {$(
345 run_test(
346 &[$(stringify!(#[$attr])),*],
347 $expected
348 );
349 )+};
350 }
351
352 macro_rules! sol_attrs {
353 ($($id:ident : $e:expr),* $(,)?) => {
354 SolAttrs {
355 $($id: Some($e),)*
356 ..Default::default()
357 }
358 };
359 }
360
361 struct OuterAttribute(Vec<Attribute>);
362
363 impl syn::parse::Parse for OuterAttribute {
364 fn parse(input: syn::parse::ParseStream<'_>) -> Result<Self> {
365 input.call(Attribute::parse_outer).map(Self)
366 }
367 }
368
369 fn run_test(
370 attrs_s: &'static [&'static str],
371 expected: std::result::Result<SolAttrs, &'static str>,
372 ) {
373 let attrs: Vec<Attribute> =
374 attrs_s.iter().flat_map(|s| syn::parse_str::<OuterAttribute>(s).unwrap().0).collect();
375 match (SolAttrs::parse(&attrs), expected) {
376 (Ok((actual, _)), Ok(expected)) => assert_eq!(actual, expected, "{attrs_s:?}"),
377 (Err(actual), Err(expected)) => {
378 let actual = actual.to_string();
379 if !actual.contains(expected) {
380 assert_eq!(actual, expected, "{attrs_s:?}")
381 }
382 }
383 (a, b) => panic!("assertion failed: `{a:?} != {b:?}`: {attrs_s:?}"),
384 }
385 }
386
387 test_sol_attrs! {
388 top_level {
389 #[cfg] => Ok(SolAttrs::default()),
390 #[cfg()] => Ok(SolAttrs::default()),
391 #[cfg = ""] => Ok(SolAttrs::default()),
392 #[derive()] #[sol()] => Ok(SolAttrs::default()),
393 #[sol()] => Ok(SolAttrs::default()),
394 #[sol()] #[sol()] => Ok(SolAttrs::default()),
395 #[sol = ""] => Err("expected `(`"),
396 #[sol] => Err("expected attribute arguments in parentheses: `sol(...)`"),
397
398 #[sol(() = "")] => Err("unexpected token in nested attribute, expected ident"),
399 #[sol(? = "")] => Err("unexpected token in nested attribute, expected ident"),
400 #[sol(::a)] => Err("expected ident"),
401 #[sol(::a = "")] => Err("expected ident"),
402 #[sol(a::b = "")] => Err("expected ident"),
403 }
404
405 extra {
406 #[sol(all_derives)] => Ok(sol_attrs! { all_derives: true }),
407 #[sol(all_derives = true)] => Ok(sol_attrs! { all_derives: true }),
408 #[sol(all_derives = false)] => Ok(sol_attrs! { all_derives: false }),
409 #[sol(all_derives = "false")] => Err("expected boolean literal"),
410 #[sol(all_derives)] #[sol(all_derives)] => Err(DUPLICATE_ERROR),
411
412 #[sol(extra_derives(Single, module::Double))] => Ok(sol_attrs! { extra_derives: vec![
413 parse_quote!(Single),
414 parse_quote!(module::Double),
415 ] }),
416
417 #[sol(extra_methods)] => Ok(sol_attrs! { extra_methods: true }),
418 #[sol(extra_methods = true)] => Ok(sol_attrs! { extra_methods: true }),
419 #[sol(extra_methods = false)] => Ok(sol_attrs! { extra_methods: false }),
420
421 #[sol(docs)] => Ok(sol_attrs! { docs: true }),
422 #[sol(docs = true)] => Ok(sol_attrs! { docs: true }),
423 #[sol(docs = false)] => Ok(sol_attrs! { docs: false }),
424
425 #[sol(abi)] => Ok(sol_attrs! { abi: true }),
426 #[sol(abi = true)] => Ok(sol_attrs! { abi: true }),
427 #[sol(abi = false)] => Ok(sol_attrs! { abi: false }),
428
429 #[sol(rpc)] => Ok(sol_attrs! { rpc: true }),
430 #[sol(rpc = true)] => Ok(sol_attrs! { rpc: true }),
431 #[sol(rpc = false)] => Ok(sol_attrs! { rpc: false }),
432
433 #[sol(alloy_sol_types)] => Err("expected `=`"),
434 #[sol(alloy_sol_types = alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy_core::sol_types) }),
435 #[sol(alloy_sol_types = ::alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy_core::sol_types) }),
436 #[sol(alloy_sol_types = alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy::sol_types) }),
437 #[sol(alloy_sol_types = ::alloy::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy::sol_types) }),
438
439 #[sol(alloy_contract)] => Err("expected `=`"),
440 #[sol(alloy_contract = alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(alloy::contract) }),
441 #[sol(alloy_contract = ::alloy::contract)] => Ok(sol_attrs! { alloy_contract: parse_quote!(::alloy::contract) }),
442 }
443
444 rename {
445 #[sol(rename = "foo")] => Ok(sol_attrs! { rename: parse_quote!("foo") }),
446
447 #[sol(rename_all = "foo")] => Err("unsupported casing: foo"),
448 #[sol(rename_all = "camelcase")] => Ok(sol_attrs! { rename_all: CasingStyle::Camel }),
449 #[sol(rename_all = "camelCase")] #[sol(rename_all = "PascalCase")] => Err(DUPLICATE_ERROR),
450 }
451
452 bytecode {
453 #[sol(deployed_bytecode = "0x1234")] => Ok(sol_attrs! { deployed_bytecode: parse_quote!("0x1234") }),
454 #[sol(bytecode = "0x1234")] => Ok(sol_attrs! { bytecode: parse_quote!("0x1234") }),
455 #[sol(bytecode = "1234")] => Ok(sol_attrs! { bytecode: parse_quote!("1234") }),
456 #[sol(bytecode = "0x123xyz")] => Err("invalid hex value: "),
457 #[sol(bytecode = "12 34")] => Err("invalid hex value: "),
458 #[sol(bytecode = "xyz")] => Err("invalid hex value: "),
459 #[sol(bytecode = "123")] => Err("invalid hex value: "),
460 }
461
462 type_check {
463 #[sol(type_check = "my_function")] => Ok(sol_attrs! { type_check: parse_quote!("my_function") }),
464 #[sol(type_check = "my_function1")] #[sol(type_check = "my_function2")] => Err(DUPLICATE_ERROR),
465 }
466
467 #[cfg_attr(miri, ignore = "env not available")]
468 inner_macro {
469 #[sol(rename = env!("CARGO_PKG_NAME"))] => Ok(sol_attrs! { rename: parse_quote!("alloy-sol-macro-input") }),
470 }
471 }
472}