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

已删除 post-expansion

在 #[derive(...)] 扩展后移除属性

使用旧的Rust 2015

0.2.0 2016年11月3日
0.1.0 2016年10月20日
0.0.2 2016年10月18日
0.0.1 2016年10月17日

#macro-expansion 中排名 22

Download history 44/week @ 2024-03-02 59/week @ 2024-03-09 52/week @ 2024-03-16 41/week @ 2024-03-23 56/week @ 2024-03-30 36/week @ 2024-04-06 56/week @ 2024-04-13 55/week @ 2024-04-20 57/week @ 2024-04-27 49/week @ 2024-05-04 48/week @ 2024-05-11 57/week @ 2024-05-18 51/week @ 2024-05-25 53/week @ 2024-06-01 26/week @ 2024-06-08 48/week @ 2024-06-15

每月下载 190

MIT/Apache

17KB
190

为什么?

自定义 derive 通常使用 属性 来自定义生成的代码的行为。例如,控制字段在序列化为 JSON 时的名称

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

在旧的编译器插件基础设施中,插件提供了标记属性为 "已使用" 的机制,以便编译器知道在运行插件后忽略它们。新的 Macros 1.1 基础设施故意保持最小化,并且不提供此机制。相反,过程宏预期在使用后将属性去除。在过程宏扩展后留下的任何未识别的属性都将转换为错误。

当多个自定义 derive 想要处理相同的属性时,这种方法会导致问题。例如,多个 crate(用于 JSON、Postgres 和 Elasticsearch 代码生成)可能希望对公共重命名属性进行标准化。如果每个自定义 derive 在使用属性后都将其去除,则后续的同结构体的自定义 derive 将看不到它们应该看到的属性。

此 crate 提供了一种在运行其他自定义 derive 后进行后扩展遍历的方式,以移除属性(以及未来可能的其他清理任务)。

如何实现?

假设 #[derive(ElasticType)] 想要利用 Serde 的 rename 属性来处理既可通过 Serde 又可通过 Elasticsearch 序列化的类型

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

一个可行但较差的解决方案是让 Serde 的代码生成知道 ElasticType 预期读取相同的属性,因此当 ElasticType 出现在 derive 列表中时不应去除属性。一个理想的解决方案不需要 Serde 的代码生成知道其他自定义 derive 的任何信息。

我们可以通过让 Serialize 和 Deserialize derive 在其他所有自定义 derive 执行后注册一个后扩展遍历来处理属性来解决这个问题。Serde 应将上述代码扩展为

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,
}

现在 ElasticType 自定义 derive 可以运行并看到所有正确的属性。

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,
}

一旦所有其他 derive 都已扩展,PostExpansion 遍历将去除属性。

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

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

示例之外,还存在一些复杂性。例如,ElasticType 需要注册自己的后扩展传递,以防有人执行 #[derive(ElasticType, Serialize)]。由于会存在冲突,Serde 和 ElasticType 的后扩展传递不能都命名为 "PostExpansion"。

还有性能考虑。在后扩展传递中删除属性需要额外的往返过程 syn -> tokenstream -> libsyntax -> tokenstream -> syn,如果当前自定义 derive 知道它是最后一个自定义 derive,则可以避免这种情况。

这个 crate 提供了辅助工具,使整个过程 简单、正确且高效

具体如何实现?

有两个部分。处理属性的 proc macros 需要使用 register_post_expansion! 宏注册后扩展传递。在扩展过程中,它们需要将对应于后扩展传递的自定义 derive 连接起来。

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()
}

依赖项

~365–800KB
~18K SLoC