Skip to main content

is_macro/
lib.rs

1extern crate proc_macro;
2
3use heck::ToSnakeCase;
4use proc_macro2::Span;
5use quote::{quote, ToTokens};
6use syn::{
7    parse,
8    parse::Parse,
9    parse2, parse_quote,
10    punctuated::{Pair, Punctuated},
11    spanned::Spanned,
12    Data, DataEnum, DeriveInput, Expr, ExprLit, Field, Fields, Generics, Ident, ImplItem, ItemImpl,
13    Lit, Meta, MetaNameValue, Path, Token, Type, TypePath, TypeReference, TypeTuple, WhereClause,
14};
15
16/// A proc macro to generate methods like is_variant / expect_variant.
17///
18///
19/// # Example
20///
21/// ```rust
22/// 
23/// use is_macro::Is;
24/// #[derive(Debug, Is)]
25/// pub enum Enum<T> {
26///     A,
27///     B(T),
28///     C(Option<T>),
29/// }
30///
31/// // Rust's type inference cannot handle this.
32/// assert!(Enum::<()>::A.is_a());
33///
34/// assert_eq!(Enum::B(String::from("foo")).b(), Some(String::from("foo")));
35///
36/// assert_eq!(Enum::B(String::from("foo")).expect_b(), String::from("foo"));
37/// ```
38///
39/// # Renaming
40///
41/// ```rust
42/// 
43/// use is_macro::Is;
44/// #[derive(Debug, Is)]
45/// pub enum Enum {
46///     #[is(name = "video_mp4")]
47///     VideoMp4,
48/// }
49///
50/// assert!(Enum::VideoMp4.is_video_mp4());
51/// ```
52#[proc_macro_derive(Is, attributes(is))]
53pub fn is(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
54    let input: DeriveInput = syn::parse(input).expect("failed to parse derive input");
55    let generics: Generics = input.generics.clone();
56
57    let items = match input.data {
58        Data::Enum(e) => expand(e),
59        _ => panic!("`Is` can be applied only on enums"),
60    };
61
62    ItemImpl {
63        attrs: vec![],
64        defaultness: None,
65        unsafety: None,
66        impl_token: Default::default(),
67        generics: Default::default(),
68        trait_: None,
69        self_ty: Box::new(Type::Path(TypePath {
70            qself: None,
71            path: Path::from(input.ident),
72        })),
73        brace_token: Default::default(),
74        items,
75    }
76    .with_generics(generics)
77    .into_token_stream()
78    .into()
79}
80
81#[derive(Debug)]
82struct Input {
83    name: String,
84}
85
86impl Parse for Input {
87    fn parse(input: parse::ParseStream) -> syn::Result<Self> {
88        let _: Ident = input.parse()?;
89        let _: Token![=] = input.parse()?;
90
91        let name = input.parse::<ExprLit>()?;
92
93        Ok(Input {
94            name: match name.lit {
95                Lit::Str(s) => s.value(),
96                _ => panic!("is(name = ...) expects a string literal"),
97            },
98        })
99    }
100}
101
102fn expand(input: DataEnum) -> Vec<ImplItem> {
103    let mut items = vec![];
104
105    for v in &input.variants {
106        let attrs = v
107            .attrs
108            .iter()
109            .filter(|attr| attr.path().is_ident("is"))
110            .collect::<Vec<_>>();
111        if attrs.len() >= 2 {
112            panic!("derive(Is) expects no attribute or one attribute")
113        }
114        let i = match attrs.into_iter().next() {
115            None => Input {
116                name: {
117                    v.ident.to_string().to_snake_case()
118                    //
119                },
120            },
121            Some(attr) => {
122                //
123
124                let mut input = Input {
125                    name: Default::default(),
126                };
127
128                let mut apply = |v: &MetaNameValue| {
129                    assert!(
130                        v.path.is_ident("name"),
131                        "Currently, is() only supports `is(name = 'foo')`"
132                    );
133
134                    input.name = match &v.value {
135                        Expr::Lit(ExprLit {
136                            lit: Lit::Str(s), ..
137                        }) => s.value(),
138                        _ => unimplemented!(
139                            "is(): name must be a string literal but {:?} is provided",
140                            v.value
141                        ),
142                    };
143                };
144
145                match &attr.meta {
146                    Meta::NameValue(v) => {
147                        //
148                        apply(v)
149                    }
150                    Meta::List(l) => {
151                        // Handle is(name = "foo")
152                        input = parse2(l.tokens.clone()).expect("failed to parse input");
153                    }
154                    _ => unimplemented!("is({:?})", attr.meta),
155                }
156
157                input
158            }
159        };
160
161        let name = &*i.name;
162        {
163            let name_of_is = Ident::new(&format!("is_{name}"), v.ident.span());
164            let docs_of_is = format!(
165                "Returns `true` if `self` is of variant [`{variant}`].\n\n[`{variant}`]: \
166                 #variant.{variant}",
167                variant = v.ident,
168            );
169
170            let variant = &v.ident;
171
172            let item_impl: ItemImpl = parse_quote!(
173                impl Type {
174                    #[doc = #docs_of_is]
175                    #[inline]
176                    pub const fn #name_of_is(&self) -> bool {
177                        match *self {
178                            Self::#variant { .. } => true,
179                            _ => false,
180                        }
181                    }
182                }
183            );
184
185            items.extend(item_impl.items);
186        }
187
188        {
189            let name_of_cast = Ident::new(&format!("as_{name}"), v.ident.span());
190            let name_of_cast_mut = Ident::new(&format!("as_mut_{name}"), v.ident.span());
191            let name_of_expect = Ident::new(&format!("expect_{name}"), v.ident.span());
192            let name_of_take = Ident::new(name, v.ident.span());
193
194            let docs_of_cast = format!(
195                "Returns `Some` if `self` is a reference of variant [`{variant}`], and `None` \
196                 otherwise.\n\n[`{variant}`]: #variant.{variant}",
197                variant = v.ident,
198            );
199            let docs_of_cast_mut = format!(
200                "Returns `Some` if `self` is a mutable reference of variant [`{variant}`], and \
201                 `None` otherwise.\n\n[`{variant}`]: #variant.{variant}",
202                variant = v.ident,
203            );
204            let docs_of_expect = format!(
205                "Unwraps the value, yielding the content of [`{variant}`].\n\n# Panics\n\nPanics \
206                 if the value is not [`{variant}`]. In debug builds the panic message includes \
207                 the content of `self`.\n\n[`{variant}`]: #variant.{variant}",
208                variant = v.ident,
209            );
210            let docs_of_take = format!(
211                "Returns `Some` if `self` is of variant [`{variant}`], and `None` \
212                 otherwise.\n\n[`{variant}`]: #variant.{variant}",
213                variant = v.ident,
214            );
215
216            if let Fields::Unnamed(fields) = &v.fields {
217                let types = fields.unnamed.iter().map(|f| f.ty.clone());
218                let cast_ty = types_to_type(types.clone().map(|ty| add_ref(false, ty)));
219                let cast_ty_mut = types_to_type(types.clone().map(|ty| add_ref(true, ty)));
220                let ty = types_to_type(types);
221
222                let mut fields: Punctuated<Ident, Token![,]> = fields
223                    .unnamed
224                    .clone()
225                    .into_pairs()
226                    .enumerate()
227                    .map(|(i, pair)| {
228                        let handle = |f: Field| {
229                            //
230                            Ident::new(&format!("v{i}"), f.span())
231                        };
232                        match pair {
233                            Pair::Punctuated(v, p) => Pair::Punctuated(handle(v), p),
234                            Pair::End(v) => Pair::End(handle(v)),
235                        }
236                    })
237                    .collect();
238
239                // Make sure that we don't have any trailing punctuation
240                // This ensure that if we have a single unnamed field,
241                // we will produce a value of the form `(v)`,
242                // not a single-element tuple `(v,)`
243                if let Some(mut pair) = fields.pop() {
244                    if let Pair::Punctuated(v, _) = pair {
245                        pair = Pair::End(v);
246                    }
247                    fields.extend(std::iter::once(pair));
248                }
249
250                let variant = &v.ident;
251                let expect_panic_arm: syn::Arm = if cfg!(feature = "small-panic") {
252                    parse_quote!(
253                        _ => {
254                            // Omit Debug formatting in release builds to reduce binary size.
255                            #[cfg(debug_assertions)]
256                            panic!(
257                                concat!("called ", stringify!(#name_of_expect), " on {:?}"),
258                                self
259                            );
260                            #[cfg(not(debug_assertions))]
261                            panic!(concat!(
262                                "called ",
263                                stringify!(#name_of_expect),
264                                " on another variant",
265                            ));
266                        }
267                    )
268                } else {
269                    parse_quote!(
270                        _ => panic!(
271                            concat!("called ", stringify!(#name_of_expect), " on {:?}"),
272                            self
273                        )
274                    )
275                };
276                let item_impl: ItemImpl = parse_quote!(
277                    impl #ty {
278                        #[doc = #docs_of_cast]
279                        #[inline]
280                        pub fn #name_of_cast(&self) -> Option<#cast_ty> {
281                            match self {
282                                Self::#variant(#fields) => Some((#fields)),
283                                _ => None,
284                            }
285                        }
286
287                        #[doc = #docs_of_cast_mut]
288                        #[inline]
289                        pub fn #name_of_cast_mut(&mut self) -> Option<#cast_ty_mut> {
290                            match self {
291                                Self::#variant(#fields) => Some((#fields)),
292                                _ => None,
293                            }
294                        }
295
296                        #[doc = #docs_of_expect]
297                        #[inline]
298                        pub fn #name_of_expect(self) -> #ty
299                        where
300                            Self: ::std::fmt::Debug,
301                        {
302                            match self {
303                                Self::#variant(#fields) => (#fields),
304                                #expect_panic_arm
305                            }
306                        }
307
308                        #[doc = #docs_of_take]
309                        #[inline]
310                        pub fn #name_of_take(self) -> Option<#ty> {
311                            match self {
312                                Self::#variant(#fields) => Some((#fields)),
313                                _ => None,
314                            }
315                        }
316                    }
317                );
318
319                items.extend(item_impl.items);
320            }
321        }
322    }
323
324    items
325}
326
327fn types_to_type(types: impl Iterator<Item = Type>) -> Type {
328    let mut types: Punctuated<_, _> = types.collect();
329    if types.len() == 1 {
330        types.pop().expect("len is 1").into_value()
331    } else {
332        TypeTuple {
333            paren_token: Default::default(),
334            elems: types,
335        }
336        .into()
337    }
338}
339
340fn add_ref(mutable: bool, ty: Type) -> Type {
341    Type::Reference(TypeReference {
342        and_token: Default::default(),
343        lifetime: None,
344        mutability: if mutable {
345            Some(Default::default())
346        } else {
347            None
348        },
349        elem: Box::new(ty),
350    })
351}
352
353/// Extension trait for `ItemImpl` (impl block).
354trait ItemImplExt {
355    /// Instead of
356    ///
357    /// ```rust,ignore
358    /// let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
359    ///
360    /// let item: Item = Quote::new(def_site::<Span>())
361    ///     .quote_with(smart_quote!(
362    /// Vars {
363    /// Type: type_name,
364    /// impl_generics,
365    /// ty_generics,
366    /// where_clause,
367    /// },
368    /// {
369    /// impl impl_generics ::swc_common::AstNode for Type ty_generics
370    /// where_clause {}
371    /// }
372    /// )).parse();
373    /// ```
374    ///
375    /// You can use this like
376    ///
377    /// ```rust,ignore
378    // let item = Quote::new(def_site::<Span>())
379    ///     .quote_with(smart_quote!(Vars { Type: type_name }, {
380    ///         impl ::swc_common::AstNode for Type {}
381    ///     }))
382    ///     .parse::<ItemImpl>()
383    ///     .with_generics(input.generics);
384    /// ```
385    fn with_generics(self, generics: Generics) -> Self;
386}
387
388impl ItemImplExt for ItemImpl {
389    fn with_generics(mut self, mut generics: Generics) -> Self {
390        // TODO: Check conflicting name
391
392        let need_new_punct = !generics.params.empty_or_trailing();
393        if need_new_punct {
394            generics
395                .params
396                .push_punct(syn::token::Comma(Span::call_site()));
397        }
398
399        // Respan
400        if let Some(t) = generics.lt_token {
401            self.generics.lt_token = Some(t)
402        }
403        if let Some(t) = generics.gt_token {
404            self.generics.gt_token = Some(t)
405        }
406
407        let ty = self.self_ty;
408
409        // Handle generics defined on struct, enum, or union.
410        let mut item: ItemImpl = {
411            let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
412            let item = if let Some((ref polarity, ref path, ref for_token)) = self.trait_ {
413                quote! {
414                    impl #impl_generics #polarity #path #for_token #ty #ty_generics #where_clause {}
415                }
416            } else {
417                quote! {
418                    impl #impl_generics #ty #ty_generics #where_clause {}
419
420                }
421            };
422            parse2(item.into_token_stream())
423                .unwrap_or_else(|err| panic!("with_generics failed: {}", err))
424        };
425
426        // Handle generics added by proc-macro.
427        item.generics
428            .params
429            .extend(self.generics.params.into_pairs());
430        match self.generics.where_clause {
431            Some(WhereClause {
432                ref mut predicates, ..
433            }) => predicates.extend(
434                generics
435                    .where_clause
436                    .into_iter()
437                    .flat_map(|wc| wc.predicates.into_pairs()),
438            ),
439            ref mut opt @ None => *opt = generics.where_clause,
440        }
441
442        ItemImpl {
443            attrs: self.attrs,
444            defaultness: self.defaultness,
445            unsafety: self.unsafety,
446            impl_token: self.impl_token,
447            brace_token: self.brace_token,
448            items: self.items,
449            ..item
450        }
451    }
452}