proc-macro-workshop:sorted-5

审题

// Get ready for a challenging step -- this test case is going to be a much
// bigger change than the others so far.
//
// Not only do we want #[sorted] to assert that variants of an enum are written
// in order inside the enum definition, but also inside match-expressions that
// match on that enum.
//
//     #[sorted]
//     match conference {
//         RustBeltRust => "...",
//         RustConf => "...",
//         RustFest => "...",
//         RustLatam => "...",
//         RustRush => "...",
//     }
//
// Currently, though, procedural macro invocations on expressions are not
// allowed by the stable compiler! To work around this limitation until the
// feature stabilizes, we'll be implementing a new #[sorted::check] macro which
// the user will need to place on whatever function contains such a match.
//
//     #[sorted::check]
//     fn f() {
//         let conference = ...;
//
//         #[sorted]
//         match conference {
//             ...
//         }
//     }
//
// The #[sorted::check] macro will expand by looking inside the function to find
// any match-expressions carrying a #[sorted] attribute, checking the order of
// the arms in that match-expression, and then stripping away the inner
// #[sorted] attribute to prevent the stable compiler from refusing to compile
// the code.
//
// Note that unlike what we have seen in the previous test cases, stripping away
// the inner #[sorted] attribute will require the new macro to mutate the input
// syntax tree rather than inserting it unchanged into the output TokenStream as
// before.
//
// Overall, the steps to pass this test will be:
//
//   - Introduce a new procedural attribute macro called `check`.
//
//   - Parse the input as a syn::ItemFn.
//
//   - Traverse the function body looking for match-expressions. This part will
//     be easiest if you can use the VisitMut trait from Syn and write a visitor
//     with a visit_expr_match_mut method.
//
//   - For each match-expression, figure out whether it has #[sorted] as one of
//     its attributes. If so, check that the match arms are sorted and delete
//     the #[sorted] attribute from the list of attributes.
//
// The result should be that we get the expected compile-time error pointing out
// that `Fmt` should come before `Io` in the match-expression.
//
//
// Resources:
//
//   - The VisitMut trait to iterate and mutate a syntax tree:
//     https://docs.rs/syn/1.0/syn/visit_mut/trait.VisitMut.html
//
//   - The ExprMatch struct:
//     https://docs.rs/syn/1.0/syn/struct.ExprMatch.html

use sorted::sorted;

use std::fmt::{self, Display};
use std::io;

#[sorted]
pub enum Error {
    Fmt(fmt::Error),
    Io(io::Error),
}

impl Display for Error {
    #[sorted::check]
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::Error::*;

        #[sorted]
        match self {
            Io(e) => write!(f, "{}", e),
            Fmt(e) => write!(f, "{}", e),
        }
    }
}

fn main() {}

这里主要有两个惊喜

  • #[sorted::check]:这个和#[tokio::main]何其相像
  • match并不支持函数式宏,但是fn支持
    从错误提示我们可以知道将要实现什么功能
    error: Fmt should sort before Io
    --> tests/05-match-expr.rs:88:13
     |
    88 |             Fmt(e) => write!(f, "{}", e),
     |             ^^^
    就是检测match中的分支顺序。

因此整体的思路是

  1. 通过检测#[sorted::check]标记的方法,查找其中的#[sorted]并进行检测。
  2. 移除#[sorted]保证代码解析正确

抽取

我们要查找的被#[sorted]标记的match,虽然核心是#[sorted],但是match才是主体,且不确定数量,因此,需要开启visit,同时,还要移除#[sorted],所以真正应该开启的是visit-mut

// common.rs
// 递归扫描
impl syn::visit_mut::VisitMut for MatchVisitor {
    fn visit_expr_match_mut(&mut self, i: &mut syn::ExprMatch) {
        // 标记属性
        let mut target_idx: isize = -1;
        for (idx, attr) in i.attrs.iter().enumerate() {
            // 是否标记了#[sorted]
            if path_to_string(&attr.path) == "sorted" {
                target_idx = idx as isize;
                break;
            }
        }
        // 如果标记了才处理
        if target_idx != -1 {
            // 移除#[sorted]
            i.attrs.remove(target_idx as usize);
            // 收集names
            let mut match_arm_names: Vec<(String, &dyn quote::ToTokens)> = Vec::new();
            for arm in i.arms.iter() {
                // 匹配分支
                match &arm.pat {
                    // 路径匹配
                    syn::Pat::Path(p) => {
                        match_arm_names.push((path_to_string(&p.path), &p.path));
                    }
                    // 元组匹配
                    syn::Pat::TupleStruct(p) => {
                        match_arm_names.push((path_to_string(&p.path), &p.path));
                    }
                    // 结构匹配
                    syn::Pat::Struct(p) => {
                        match_arm_names.push((path_to_string(&p.path), &p.path));
                    }
                    // 标识符匹配
                    syn::Pat::Ident(p) => {
                        match_arm_names.push((p.ident.to_string(), &p.ident));
                    }
                    // _ 券匹配
                    syn::Pat::Wild(p) => {
                        match_arm_names.push(("_".to_string(), &p.underscore_token));
                    }
                    // 没啥匹配
                    _ => {
                        self.err = std::option::Option::Some(syn::Error::new_spanned(
                            &arm.pat,
                            "unsupported by #[sorted]",
                        ));
                        return;
                    }
                }
            }
            // 检查错误
            if let Some(e) = check_order(match_arm_names) {
                self.err = std::option::Option::Some(e);
                return;
            }
        }
        // 递归查找
        syn::visit_mut::visit_expr_match_mut(self, i)
    }
}
// 全路径拼接
fn path_to_string(path: &syn::Path) -> String {
    path.segments
        .iter()
        .map(|s| s.ident.to_string())
        .collect::<Vec<String>>()
        .join("::")
}

这里主要从match的角度进行思考,匹配的场景有很多,这里只是列举出了基本的几种。
虽然代码中基本算是TupleStruct,但是其他的情况可能也会有,算是小小的拓展。

还有一点,就是关于visit,之前我们也使用过visit,但是需要主要它的入口和接续。
我们解析这一个之后,其实限定的上下文是在一个fn里面,因此visit_expr_match_mut

题解

// solution5.rs
pub(crate) fn solution(fn_item: &mut syn::ItemFn) -> syn::Result<proc_macro2::TokenStream> {
    let mut visitor = crate::common::MatchVisitor {
        err: std::option::Option::None,
    };
    syn::visit_mut::visit_item_fn_mut(&mut visitor, fn_item);
    match visitor.err {
        Some(e) => syn::Result::Err(e),
        None => syn::Result::Ok(crate::common::to_token_stream(fn_item)),
    }
}

这里可以看出来,visit的入口其实是visit_item_fn_mut
虽然都是递归遍历,不过需要注意入口和接续的遍历位置。

整体

虽然看起来有两个#[sorted],但是不得不打断的是,其中有一个假货。
if path_to_string(&attr.path) == "sorted",这里解析的其实是一个标记,并不具备排序功能,这里真正生效的应该是#[sorted::check],分支下面的#[sorted]就像是派生宏的惰性绑定一样,归属于#[sorted::check]之下。

也就是说,我们实际上还定义了一个宏

mod common;
mod solution1;
mod solution2;
mod solution3;
mod solution5;

#[proc_macro_attribute]
pub fn sorted(
    _args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item = syn::parse_macro_input!(input as syn::Item);
    match solution1::solution(&item) {
        syn::Result::Ok(stream) => stream,
        syn::Result::Err(e) => {
            let mut res = e.into_compile_error();
            res.extend(crate::common::to_token_stream(item));
            res
        }
    }
    .into()
}

#[proc_macro_attribute]
pub fn check(
    _args: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let mut fn_item = syn::parse_macro_input!(input as syn::ItemFn);
    match solution5::solution(&mut fn_item) {
        syn::Result::Ok(stream) => stream,
        syn::Result::Err(e) => {
            let mut res = e.into_compile_error();
            res.extend(crate::common::to_token_stream(fn_item));
            res
        }
    }
    .into()
}

这里的#[sorted::check]还能够给我们一个提示,这里的sorted并非是一个macro,而是一个crate,并不存在关联或者归属的属性宏,固然有惰性绑定的宏,但只不过是一个宏解析时候的标记,绑定的宏并不具备解析入口的功能。

我一直以为#[tokio::main]有什么黑魔法,只不过是长路径。

这里比较容易模糊的是标记枚举#[sorted]和标记match#[sorted]的区别。
对于标记枚举的#[sorted]可以修改为#[sorted::sorted],但是标记match#[sorted]想要修改为#[sorted::sorted],需要修改#[sorted::check]的内部处理方式,因为一个是确定的宏使用不同路劲,而一个是直接更换了解析方式。

开始的时候我还搞混了这两个,还以为check下的sorted解析不完全…

其实,到这里,sorted已经结束了,后续娓娓道来。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!