Skip to main content

lintspec_macros/
lib.rs

1#![expect(clippy::doc_markdown)]
2#![expect(clippy::result_large_err)]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
4use unsynn::{
5    BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either, Except, Gt,
6    Ident, LiteralString, Lt, Many, Optional, ParenthesisGroupContaining, Parse as _, PathSep,
7    PathSepDelimited, Pound, ToTokens as _, TokenStream, TokenTree, format_ident, quote, unsynn,
8};
9
10/// Represents a module path, consisting of an optional path separator followed by
11/// a path-separator-delimited sequence of identifiers.
12type ModPath = Cons<Option<PathSep>, PathSepDelimited<Ident>>;
13
14unsynn! {
15    operator Eq = "=";
16    keyword EnumKeyword = "enum";
17    keyword DocKeyword = "doc";
18    keyword ReprKeyword = "repr";
19    keyword PubKeyword = "pub";
20    keyword InKeyword = "in";
21    keyword ConstKeyword = "const";
22
23    /// Represents documentation for an item.
24    struct DocInner {
25        /// The "doc" keyword.
26        _kw_doc: DocKeyword,
27        /// The equality operator.
28        _eq: Eq,
29        /// The documentation content as a literal string.
30        value: LiteralString,
31    }
32
33    /// Represents the inner content of a `repr` attribute, typically used for specifying
34    /// memory layout or representation hints.
35    struct ReprInner {
36        /// The "repr" keyword.
37        _kw_repr: ReprKeyword,
38        /// The representation attributes enclosed in parentheses.
39        attr: ParenthesisGroupContaining<CommaDelimitedVec<Ident>>,
40    }
41
42    /// Represents the inner content of an attribute annotation.
43    enum AttributeInner {
44        /// A documentation attribute typically used for generating documentation.
45        Doc(DocInner),
46        /// A representation attribute that specifies how data should be laid out.
47        Repr(ReprInner),
48        /// Any other attribute represented as a sequence of token trees.
49        Any(Many<TokenTree>),
50    }
51
52    /// Represents an attribute annotation on a field, typically in the form `#[attr]`.
53    struct Attribute {
54        /// The pound sign preceding the attribute.
55        _pound: Pound,
56        /// The content of the attribute enclosed in square brackets.
57        body: BracketGroupContaining<AttributeInner>,
58    }
59
60    /// Represents visibility modifiers for items.
61    enum Vis {
62        /// `pub(in? crate::foo::bar)`/`pub(in? ::foo::bar)`
63        PubIn(Cons<PubKeyword, ParenthesisGroupContaining<Cons<Option<InKeyword>, ModPath>>>),
64        /// Public visibility, indicated by the "pub" keyword.
65        Pub(PubKeyword),
66    }
67
68    /// Parses either a `TokenTree` or `<...>` grouping (which is not a [`Group`] as far as proc-macros
69    /// are concerned).
70    struct AngleTokenTree(
71        pub Either<Cons<Lt, Many<Cons<Except<Gt>, AngleTokenTree>>, Gt>, TokenTree>
72    );
73
74    /// A simple type or a generic type.
75    struct Type{
76        pub name: Ident,
77        pub generics: Optional<AngleTokenTree>,
78    }
79
80    /// Represents a simple enum variant.
81    struct EnumVariant {
82        /// The discriminant
83        name: Ident,
84        /// The type contained inside of the variant
85        body: ParenthesisGroupContaining<Type>,
86    }
87
88    /// Represents an enum with simple variants.
89    struct SimpleEnum {
90        /// Optional attributes (docs, repr, etc.)
91        _attributes: Optional<Many<Attribute>>,
92        /// Optional visibility
93        _vis: Optional<Vis>,
94        /// The "enum" keyword
95        _enum_token: EnumKeyword,
96        /// The name of the enum
97        name: Ident,
98        /// The contents of the enum body
99        body: BraceGroupContaining<CommaDelimitedVec<EnumVariant>>,
100    }
101}
102
103/// Derive `as_variant(&self) -> Option<&InnerType>` and `to_variant(self) -> Option<InnerType>` for an enum with simple
104/// variants in the form `Variant(InnerType)`.
105#[proc_macro_derive(AsToVariant)]
106pub fn derive_as_to_variant(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
107    let input: TokenStream = input.into();
108    let mut it = input.to_token_iter();
109
110    let enum_def = match SimpleEnum::parse(&mut it) {
111        Ok(def) => def,
112        Err(e) => panic!("failed to parse enum definition: {e:#?}"),
113    };
114
115    let enum_name = enum_def.name;
116    let variants = enum_def.body.content;
117
118    // Generate methods for each variant
119    let variant_methods = variants.into_iter().map(|variant| {
120        let variant_name = &variant.value.name;
121        let variant_name_snake = to_snake_case(&variant.value.name.to_string());
122        let to_method = format_ident!("to_{variant_name_snake}");
123        let as_method = format_ident!("as_{variant_name_snake}");
124        let inner_type = variant.value.body.content.into_token_stream();
125        let doc_to = LiteralString::from_str(format!(
126            "Convert to the inner {variant_name_snake} definition."
127        ));
128        let doc_as = LiteralString::from_str(format!(
129            "Reference to the inner {variant_name_snake} definition."
130        ));
131
132        quote! {
133            #[doc = #doc_to]
134            #[must_use]
135            pub fn #to_method(self) -> Option<#inner_type> {
136                match self {
137                    #enum_name::#variant_name(value) => Some(value),
138                    _ => None,
139                }
140            }
141
142            #[doc = #doc_as]
143            #[must_use]
144            pub fn #as_method(&self) -> Option<&#inner_type> {
145                match self {
146                    #enum_name::#variant_name(value) => Some(value),
147                    _ => None,
148                }
149            }
150        }
151    });
152
153    let expanded = quote! {
154        impl #enum_name {
155            #{variant_methods}
156        }
157    };
158
159    proc_macro::TokenStream::from(expanded)
160}
161
162/// Converts a string to `s̀nake_case`: `FooBar` -> `foo_bar`
163fn to_snake_case(input: &str) -> String {
164    let words = split_into_words(input);
165    words
166        .iter()
167        .map(|word| word.to_lowercase())
168        .collect::<Vec<_>>()
169        .join("_")
170}
171
172/// Splits a string into words based on case and separators
173///
174/// Logic:
175/// - Iterates through characters in the input string.
176/// - Splits at underscores, hyphens, or whitespace.
177/// - Starts a new word on case boundaries, e.g. between lowercase and uppercase (as in "fooBar").
178/// - Handles consecutive uppercase letters correctly (e.g. `HTTPServer`).
179/// - Aggregates non-separator characters into words.
180/// - Returns a vector of non-empty words as Strings.
181fn split_into_words(input: &str) -> Vec<String> {
182    if input.is_empty() {
183        return vec![];
184    }
185
186    let mut words = Vec::new();
187    let mut current_word = String::new();
188    let mut chars = input.chars().peekable();
189
190    while let Some(c) = chars.next() {
191        // If separator, start new word
192        if c == '_' || c == '-' || c.is_whitespace() {
193            if !current_word.is_empty() {
194                words.push(std::mem::take(&mut current_word));
195            }
196            continue;
197        }
198
199        // Peek at next character for deciding about word boundaries
200        let next = chars.peek().copied();
201
202        if c.is_uppercase() {
203            if let Some(prev) = current_word.chars().last() {
204                // Both cases should take the same action, so fold them together.
205                // Case 1: previous is lowercase or digit, now uppercase (e.g. fooBar, foo1Bar)
206                // Case 2: end of consecutive uppercase group, e.g. "BARBaz"
207                // (prev is uppercase and next char is lowercase)
208                if prev.is_lowercase()
209                    || prev.is_ascii_digit()
210                    || (prev.is_uppercase() && next.is_some_and(char::is_lowercase))
211                {
212                    words.push(std::mem::take(&mut current_word));
213                }
214            }
215            current_word.push(c);
216        } else {
217            // Lowercase or digit, just append
218            // If previous is uppercase and next is lowercase, need to split, but handled above
219            current_word.push(c);
220        }
221    }
222
223    if !current_word.is_empty() {
224        words.push(current_word);
225    }
226
227    words.into_iter().filter(|s| !s.is_empty()).collect()
228}