#attributes #derive #strip #expansion #macro-derive #macro-expansion #pass

yanked post-expansion

Strip attributes after #[derive(...)] expansion

Uses old Rust 2015

0.2.0 Nov 3, 2016
0.1.0 Oct 20, 2016
0.0.2 Oct 18, 2016
0.0.1 Oct 17, 2016

#22 in #macro-expansion

Download history 50/week @ 2023-12-18 24/week @ 2023-12-25 13/week @ 2024-01-01 45/week @ 2024-01-08 38/week @ 2024-01-15 31/week @ 2024-01-22 18/week @ 2024-01-29 24/week @ 2024-02-05 34/week @ 2024-02-12 33/week @ 2024-02-19 48/week @ 2024-02-26 51/week @ 2024-03-04 55/week @ 2024-03-11 54/week @ 2024-03-18 34/week @ 2024-03-25 124/week @ 2024-04-01

274 downloads per month

MIT/Apache

17KB
190 lines

Why?

Custom derives commonly use attributes to customize the behavior of generated code. For example to control the name of a field when serialized as JSON:

#[derive(Serialize, Deserialize)]
struct Person {
    #[serde(rename = "firstName")]
    first_name: String,
    #[serde(rename = "lastName")]
    last_name: String,
}

In the old compiler plugin infrastructure, plugins were provided with a mechanism to mark attributes as "used" so the compiler would know to ignore them after running the plugin. The new Macros 1.1 infrastructure is deliberately minimal and does not provide this mechanism. Instead, proc macros are expected to strip away attributes after using them. Any unrecognized attributes that remain after proc macro expansion are turned into errors.

This approach causes problems when multiple custom derives want to process the same attributes. For example multiple crates (for JSON, Postgres, and Elasticsearch code generation) may want to standardize on a common rename attribute. If each custom derive is stripping away attributes after using them, subsequent custom derives on the same struct will not see the attributes they should.

This crate provides a way to strip attributes (and possibly other cleanup tasks in the future) in a post-expansion pass that happens after other custom derives have been run.

How?

Suppose #[derive(ElasticType)] wants to piggy-back on Serde's rename attributes for types that are serializable by both Serde and Elasticsearch:

#[derive(Serialize, Deserialize, ElasticType)]
struct Point {
    #[serde(rename = "xCoord")]
    x: f64,
    #[serde(rename = "yCoord")]
    y: f64,
}

A workable but poor solution would be to have Serde's code generation know that ElasticType expects to read the same attributes, so it should not strip attributes when ElasticType is present in the list of derives. An ideal solution would not require Serde's code generation to know anything about other custom derives.

We can handle this by having the Serialize and Deserialize derives register a post-expansion pass to strip the attributes after all other custom derives have been executed. Serde should expand the above code into:

impl Serialize for Point { /* ... */ }
impl Deserialize for Point { /* ... */ }

#[derive(ElasticType)]
#[derive(PostExpansion)] // insert a post-expansion pass after all other derives
#[post_expansion(strip = "serde")] // during post-expansion, strip "serde" attributes
struct Point {
    #[serde(rename = "xCoord")]
    x: f64,
    #[serde(rename = "yCoord")]
    y: f64,
}

Now the ElasticType custom derive can run and see all the right attributes.

impl Serialize for Point { /* ... */ }
impl Deserialize for Point { /* ... */ }
impl ElasticType for Point { /* ... */ }

#[derive(PostExpansion)]
#[post_expansion(strip = "serde")]
struct Point {
    #[serde(rename = "xCoord")]
    x: f64,
    #[serde(rename = "yCoord")]
    y: f64,
}

Once all other derives have been expanded the PostExpansion pass strips the attributes.

impl Serialize for Point { /* ... */ }
impl Deserialize for Point { /* ... */ }
impl ElasticType for Point { /* ... */ }

struct Point {
    x: f64,
    y: f64,
}

There are some complications beyond what is shown in the example. For one, ElasticType needs to register its own post-expansion pass in case somebody does #[derive(ElasticType, Serialize)]. The post-expansion passes from Serde and ElasticType cannot both be called "PostExpansion" because that would be a conflict.

There are also performance considerations. Stripping attributes in a post-expansion pass requires an extra round trip of syn -> tokenstream -> libsyntax -> tokenstream -> syn, which can be avoided if the current custom derive knows that it is the last custom derive.

This crate provides helpers to make the whole process easy, correct, and performant.

How Exactly?

There are two pieces. Proc macros that process attributes need to register a post-expansion pass using the register_post_expansion! macro. During expansion, they need to wire up the custom derive corresponding to the post-expansion pass.

extern crate syn;
#[macro_use]
extern crate post_expansion;

register_post_expansion!(PostExpansion_my_macro);

#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let source = input.to_string();
    let ast = syn::parse_macro_input(&source).unwrap();

    let derived_impl = expand_my_macro(&ast);

    let stripped = post_expansion::strip_attrs_later(ast, &["my_attr"], "my_macro");

    let tokens = quote! {
        #stripped
        #derived_impl
    };

    tokens.to_string().parse().unwrap()
}

Dependencies

~0.4–0.8MB
~19K SLoC