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