trait_map_derive/
lib.rs

1//! # Trait Map Derive
2//!
3//! This crate allows types to derive the [TraitMapEntry](https://docs.rs/trait-map/latest/trait_map/trait.TraitMapEntry.html) trait.
4//! See the [trait_map](https://docs.rs/trait-map/latest/) crate for more details.
5//!
6//! When compiling on nightly, it uses the [`proc_macro_diagnostic`](https://doc.rust-lang.org/beta/unstable-book/library-features/proc-macro-diagnostic.html) feature to emit helpful compiler warnings.
7
8// Allow for compiler warnings if building on nightly
9#![cfg_attr(nightly, feature(proc_macro_diagnostic))]
10
11use std::collections::HashSet;
12
13use ctxt::Ctxt;
14use quote::{quote, quote_spanned, ToTokens};
15use syn::spanned::Spanned;
16use syn::{parse_macro_input, parse_quote, Attribute, DeriveInput, GenericParam, Generics, Meta, NestedMeta, Path};
17
18mod ctxt;
19
20/// Derive macro for the [TraitMapEntry](https://docs.rs/trait-map/latest/trait_map/trait.TraitMapEntry.html) trait.
21///
22/// See the [trait_map](https://docs.rs/trait-map/latest/) crate for more details.
23#[proc_macro_derive(TraitMapEntry, attributes(trait_map))]
24pub fn derive_trait_map_entry(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
25  let derive_input = parse_macro_input!(input as DeriveInput);
26
27  let name = derive_input.ident;
28
29  // Parse any generics on the struct and add T: 'static trait bounds
30  let generics = add_trait_bounds(derive_input.generics);
31  let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
32
33  // Parse the struct attributes to get the trait paths from attributes:
34  //  #[trait_map(TraitOne, TraitTwo)]  =>  vec![TraitOne, TraitTwo]
35  let mut ctx = Ctxt::new();
36  let traits_to_include: Vec<_> = parse_attributes(&mut ctx, derive_input.attrs)
37    .into_iter()
38    .filter_map(|attr| parse_attribute_trait(&mut ctx, attr))
39    .collect();
40
41  // Converts from `#[trait_map(TraitOne)]` into `.add_trait::<dyn TraitOne>()`
42  //
43  // Small optimization: filter duplicate traits.
44  //  Although calls to `.add_trait()` are idempotent, it doesn't hurt to optimize.
45  //
46  // One limitation: macros cannot distinguish imported traits based on type.
47  //  So `MyTrait` and `some::path::MyTrait` are not filtered even if they both
48  //  refer to the same type. This case should be rare anyways.
49  let mut found_traits = HashSet::new();
50  let mut duplicate_traits = Vec::new();
51  let functions: Vec<_> = traits_to_include
52    .into_iter()
53    .filter_map(|t| {
54      if found_traits.contains(&t) {
55        duplicate_traits.push(t);
56        None
57      } else {
58        let span = t.span();
59        let function = quote_spanned!(span => .add_trait::<dyn #t>());
60        found_traits.insert(t);
61        Some(function)
62      }
63    })
64    .collect();
65
66  // Test for any compile errors
67  if let Err(errors) = ctx.check() {
68    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
69    return quote!(#(#compile_errors)*).into();
70  }
71
72  // If building on nightly, show helpful compiler warnings
73  #[cfg(nightly)]
74  {
75    if found_traits.len() == 0 {
76      proc_macro::Span::call_site()
77        .warning("no traits specified for `TraitMapEntry`")
78        .help("specify one or more traits using `#[trait_map(TraitOne, some::path::TraitTwo, ...)]`")
79        .emit();
80    }
81
82    for t in duplicate_traits {
83      let path_str: String = t.to_token_stream().into_iter().map(|t| format!("{}", t)).collect();
84      t.span()
85        .unwrap()
86        .warning(format!("duplicate trait `{}`", path_str))
87        .note("including the same trait multiple times is a no-op")
88        .emit();
89    }
90  }
91
92  quote! {
93    impl #impl_generics trait_map::TraitMapEntry for #name #ty_generics #where_clause {
94      fn on_create<'a>(&mut self, context: trait_map::Context<'a>) {
95        context.downcast::<Self>() #(#functions)* ;
96      }
97    }
98  }
99  .into()
100}
101
102// Add a bound `T: 'static` to every type parameter T.
103fn add_trait_bounds(mut generics: Generics) -> Generics {
104  for param in &mut generics.params {
105    if let GenericParam::Type(ref mut type_param) = *param {
106      type_param.bounds.push(parse_quote!('static));
107    }
108  }
109  generics
110}
111
112/// Get all `#[trait_map(...)]` attributes
113fn parse_attributes(ctx: &mut Ctxt, attributes: Vec<Attribute>) -> Vec<NestedMeta> {
114  attributes
115    .into_iter()
116    // We only want attributes that look like
117    //  #[trait_map(...)]
118    .filter(|attr| attr.path.is_ident("trait_map"))
119    .map(|attr| match attr.parse_meta() {
120      // Valid form:
121      //   #[trait_map(TraitOne, some::path::TraitTwo, ...)]
122      Ok(Meta::List(meta)) => meta.nested.into_iter().collect::<Vec<_>>(),
123
124      // Invalid form:
125      //   #[trait_map = "value"]
126      Ok(other) => {
127        ctx.error_spanned_by(other, "expected #[trait_map(...)]");
128        Vec::new()
129      },
130
131      Err(err) => {
132        ctx.syn_error(err);
133        Vec::new()
134      },
135    })
136    .flatten()
137    .collect()
138}
139
140/// Parse all attributes that look like:
141///
142/// ```
143/// #[trait_map(TraitOne, some::path::TraitTwo)]
144/// ```
145fn parse_attribute_trait(ctx: &mut Ctxt, attr: NestedMeta) -> Option<Path> {
146  match attr {
147    // Valid form:
148    //   trait_map(TraitOne)
149    //   trait_map(some::path::TraitTwo)
150    NestedMeta::Meta(Meta::Path(trait_path)) => Some(trait_path),
151
152    // Invalid forms:
153    //   trait_map("literal")
154    //   trait_map(Key = Value)
155    other => {
156      ctx.error_spanned_by(other, "unexpected attribute, please specify a valid trait");
157      None
158    },
159  }
160}