alloy_sol_macro_input/
input.rs

1use ast::Spanned;
2use std::path::PathBuf;
3use syn::{
4    parse::{discouraged::Speculative, Parse, ParseStream},
5    Attribute, Error, Ident, LitStr, Result, Token,
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        let attrs = Attribute::parse_inner(input)?;
58
59        // Ignore outer attributes when peeking.
60        let fork = input.fork();
61        let _fork_outer = Attribute::parse_outer(&fork)?;
62
63        // Include macro calls like `concat!(env!())`;
64        let is_litstr_like = |fork: syn::parse::ParseStream<'_>| {
65            fork.peek(LitStr) || (fork.peek(Ident) && fork.peek2(Token![!]))
66        };
67
68        if is_litstr_like(&fork)
69            || (fork.peek(Ident) && fork.peek2(Token![,]) && {
70                let _ = fork.parse::<Ident>();
71                let _ = fork.parse::<Token![,]>();
72                is_litstr_like(&fork)
73            })
74        {
75            Self::parse_abigen(attrs, input)
76        } else {
77            input.parse().map(|kind| Self { attrs, path: None, kind })
78        }
79    }
80}
81
82impl SolInput {
83    /// `abigen`-like syntax: `sol!(name, "path/to/file")`
84    fn parse_abigen(mut attrs: Vec<Attribute>, input: ParseStream<'_>) -> Result<Self> {
85        attrs.extend(Attribute::parse_outer(input)?);
86
87        let name = input.parse::<Option<Ident>>()?;
88        if name.is_some() {
89            input.parse::<Token![,]>()?;
90        }
91        let span = input.span();
92        let macro_string::MacroString(mut value) = input.parse::<macro_string::MacroString>()?;
93
94        let _ = input.parse::<Option<Token![,]>>()?;
95        if !input.is_empty() {
96            let msg = "unexpected token, expected end of input";
97            return Err(Error::new(input.span(), msg));
98        }
99
100        let mut path = None;
101
102        let is_path = {
103            let s = value.trim();
104            !(s.is_empty()
105                || (s.starts_with('{') && s.ends_with('}'))
106                || (s.starts_with('[') && s.ends_with(']')))
107        };
108        if is_path {
109            let mut p = PathBuf::from(value);
110            if p.is_relative() {
111                let dir = std::env::var_os("CARGO_MANIFEST_DIR")
112                    .map(PathBuf::from)
113                    .ok_or_else(|| Error::new(span, "failed to get manifest dir"))?;
114                p = dir.join(p);
115            }
116            p = dunce::canonicalize(&p)
117                .map_err(|e| Error::new(span, format!("failed to canonicalize path {p:?}: {e}")))?;
118            value = std::fs::read_to_string(&p)
119                .map_err(|e| Error::new(span, format!("failed to read file {p:?}: {e}")))?;
120            path = Some(p);
121        }
122
123        let s = value.trim();
124        if s.is_empty() {
125            let msg = if is_path { "file path is empty" } else { "empty input is not allowed" };
126            Err(Error::new(span, msg))
127        } else if (s.starts_with('{') && s.ends_with('}'))
128            || (s.starts_with('[') && s.ends_with(']'))
129        {
130            #[cfg(feature = "json")]
131            {
132                let json = serde_json::from_str(s)
133                    .map_err(|e| Error::new(span, format!("invalid JSON: {e}")))?;
134                let name = name.ok_or_else(|| Error::new(span, "need a name for JSON ABI"))?;
135                Ok(Self { attrs, path, kind: SolInputKind::Json(name, json) })
136            }
137            #[cfg(not(feature = "json"))]
138            {
139                let msg = "JSON support must be enabled with the \"json\" feature";
140                Err(Error::new(span, msg))
141            }
142        } else {
143            if let Some(name) = name {
144                let msg = "names are not allowed outside of JSON ABI, remove this name";
145                return Err(Error::new(name.span(), msg));
146            }
147            let kind = syn::parse_str(s).map_err(|e| {
148                let msg = format!("expected a valid JSON ABI string or Solidity string: {e}");
149                Error::new(span, msg)
150            })?;
151            Ok(Self { attrs, path, kind })
152        }
153    }
154}