bon_macros/util/
ident.rs

1use crate::util::prelude::*;
2use ident_case::RenameRule;
3use syn::spanned::Spanned;
4
5pub(crate) trait IdentExt {
6    /// Converts the ident (assumed to be in `snake_case`) to `PascalCase` without
7    /// preserving its span.
8    ///
9    /// Span loss is intentional to work around the semantic token type assignment
10    /// ambiguity that may be experienced by IDEs. For example, rust analyzer
11    /// assigns different colors to identifiers according to their semantic meaning.
12    ///
13    /// If identifiers with the same span were used in different contexts such as
14    /// in function name and struct name positions, then rust-analyzer would chose
15    /// the semantic meaning for syntax highlighting of the input identifier randomly
16    /// out of these two contexts.
17    ///
18    /// By not preserving the span, we can ensure that the semantic meaning of the
19    /// produced identifier won't influence the syntax highlighting of the original
20    /// identifier.
21    fn snake_to_pascal_case(&self) -> Self;
22
23    /// Same thing as `snake_to_pascal_case` but converts `PascalCase` to `snake_case`.
24    fn pascal_to_snake_case(&self) -> Self;
25
26    /// Creates a new ident with the given name and span. If the name starts with
27    /// `r#` then automatically creates a raw ident.
28    fn new_maybe_raw(name: &str, span: Span) -> Self;
29
30    /// Returns the name of the identifier stripping the `r#` prefix if it exists.
31    fn raw_name(&self) -> String;
32}
33
34impl IdentExt for syn::Ident {
35    fn snake_to_pascal_case(&self) -> Self {
36        // There are no pascal case keywords in Rust except for `Self`, which
37        // is anyway not allowed even as a raw identifier:
38        // https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094
39        //
40        // So no need to handle raw identifiers here.
41        let mut renamed = RenameRule::PascalCase.apply_to_field(self.raw_name());
42
43        // Make sure `Self` keyword isn't generated.
44        // This may happen if the input was `self_`, for example.
45        if renamed == "Self" {
46            renamed.push('_');
47        }
48
49        Self::new(&renamed, renamed.span())
50    }
51
52    fn pascal_to_snake_case(&self) -> Self {
53        let renamed = RenameRule::SnakeCase.apply_to_variant(self.raw_name());
54        Self::new_maybe_raw(&renamed, Span::call_site())
55    }
56
57    fn new_maybe_raw(name: &str, span: Span) -> Self {
58        // If the ident is already raw (starts with `r#`) then just create a raw ident.
59        if let Some(name) = name.strip_prefix("r#") {
60            return Self::new_raw(name, span);
61        }
62
63        // ..otherwise validate if it is a valid identifier.
64        // The `parse_str` method will return an error if the name is not a valid
65        // identifier.
66        if syn::parse_str::<Self>(name).is_ok() {
67            return Self::new(name, span);
68        }
69
70        // Try to make it a raw ident by adding `r#` prefix.
71        // This won't work for some keywords such as `super`, `crate`,
72        // `Self`, which are not allowed as raw identifiers
73        if syn::parse_str::<Self>(&format!("r#{name}")).is_ok() {
74            return Self::new_raw(name, span);
75        }
76
77        // As the final fallback add a trailing `_` to create a valid identifier
78        Self::new(&format!("{name}_"), span)
79    }
80
81    fn raw_name(&self) -> String {
82        let name = self.to_string();
83        if let Some(raw) = name.strip_prefix("r#") {
84            raw.to_owned()
85        } else {
86            name
87        }
88    }
89}