syn_solidity/ident/
mod.rs

1use crate::Spanned;
2use proc_macro2::{Ident, Span};
3use quote::ToTokens;
4use std::fmt;
5use syn::{
6    Result, Token,
7    ext::IdentExt,
8    parse::{Parse, ParseStream},
9};
10
11mod path;
12pub use path::SolPath;
13
14// See `./kw.c`.
15
16/// The set difference of the Rust and Solidity keyword sets. We need this so that we can emit raw
17/// identifiers for Solidity keywords.
18static KW_DIFFERENCE: &[&str] = &include!("./difference.expr");
19
20/// A Solidity identifier.
21#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[repr(transparent)]
23pub struct SolIdent(pub Ident);
24
25impl quote::IdentFragment for SolIdent {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        self.0.fmt(f)
28    }
29
30    fn span(&self) -> Option<Span> {
31        Some(self.0.span())
32    }
33}
34
35impl fmt::Display for SolIdent {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        self.0.fmt(f)
38    }
39}
40
41impl fmt::Debug for SolIdent {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.debug_tuple("SolIdent").field(&self.to_string()).finish()
44    }
45}
46
47impl<T: ?Sized + AsRef<str>> PartialEq<T> for SolIdent {
48    fn eq(&self, other: &T) -> bool {
49        self.0 == other
50    }
51}
52
53impl From<Ident> for SolIdent {
54    fn from(value: Ident) -> Self {
55        Self::new_spanned(&value.to_string(), value.span())
56    }
57}
58
59impl From<SolIdent> for Ident {
60    fn from(value: SolIdent) -> Self {
61        value.0
62    }
63}
64
65impl From<&str> for SolIdent {
66    fn from(value: &str) -> Self {
67        Self::new(value)
68    }
69}
70
71impl Parse for SolIdent {
72    fn parse(input: ParseStream<'_>) -> Result<Self> {
73        check_dollar(input)?;
74        let id = Ident::parse_any(input)?;
75        Ok(Self::from(id))
76    }
77}
78
79impl ToTokens for SolIdent {
80    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
81        self.0.to_tokens(tokens);
82    }
83}
84
85impl Spanned for SolIdent {
86    fn span(&self) -> Span {
87        self.0.span()
88    }
89
90    fn set_span(&mut self, span: Span) {
91        self.0.set_span(span);
92    }
93}
94
95impl SolIdent {
96    pub fn new(s: &str) -> Self {
97        Self::new_spanned(s, Span::call_site())
98    }
99
100    pub fn new_spanned(mut s: &str, span: Span) -> Self {
101        let mut new_raw = KW_DIFFERENCE.contains(&s);
102
103        if s.starts_with("r#") {
104            new_raw = true;
105            s = &s[2..];
106        }
107
108        if matches!(s, "_" | "self" | "Self" | "super" | "crate") {
109            new_raw = false;
110
111            // `self` renamed to `this` as `r#self` is not accepted by rust.
112            // See: <https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094/4>
113            if matches!(s, "self") {
114                s = "this";
115            }
116            if matches!(s, "Self") {
117                s = "This";
118            }
119        }
120
121        if new_raw { Self(Ident::new_raw(s, span)) } else { Self(Ident::new(s, span)) }
122    }
123
124    /// Returns the identifier as a string, without the `r#` prefix if present.
125    pub fn as_string(&self) -> String {
126        let mut s = self.0.to_string();
127        if s.starts_with("r#") {
128            s = s[2..].to_string();
129        }
130        s
131    }
132
133    /// Parses any identifier including keywords.
134    pub fn parse_any(input: ParseStream<'_>) -> Result<Self> {
135        check_dollar(input)?;
136
137        input.call(Ident::parse_any).map(Into::into)
138    }
139
140    /// Peeks any identifier including keywords.
141    pub fn peek_any(input: ParseStream<'_>) -> bool {
142        input.peek(Ident::peek_any)
143    }
144
145    pub fn parse_opt(input: ParseStream<'_>) -> Result<Option<Self>> {
146        if Self::peek_any(input) { input.parse().map(Some) } else { Ok(None) }
147    }
148}
149
150fn check_dollar(input: ParseStream<'_>) -> Result<()> {
151    if input.peek(Token![$]) {
152        Err(input.error("Solidity identifiers starting with `$` are unsupported. This is a known limitation of syn-solidity."))
153    } else {
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::sol_path;
162
163    #[test]
164    fn ident() {
165        let id: SolIdent = syn::parse_str("a").unwrap();
166        assert_eq!(id, SolIdent::new("a"));
167    }
168
169    #[test]
170    fn keywords() {
171        // keywords in Rust, but not Solidity; we try to make them "raw", although some, like
172        // `crate` can never be made identifiers. See ./kw.c`.
173        let difference: &[&str] = &include!("./difference.expr");
174        for &s in difference {
175            let id: SolIdent = syn::parse_str(s).unwrap();
176            assert_eq!(id, SolIdent::new(s));
177            assert_eq!(id.to_string(), format!("r#{s}"));
178            assert_eq!(id.as_string(), s);
179        }
180
181        // keywords in both languages; we don't make them "raw" because they are always invalid.
182        let intersection: &[&str] = &include!("./intersection.expr");
183        for &s in intersection {
184            let id: SolIdent = syn::parse_str(s).unwrap();
185            assert_eq!(id, SolIdent::new(s));
186            assert_eq!(id.to_string(), s);
187            assert_eq!(id.as_string(), s);
188        }
189    }
190
191    // <https://github.com/alloy-rs/core/issues/902>
192    #[test]
193    fn self_keywords() {
194        let id: SolIdent = syn::parse_str("self").unwrap();
195        assert_eq!(id, SolIdent::new("this"));
196        assert_eq!(id.to_string(), "this");
197        assert_eq!(id.as_string(), "this");
198
199        let id: SolIdent = syn::parse_str("Self").unwrap();
200        assert_eq!(id, SolIdent::new("This"));
201        assert_eq!(id.to_string(), "This");
202        assert_eq!(id.as_string(), "This");
203    }
204
205    #[test]
206    fn ident_path() {
207        let path: SolPath = syn::parse_str("a.b.c").unwrap();
208        assert_eq!(path, sol_path!["a", "b", "c"]);
209    }
210
211    #[test]
212    fn ident_path_trailing() {
213        let _e = syn::parse_str::<SolPath>("a.b.").unwrap_err();
214    }
215
216    #[test]
217    fn ident_dollar() {
218        assert!(
219            syn::parse_str::<SolIdent>("$hello")
220                .unwrap_err()
221                .to_string()
222                .contains("Solidity identifiers starting with `$` are unsupported.")
223        );
224    }
225}