Skip to main content

alloy_sol_macro_input/
input.rs

1use ast::Spanned;
2use std::path::PathBuf;
3use syn::{
4    Attribute, Error, Ident, LitStr, Result, Token,
5    parse::{Parse, ParseStream, discouraged::Speculative},
6};
7
8/// Parsed input for `sol!`-like macro expanders. This enum represents a `Sol` file, a JSON ABI, or
9/// a Solidity type.
10#[derive(Clone, Debug)]
11pub enum SolInputKind {
12    /// Solidity type.
13    Type(ast::Type),
14    /// Solidity file or snippet.
15    Sol(ast::File),
16    /// JSON ABI file
17    #[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/// Parsed input for `sol!`-like macro expanders. This struct represents a list
43/// of expandable items parsed from either solidity code snippets, or from a
44/// JSON abi.
45#[derive(Clone, Debug)]
46pub struct SolInput {
47    /// Attributes attached to the input, of the form `#[...]`.
48    pub attrs: Vec<Attribute>,
49    /// Path to the input, if any.
50    pub path: Option<PathBuf>,
51    /// The input kind.
52    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    /// Parse the [`SolInput`] with the given settings.
63    pub fn parse_with(input: ParseStream<'_>, config: SolInputParseConfig) -> Result<Self> {
64        let attrs = Attribute::parse_inner(input)?;
65
66        // Ignore outer attributes when peeking.
67        let fork = input.fork();
68        let fork_outer = Attribute::parse_outer(&fork)?;
69        let ignore_unlinked_outer = contains_ignore_unlinked(&fork_outer);
70
71        // Include macro calls like `concat!(env!())`;
72        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    /// `abigen`-like syntax: `sol!(name, "path/to/file")`
95    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/// Settings determining how to parse [`SolInput`]
176#[derive(Debug, Clone, Default)]
177pub struct SolInputParseConfig {
178    /// Whether unlinked bytecode objects should be ignored.
179    ignore_unlinked_bytecode: bool,
180}
181
182impl SolInputParseConfig {
183    /// Ignores bytecode from json abi parsing if the bytecode is unlinked.
184    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
194/// Checks if the `ignore_unlinked` sol attr is present in the given attributes.
195fn 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}