syn_solidity/ident/
mod.rs

1use crate::Spanned;
2use proc_macro2::{Ident, Span};
3use quote::ToTokens;
4use std::fmt;
5use syn::{
6    ext::IdentExt,
7    parse::{Parse, ParseStream},
8    Result, Token,
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 {
122            Self(Ident::new_raw(s, span))
123        } else {
124            Self(Ident::new(s, span))
125        }
126    }
127
128    /// Returns the identifier as a string, without the `r#` prefix if present.
129    pub fn as_string(&self) -> String {
130        let mut s = self.0.to_string();
131        if s.starts_with("r#") {
132            s = s[2..].to_string();
133        }
134        s
135    }
136
137    /// Parses any identifier including keywords.
138    pub fn parse_any(input: ParseStream<'_>) -> Result<Self> {
139        check_dollar(input)?;
140
141        input.call(Ident::parse_any).map(Into::into)
142    }
143
144    /// Peeks any identifier including keywords.
145    pub fn peek_any(input: ParseStream<'_>) -> bool {
146        input.peek(Ident::peek_any)
147    }
148
149    pub fn parse_opt(input: ParseStream<'_>) -> Result<Option<Self>> {
150        if Self::peek_any(input) {
151            input.parse().map(Some)
152        } else {
153            Ok(None)
154        }
155    }
156}
157
158fn check_dollar(input: ParseStream<'_>) -> Result<()> {
159    if input.peek(Token![$]) {
160        Err(input.error("Solidity identifiers starting with `$` are unsupported. This is a known limitation of syn-solidity."))
161    } else {
162        Ok(())
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::sol_path;
170
171    #[test]
172    fn ident() {
173        let id: SolIdent = syn::parse_str("a").unwrap();
174        assert_eq!(id, SolIdent::new("a"));
175    }
176
177    #[test]
178    fn keywords() {
179        // keywords in Rust, but not Solidity; we try to make them "raw", although some, like
180        // `crate` can never be made identifiers. See ./kw.c`.
181        let difference: &[&str] = &include!("./difference.expr");
182        for &s in difference {
183            let id: SolIdent = syn::parse_str(s).unwrap();
184            assert_eq!(id, SolIdent::new(s));
185            assert_eq!(id.to_string(), format!("r#{s}"));
186            assert_eq!(id.as_string(), s);
187        }
188
189        // keywords in both languages; we don't make them "raw" because they are always invalid.
190        let intersection: &[&str] = &include!("./intersection.expr");
191        for &s in intersection {
192            let id: SolIdent = syn::parse_str(s).unwrap();
193            assert_eq!(id, SolIdent::new(s));
194            assert_eq!(id.to_string(), s);
195            assert_eq!(id.as_string(), s);
196        }
197    }
198
199    // <https://github.com/alloy-rs/core/issues/902>
200    #[test]
201    fn self_keywords() {
202        let id: SolIdent = syn::parse_str("self").unwrap();
203        assert_eq!(id, SolIdent::new("this"));
204        assert_eq!(id.to_string(), "this");
205        assert_eq!(id.as_string(), "this");
206
207        let id: SolIdent = syn::parse_str("Self").unwrap();
208        assert_eq!(id, SolIdent::new("This"));
209        assert_eq!(id.to_string(), "This");
210        assert_eq!(id.as_string(), "This");
211    }
212
213    #[test]
214    fn ident_path() {
215        let path: SolPath = syn::parse_str("a.b.c").unwrap();
216        assert_eq!(path, sol_path!["a", "b", "c"]);
217    }
218
219    #[test]
220    fn ident_path_trailing() {
221        let _e = syn::parse_str::<SolPath>("a.b.").unwrap_err();
222    }
223
224    #[test]
225    fn ident_dollar() {
226        assert!(syn::parse_str::<SolIdent>("$hello")
227            .unwrap_err()
228            .to_string()
229            .contains("Solidity identifiers starting with `$` are unsupported."));
230    }
231}