#macro #proc-macro #lint #collector #context #compose #trying

macro-compose

macro-compose is a library trying to simplify and organize proc-macros

1 unstable release

0.1.0 May 27, 2020

#858 in Procedural macros


Used in 4 crates (3 directly)

WTFPL license

16KB
150 lines

macro-compose is a library trying to simplify and organize proc-macros. It offers traits (Lint, Expand) to split up the macro expansion into multiple smaller, reusable parts and structs the collect the results (Collector, Context).

Example macro

The following subsections show examples for different parts of this library.

The examples are taken from the example macro in examples/enum_from_str_macro which implements a derive macro for FromStr for an enum.

Linting and error handling

The Lint trait is used to lint the macro input. Collector::error can be used to output errors.

Example

use macro_compose::{Collector, Lint};
use syn::{Data, DeriveInput, Error, Fields};

struct EnsureEnumLint;

impl Lint<DeriveInput> for EnsureEnumLint {
    fn lint(&self, input: &DeriveInput, c: &mut Collector) {
        match &input.data {
            Data::Enum(e) => {
                for variant in e.variants.iter() {
                    if variant.fields != Fields::Unit {
                        c.error(Error::new_spanned(&variant.fields, "unexpected fields"))
                    }
                }
            }
            _ => c.error(Error::new_spanned(input, "expected an enum")),
        }
    }
}

Expanding the macro

The Expand trait is used to expand the macro.

Once a Lint or Expand has reported an error to the collector, the macro will no longer be expanded. This way Expand implementations can assume that the data checked by Lints is valid. Returning None from an Expand does not automatically report an error.

Example

use macro_compose::{Collector, Expand};
use proc_macro2::Ident;
use syn::{parse_quote, Arm, Data, DeriveInput, Error, Fields, ItemImpl};

struct ImplFromStrExpand;

impl Expand<DeriveInput> for ImplFromStrExpand {
    type Output = ItemImpl;

    fn expand(&self, input: &DeriveInput, _: &mut Collector) -> Option<Self::Output> {
        let variants = match &input.data {
            Data::Enum(e) => &e.variants,
            _ => unreachable!(),
        };
        let ident = &input.ident;

        let arms = variants.iter().map(|v| -> Arm {
            let v = &v.ident;
            let name = v.to_string();
            parse_quote!(
                #name => ::core::result::Result::Ok(#ident :: #v)
            )
        });

        let ident = &input.ident;
        let error = error_struct_ident(input);
        Some(parse_quote!(
            impl ::core::str::FromStr for #ident {
                type Err = #error;

                fn from_str(s: &::core::primitive::str) -> ::core::result::Result<Self, Self::Err> {
                    match s {
                        #(#arms,)*
                        invalid => ::core::result::Result::Err( #error (::std::string::ToString::to_string(invalid))),
                    }
                }
            }
        ))
    }
}

Implementing the macro

Context::new_parse can be used to create a context from a TokenStream(proc_macro::TokenStream). This Context can be used to run Lints and Expands and get the resulting output.

Example

use macro_compose::{Collector, Context};
use proc_macro::TokenStream;

#[proc_macro_derive(FromStr)]
pub fn derive_from_str(item: TokenStream) -> TokenStream {
    let mut collector = Collector::new();

    let mut ctx = Context::new_parse(&mut collector, item);
    ctx.lint(&EnsureEnumLint);

    ctx.expand(&ErrorStructExpand);
    ctx.expand(&ImplDebugErrorStructExpand);
    ctx.expand(&ImplFromStrExpand);

    collector.finish()
}

Dependencies

~1.5MB
~36K SLoC