ruma_macros/
util.rs

1use proc_macro_crate::{FoundCrate, crate_name};
2use proc_macro2::{Span, TokenStream};
3use quote::{ToTokens, TokenStreamExt, format_ident, quote};
4use syn::{
5    Attribute, Field, Ident, LitStr, meta::ParseNestedMeta, punctuated::Punctuated, visit::Visit,
6};
7
8/// The path to use for imports from the ruma-common crate.
9///
10/// To access a reexported crate, prefer to use the [`reexported()`](Self::reexported) method.
11pub(crate) struct RumaCommon(TokenStream);
12
13impl RumaCommon {
14    /// Construct a new `RumaCommon`.
15    pub(crate) fn new() -> Self {
16        let inner = if let Ok(FoundCrate::Name(name)) = crate_name("ruma-common") {
17            let import = format_ident!("{name}");
18            quote! { ::#import }
19        } else if let Ok(FoundCrate::Name(name)) = crate_name("ruma") {
20            let import = format_ident!("{name}");
21            quote! { ::#import }
22        } else if let Ok(FoundCrate::Name(name)) = crate_name("matrix-sdk") {
23            let import = format_ident!("{name}");
24            quote! { ::#import::ruma }
25        } else {
26            quote! { ::ruma_common }
27        };
28
29        Self(inner)
30    }
31
32    /// The path to use for imports from the given reexported crate.
33    pub(crate) fn reexported(&self, reexport: RumaCommonReexport) -> TokenStream {
34        quote! { #self::exports::#reexport }
35    }
36}
37
38impl ToTokens for RumaCommon {
39    fn to_tokens(&self, tokens: &mut TokenStream) {
40        self.0.to_tokens(tokens);
41    }
42}
43
44/// The crates reexported by ruma-common.
45pub(crate) enum RumaCommonReexport {
46    /// The ruma-macros crate.
47    RumaMacros,
48
49    /// The serde crate.
50    Serde,
51
52    /// The serde_html_form crate.
53    SerdeHtmlForm,
54
55    /// The serde_json crate.
56    SerdeJson,
57
58    /// The http crate.
59    Http,
60
61    /// The bytes crate.
62    Bytes,
63}
64
65impl ToTokens for RumaCommonReexport {
66    fn to_tokens(&self, tokens: &mut TokenStream) {
67        let crate_name = match self {
68            Self::RumaMacros => "ruma_macros",
69            Self::Serde => "serde",
70            Self::SerdeHtmlForm => "serde_html_form",
71            Self::SerdeJson => "serde_json",
72            Self::Http => "http",
73            Self::Bytes => "bytes",
74        };
75
76        tokens.append(Ident::new(crate_name, Span::call_site()));
77    }
78}
79
80/// The path to use for imports from the ruma-events crate.
81///
82/// To access a reexported crate, prefer to use [`reexported()`](Self::reexported) or one of the
83/// other methods.
84pub(crate) struct RumaEvents(TokenStream);
85
86impl RumaEvents {
87    /// Construct a new `RumaEvents`.
88    pub(crate) fn new() -> Self {
89        let inner = if let Ok(FoundCrate::Name(name)) = crate_name("ruma-events") {
90            let import = format_ident!("{name}");
91            quote! { ::#import }
92        } else if let Ok(FoundCrate::Name(name)) = crate_name("ruma") {
93            let import = format_ident!("{name}");
94            quote! { ::#import::events }
95        } else if let Ok(FoundCrate::Name(name)) = crate_name("matrix-sdk") {
96            let import = format_ident!("{name}");
97            quote! { ::#import::ruma::events }
98        } else {
99            quote! { ::ruma_events }
100        };
101
102        Self(inner)
103    }
104
105    /// The path to use for imports from the given reexported crate.
106    pub(crate) fn reexported(&self, reexport: RumaEventsReexport) -> TokenStream {
107        quote! { #self::exports::#reexport }
108    }
109
110    /// The path to use for imports from the ruma-common crate.
111    pub(crate) fn ruma_common(&self) -> RumaCommon {
112        RumaCommon(quote! { #self::exports::ruma_common })
113    }
114}
115
116impl ToTokens for RumaEvents {
117    fn to_tokens(&self, tokens: &mut TokenStream) {
118        self.0.to_tokens(tokens);
119    }
120}
121
122/// The crates reexported by ruma-events.
123pub(crate) enum RumaEventsReexport {
124    /// The serde crate.
125    Serde,
126
127    /// The serde_json crate.
128    SerdeJson,
129}
130
131impl ToTokens for RumaEventsReexport {
132    fn to_tokens(&self, tokens: &mut TokenStream) {
133        let crate_name = match self {
134            Self::Serde => "serde",
135            Self::SerdeJson => "serde_json",
136        };
137
138        tokens.append(Ident::new(crate_name, Span::call_site()));
139    }
140}
141
142/// CamelCase's a field ident like "foo_bar" to "FooBar".
143pub(crate) fn to_camel_case(name: &Ident) -> Ident {
144    let span = name.span();
145    let name = name.to_string();
146
147    let s: String = name
148        .split('_')
149        .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
150        .collect();
151    Ident::new(&s, span)
152}
153
154/// Splits the given string on `.` and `_` removing the `m.` then camel casing to give a Rust type
155/// name.
156pub(crate) fn m_prefix_name_to_type_name(name: &LitStr) -> syn::Result<Ident> {
157    let span = name.span();
158    let name = name.value();
159
160    let name = name.strip_prefix("m.").ok_or_else(|| {
161        syn::Error::new(
162            span,
163            format!("well-known matrix events have to start with `m.` found `{name}`"),
164        )
165    })?;
166
167    let s: String = name
168        .strip_suffix(".*")
169        .unwrap_or(name)
170        .split(&['.', '_'] as &[char])
171        .map(|s| s.chars().next().unwrap().to_uppercase().to_string() + &s[1..])
172        .collect();
173
174    Ok(Ident::new(&s, span))
175}
176
177/// Wrapper around [`syn::Field`] that emits the field without its visibility,
178/// thus making it private.
179pub struct PrivateField<'a>(pub &'a Field);
180
181impl ToTokens for PrivateField<'_> {
182    fn to_tokens(&self, tokens: &mut TokenStream) {
183        let Field { attrs, vis: _, mutability, ident, colon_token, ty } = self.0;
184        assert_eq!(*mutability, syn::FieldMutability::None);
185
186        for attr in attrs {
187            attr.to_tokens(tokens);
188        }
189        ident.to_tokens(tokens);
190        colon_token.to_tokens(tokens);
191        ty.to_tokens(tokens);
192    }
193}
194
195#[cfg(feature = "__internal_macro_expand")]
196pub fn cfg_expand_struct(item: &mut syn::ItemStruct) {
197    use std::mem;
198
199    use proc_macro2::TokenTree;
200    use syn::{Fields, LitBool, Meta, visit_mut::VisitMut};
201
202    fn eval_cfg(cfg_expr: TokenStream) -> Option<bool> {
203        let cfg_macro_call = quote! { ::core::cfg!(#cfg_expr) };
204        let expanded = match proc_macro::TokenStream::from(cfg_macro_call).expand_expr() {
205            Ok(t) => t,
206            Err(e) => {
207                eprintln!("Failed to expand cfg! {e}");
208                return None;
209            }
210        };
211
212        let lit: LitBool = syn::parse(expanded).expect("cfg! must expand to a boolean literal");
213        Some(lit.value())
214    }
215
216    fn tokentree_not_comma(tree: &TokenTree) -> bool {
217        match tree {
218            TokenTree::Punct(p) => p.as_char() != ',',
219            _ => true,
220        }
221    }
222
223    struct CfgAttrExpand;
224
225    impl VisitMut for CfgAttrExpand {
226        fn visit_attribute_mut(&mut self, attr: &mut Attribute) {
227            if attr.meta.path().is_ident("cfg_attr") {
228                // Ignore invalid cfg attributes
229                let Meta::List(list) = &attr.meta else { return };
230                let mut token_iter = list.tokens.clone().into_iter();
231
232                // Take all the tokens until the first toplevel comma.
233                // That's the cfg-expression part of cfg_attr.
234                let cfg_expr: TokenStream =
235                    token_iter.by_ref().take_while(tokentree_not_comma).collect();
236
237                let Some(cfg_value) = eval_cfg(cfg_expr) else { return };
238                if cfg_value {
239                    // If we had the whole attribute list and could emit more
240                    // than one attribute, we'd split the remaining arguments to
241                    // cfg_attr by commas and turn them into regular attributes
242                    //
243                    // Because we can emit only one, do the first and error if
244                    // there's any more after it.
245                    let attr_tokens: TokenStream =
246                        token_iter.by_ref().take_while(tokentree_not_comma).collect();
247
248                    if attr_tokens.is_empty() {
249                        // no-op cfg_attr??
250                        return;
251                    }
252
253                    attr.meta = syn::parse2(attr_tokens)
254                        .expect("syn must be able to parse cfg-attr arguments as syn::Meta");
255
256                    let rest: TokenStream = token_iter.collect();
257                    assert!(
258                        rest.is_empty(),
259                        "cfg_attr's with multiple arguments after the cfg expression are not \
260                         currently supported by __internal_macro_expand."
261                    );
262                }
263            }
264        }
265    }
266
267    CfgAttrExpand.visit_item_struct_mut(item);
268
269    let Fields::Named(fields) = &mut item.fields else {
270        panic!("only named fields are currently supported by __internal_macro_expand");
271    };
272
273    // Take out all the fields
274    'fields: for mut field in mem::take(&mut fields.named) {
275        // Take out all the attributes
276        for attr in mem::take(&mut field.attrs) {
277            // For non-cfg attrs, put them back
278            if !attr.meta.path().is_ident("cfg") {
279                field.attrs.push(attr);
280                continue;
281            }
282
283            // Also put back / ignore invalid cfg attributes
284            let Meta::List(list) = &attr.meta else {
285                field.attrs.push(attr);
286                continue;
287            };
288            // Also put back / ignore cfg attributes we can't eval
289            let Some(cfg_value) = eval_cfg(list.tokens.clone()) else {
290                field.attrs.push(attr);
291                continue;
292            };
293
294            // Finally, if the cfg is `false`, skip the part where it's put back
295            if !cfg_value {
296                continue 'fields;
297            }
298        }
299
300        // If `continue 'fields` above wasn't hit, we didn't find a cfg that
301        // evals to false, so put the field back
302        fields.named.push(field);
303    }
304}
305
306/// Helper trait for a [`syn::Field`] belonging to a `struct`.
307pub(crate) trait StructFieldExt {
308    /// Get a reference to the `ident` of this field.
309    ///
310    /// Panics if this is not a named field.
311    fn ident(&self) -> &Ident;
312
313    /// Get the `#[cfg]` attributes on this field.
314    fn cfg_attrs(&self) -> impl Iterator<Item = &'_ Attribute>;
315
316    /// Get the serde meta items on this field, if it has `#[serde(…)]` attributes.
317    fn serde_meta_items(&self) -> impl Iterator<Item = syn::Meta>;
318
319    /// Whether this field has a `#[serde(…)]` containing the given meta item.
320    fn has_serde_meta_item(&self, meta: SerdeMetaItem) -> bool;
321}
322
323impl StructFieldExt for Field {
324    fn ident(&self) -> &Ident {
325        self.ident.as_ref().expect("struct field should be named")
326    }
327
328    fn cfg_attrs(&self) -> impl Iterator<Item = &'_ Attribute> {
329        self.attrs.iter().filter(|a| a.path().is_ident("cfg"))
330    }
331
332    fn serde_meta_items(&self) -> impl Iterator<Item = syn::Meta> {
333        self.attrs.iter().flat_map(AttributeExt::serde_meta_items)
334    }
335
336    fn has_serde_meta_item(&self, meta: SerdeMetaItem) -> bool {
337        self.serde_meta_items().any(|serde_meta| serde_meta == meta)
338    }
339}
340
341/// Possible meta items for `#[serde(…)]` attributes.
342#[derive(Clone, Copy)]
343pub(crate) enum SerdeMetaItem {
344    /// `flatten`.
345    Flatten,
346
347    /// `default`.
348    Default,
349
350    /// `rename`.
351    Rename,
352
353    /// `alias`.
354    Alias,
355}
356
357impl SerdeMetaItem {
358    /// The string representation of this meta item.
359    fn as_str(self) -> &'static str {
360        match self {
361            Self::Flatten => "flatten",
362            Self::Default => "default",
363            Self::Rename => "rename",
364            Self::Alias => "alias",
365        }
366    }
367}
368
369impl PartialEq<SerdeMetaItem> for syn::Meta {
370    fn eq(&self, other: &SerdeMetaItem) -> bool {
371        self.path().is_ident(other.as_str())
372    }
373}
374
375/// Helper trait for a [`syn::Attribute`].
376pub(crate) trait AttributeExt {
377    /// Get the list of meta items if this is a `#[serde(…)]` attribute.
378    fn serde_meta_items(&self) -> impl Iterator<Item = syn::Meta>;
379}
380
381impl AttributeExt for Attribute {
382    fn serde_meta_items(&self) -> impl Iterator<Item = syn::Meta> {
383        if self.path().is_ident("serde")
384            && let syn::Meta::List(list) = &self.meta
385        {
386            list.parse_args_with(Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated).ok()
387        } else {
388            None
389        }
390        .into_iter()
391        .flatten()
392    }
393}
394
395/// Helper trait for a [`syn::Type`].
396pub(crate) trait TypeExt {
397    /// Get the inner type if this is wrapped in an `Option`.
398    fn option_inner_type(&self) -> Option<&syn::Type>;
399
400    /// Whether this type has a lifetime.
401    fn has_lifetime(&self) -> bool;
402}
403
404impl TypeExt for syn::Type {
405    fn option_inner_type(&self) -> Option<&syn::Type> {
406        let syn::Type::Path(syn::TypePath { path: syn::Path { segments, .. }, .. }) = self else {
407            return None;
408        };
409
410        if segments.last().unwrap().ident != "Option" {
411            return None;
412        }
413
414        let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
415            args: option_args,
416            ..
417        }) = &segments.last().unwrap().arguments
418        else {
419            panic!("Option should use angle brackets");
420        };
421        let syn::GenericArgument::Type(inner_type) = option_args.first().unwrap() else {
422            panic!("Option brackets should contain type");
423        };
424
425        Some(inner_type)
426    }
427
428    fn has_lifetime(&self) -> bool {
429        struct Visitor {
430            found_lifetime: bool,
431        }
432
433        impl<'ast> Visit<'ast> for Visitor {
434            fn visit_lifetime(&mut self, _lt: &'ast syn::Lifetime) {
435                self.found_lifetime = true;
436            }
437        }
438
439        let mut vis = Visitor { found_lifetime: false };
440        vis.visit_type(self);
441
442        vis.found_lifetime
443    }
444}
445
446/// Generate code for a list of struct fields.
447///
448/// If the fields have `cfg` attributes, they are also used.
449///
450/// This generates code looking like this for each field:
451///
452/// ```ignore
453/// #[cfg(feature = "my-feature")]
454/// ident,
455/// ```
456pub(crate) fn expand_fields_as_list<'a>(
457    fields: impl IntoIterator<Item = &'a Field>,
458) -> TokenStream {
459    fields
460        .into_iter()
461        .map(|field| {
462            let ident = field.ident();
463            let cfg_attrs = field.cfg_attrs();
464
465            quote! {
466                #( #cfg_attrs )*
467                #ident,
468            }
469        })
470        .collect()
471}
472
473/// Generate code to create variable declarations for the given struct fields.
474///
475/// If the fields have `cfg` attributes, they are also used.
476///
477/// This generates code looking like this for each field:
478///
479/// ```ignore
480/// #[cfg(feature = "my-feature")]
481/// let ident = src.ident;
482/// ```
483pub(crate) fn expand_fields_as_variable_declarations(fields: &[Field], src: &Ident) -> TokenStream {
484    fields
485        .iter()
486        .map(|field| {
487            let ident = field.ident();
488            let cfg_attrs = field.cfg_attrs();
489
490            quote! {
491                #( #cfg_attrs )*
492                let #ident = #src.#ident;
493            }
494        })
495        .collect()
496}
497
498/// Extension trait for [`syn::meta::ParseNestedMeta`].
499pub(crate) trait ParseNestedMetaExt {
500    /// Whether this meta item has a value.
501    fn has_value(&self) -> bool;
502}
503
504impl ParseNestedMetaExt for ParseNestedMeta<'_> {
505    fn has_value(&self) -> bool {
506        !self.input.is_empty() && !self.input.peek(syn::Token![,])
507    }
508}