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