alloy_sol_macro_input/
input.rs1use ast::Spanned;
2use std::path::PathBuf;
3use syn::{
4 Attribute, Error, Ident, LitStr, Result, Token,
5 parse::{Parse, ParseStream, discouraged::Speculative},
6};
7
8#[derive(Clone, Debug)]
11pub enum SolInputKind {
12 Type(ast::Type),
14 Sol(ast::File),
16 #[cfg(feature = "json")]
18 Json(Ident, alloy_json_abi::ContractObject),
19}
20
21impl Parse for SolInputKind {
22 fn parse(input: ParseStream<'_>) -> Result<Self> {
23 let fork = input.fork();
24 match fork.parse() {
25 Ok(file) => {
26 input.advance_to(&fork);
27 Ok(Self::Sol(file))
28 }
29 Err(e) => match input.parse() {
30 Ok(ast::Type::Custom(_)) | Err(_) => Err(e),
31
32 Ok(ast::Type::Mapping(m)) => {
33 Err(Error::new(m.span(), "mapping types are not yet supported"))
34 }
35
36 Ok(ty) => Ok(Self::Type(ty)),
37 },
38 }
39 }
40}
41
42#[derive(Clone, Debug)]
46pub struct SolInput {
47 pub attrs: Vec<Attribute>,
49 pub path: Option<PathBuf>,
51 pub kind: SolInputKind,
53}
54
55impl Parse for SolInput {
56 fn parse(input: ParseStream<'_>) -> Result<Self> {
57 Self::parse_with(input, Default::default())
58 }
59}
60
61impl SolInput {
62 pub fn parse_with(input: ParseStream<'_>, config: SolInputParseConfig) -> Result<Self> {
64 let attrs = Attribute::parse_inner(input)?;
65
66 let fork = input.fork();
68 let fork_outer = Attribute::parse_outer(&fork)?;
69 let ignore_unlinked_outer = contains_ignore_unlinked(&fork_outer);
70
71 let is_litstr_like = |fork: syn::parse::ParseStream<'_>| {
73 fork.peek(LitStr) || (fork.peek(Ident) && fork.peek2(Token![!]))
74 };
75
76 if is_litstr_like(&fork)
77 || (fork.peek(Ident) && fork.peek2(Token![,]) && {
78 let _ = fork.parse::<Ident>();
79 let _ = fork.parse::<Token![,]>();
80 is_litstr_like(&fork)
81 })
82 {
83 let ignore_unlinked_inner = contains_ignore_unlinked(&attrs);
84 Self::parse_abigen(
85 attrs,
86 input,
87 config.set_ignore_unlinked_bytecode(ignore_unlinked_inner || ignore_unlinked_outer),
88 )
89 } else {
90 input.parse().map(|kind| Self { attrs, path: None, kind })
91 }
92 }
93
94 fn parse_abigen(
96 mut attrs: Vec<Attribute>,
97 input: ParseStream<'_>,
98 _config: SolInputParseConfig,
99 ) -> Result<Self> {
100 attrs.extend(Attribute::parse_outer(input)?);
101
102 let name = input.parse::<Option<Ident>>()?;
103 if name.is_some() {
104 input.parse::<Token![,]>()?;
105 }
106 let span = input.span();
107 let macro_string::MacroString(mut value) = input.parse::<macro_string::MacroString>()?;
108
109 let _ = input.parse::<Option<Token![,]>>()?;
110 if !input.is_empty() {
111 let msg = "unexpected token, expected end of input";
112 return Err(Error::new(input.span(), msg));
113 }
114
115 let mut path = None;
116
117 let is_path = {
118 let s = value.trim();
119 !(s.is_empty()
120 || (s.starts_with('{') && s.ends_with('}'))
121 || (s.starts_with('[') && s.ends_with(']')))
122 };
123 if is_path {
124 let mut p = PathBuf::from(value);
125 if p.is_relative() {
126 let dir = std::env::var_os("CARGO_MANIFEST_DIR")
127 .map(PathBuf::from)
128 .ok_or_else(|| Error::new(span, "failed to get manifest dir"))?;
129 p = dir.join(p);
130 }
131 p = dunce::canonicalize(&p)
132 .map_err(|e| Error::new(span, format!("failed to canonicalize path {p:?}: {e}")))?;
133 value = std::fs::read_to_string(&p)
134 .map_err(|e| Error::new(span, format!("failed to read file {p:?}: {e}")))?;
135 path = Some(p);
136 }
137
138 let s = value.trim();
139 if s.is_empty() {
140 let msg = if is_path { "file path is empty" } else { "empty input is not allowed" };
141 Err(Error::new(span, msg))
142 } else if (s.starts_with('{') && s.ends_with('}'))
143 || (s.starts_with('[') && s.ends_with(']'))
144 {
145 #[cfg(feature = "json")]
146 {
147 let json = alloy_json_abi::ContractObject::from_json_with(
148 s,
149 _config.ignore_unlinked_bytecode,
150 )
151 .map_err(|e| Error::new(span, format!("invalid JSON: {e}")))?;
152
153 let name = name.ok_or_else(|| Error::new(span, "need a name for JSON ABI"))?;
154 Ok(Self { attrs, path, kind: SolInputKind::Json(name, json) })
155 }
156 #[cfg(not(feature = "json"))]
157 {
158 let msg = "JSON support must be enabled with the \"json\" feature";
159 Err(Error::new(span, msg))
160 }
161 } else {
162 if let Some(name) = name {
163 let msg = "names are not allowed outside of JSON ABI, remove this name";
164 return Err(Error::new(name.span(), msg));
165 }
166 let kind = syn::parse_str(s).map_err(|e| {
167 let msg = format!("expected a valid JSON ABI string or Solidity string: {e}");
168 Error::new(span, msg)
169 })?;
170 Ok(Self { attrs, path, kind })
171 }
172 }
173}
174
175#[derive(Debug, Clone, Default)]
177pub struct SolInputParseConfig {
178 ignore_unlinked_bytecode: bool,
180}
181
182impl SolInputParseConfig {
183 pub fn ignore_unlinked_bytecode(self) -> Self {
185 self.set_ignore_unlinked_bytecode(true)
186 }
187
188 pub fn set_ignore_unlinked_bytecode(mut self, ignore_unlinked_bytecode: bool) -> Self {
189 self.ignore_unlinked_bytecode = ignore_unlinked_bytecode;
190 self
191 }
192}
193
194fn contains_ignore_unlinked(attrs: &[Attribute]) -> bool {
196 attrs.iter().any(|attr| {
197 attr.path().is_ident("sol") && {
198 if let Ok(meta) = attr.meta.require_list() {
199 let mut found = false;
200 let _ = meta.parse_nested_meta(|meta| {
201 if meta.path.is_ident("ignore_unlinked") {
202 found = true;
203 }
204 Ok(())
205 });
206 found
207 } else {
208 false
209 }
210 }
211 })
212}